Compare commits
308 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c37901824 | ||
|
|
0cf8661432 | ||
|
|
ff561157ac | ||
|
|
a7cf9fd74f | ||
|
|
788d24a455 | ||
|
|
f9e3870941 | ||
|
|
3a92fdd0f3 | ||
|
|
6a38defac5 | ||
|
|
3dfa7a8427 | ||
|
|
8dca835a8e | ||
|
|
f2278d47a5 | ||
|
|
de3f195187 | ||
|
|
f33c3ce21a | ||
|
|
67ba424a19 | ||
|
|
3376a08eb8 | ||
|
|
1ae15fe209 | ||
|
|
3c2820a5e1 | ||
|
|
8da6088f10 | ||
|
|
8961a266b3 | ||
|
|
db8ab80bef | ||
|
|
36efc359f7 | ||
|
|
4f4dab143e | ||
|
|
af1e31fb29 | ||
|
|
1f757a7378 | ||
|
|
6028172018 | ||
|
|
2d374160a1 | ||
|
|
587fbfb4ff | ||
|
|
84820a87c8 | ||
|
|
937d417e80 | ||
|
|
5b76cfd4dd | ||
|
|
957ed22b95 | ||
|
|
ebbe89cfb8 | ||
|
|
c8eb6f275f | ||
|
|
5f1213415b | ||
|
|
d36ccb2602 | ||
|
|
486b803856 | ||
|
|
d35630329f | ||
|
|
aa9f742852 | ||
|
|
960906e00c | ||
|
|
432acd2b0e | ||
|
|
84a03a81a0 | ||
|
|
f64791eadd | ||
|
|
51db76ac41 | ||
|
|
1ec7f71b6c | ||
|
|
bb3abdcc48 | ||
|
|
4d3ce038bc | ||
|
|
d66db547cb | ||
|
|
ebf1627753 | ||
|
|
bc818ca9cf | ||
|
|
0827124492 | ||
|
|
fea970f434 | ||
|
|
61cc14939b | ||
|
|
835d8c867e | ||
|
|
62601e4252 | ||
|
|
cc0b482c7a | ||
|
|
ff94f9ca0c | ||
|
|
8cd25dbd4f | ||
|
|
8ecf2e10c8 | ||
|
|
5fb13bb545 | ||
|
|
a24b745f5c | ||
|
|
a127ff8d89 | ||
|
|
d3aa27533a | ||
|
|
35ec9e0063 | ||
|
|
1bac98d086 | ||
|
|
fc2f759dd3 | ||
|
|
f337a13577 | ||
|
|
eebd89aedb | ||
|
|
a9573987ec | ||
|
|
be8c82870f | ||
|
|
e667da8453 | ||
|
|
1258466529 | ||
|
|
8495b223c6 | ||
|
|
8339e4a4cb | ||
|
|
fa7288e03e | ||
|
|
763b6fce6e | ||
|
|
7224acca84 | ||
|
|
e12e7c4170 | ||
|
|
0060d751b6 | ||
|
|
b368463670 | ||
|
|
21eb931701 | ||
|
|
e12b44ab2b | ||
|
|
723dc59490 | ||
|
|
5e1bc3e199 | ||
|
|
857548bbe4 | ||
|
|
56070f3017 | ||
|
|
f553efa69e | ||
|
|
84bf375f72 | ||
|
|
807455eb3d | ||
|
|
57284a9398 | ||
|
|
c8705a8022 | ||
|
|
bbdc4591df | ||
|
|
0a13e7943b | ||
|
|
1e0bc57d32 | ||
|
|
14e6cb05b3 | ||
|
|
b617bb0277 | ||
|
|
ac7b02a434 | ||
|
|
17458f3f4e | ||
|
|
e87fc8a839 | ||
|
|
6cf4d53b7c | ||
|
|
619545b993 | ||
|
|
e75d17f0e1 | ||
|
|
77a5b70576 | ||
|
|
59b0a00c6f | ||
|
|
5d24da4f2b | ||
|
|
0d3e96ee9f | ||
|
|
16480a6c44 | ||
|
|
813a59a36e | ||
|
|
23fd33030d | ||
|
|
2bdce4baa7 | ||
|
|
d2df0b7c84 | ||
|
|
16468b1216 | ||
|
|
3abdee5e87 | ||
|
|
d69a69da94 | ||
|
|
bc806bba34 | ||
|
|
81081ba2d4 | ||
|
|
5ad567803d | ||
|
|
486cafba9d | ||
|
|
c0a1119d1d | ||
|
|
bd4894ebd3 | ||
|
|
e9e95eda0a | ||
|
|
1b3f1ba2e8 | ||
|
|
5e27f981de | ||
|
|
c97f78bb39 | ||
|
|
f7b1032b7a | ||
|
|
7ee2649feb | ||
|
|
281320f2c4 | ||
|
|
30f7c74aee | ||
|
|
2dfd7257dd | ||
|
|
1f9dd5fd8c | ||
|
|
dd83c05a89 | ||
|
|
30cba053da | ||
|
|
5428d3f0b0 | ||
|
|
95398354e3 | ||
|
|
7e73216539 | ||
|
|
bbac1df463 | ||
|
|
208dd209a4 | ||
|
|
9139feaa30 | ||
|
|
f9f6c8b700 | ||
|
|
db8457af60 | ||
|
|
361dd00e9d | ||
|
|
7fd870cfd2 | ||
|
|
967ef99277 | ||
|
|
d93abe8e7d | ||
|
|
a4ba21f9db | ||
|
|
feabb20748 | ||
|
|
31fe06e3ce | ||
|
|
ef86bacf7f | ||
|
|
beabe48aec | ||
|
|
e335f51dbc | ||
|
|
38e422e84c | ||
|
|
1d6d11171d | ||
|
|
e32ced107e | ||
|
|
99d78ce9b8 | ||
|
|
066aff16f1 | ||
|
|
352dc6b311 | ||
|
|
a176ddbf3f | ||
|
|
1c6571d1db | ||
|
|
bf7b35230f | ||
|
|
46d901ada7 | ||
|
|
66f94d9452 | ||
|
|
62f428f434 | ||
|
|
713ad03c3b | ||
|
|
ad2ebc11dd | ||
|
|
72a0c4a487 | ||
|
|
fba5a35514 | ||
|
|
e2e5e40ea9 | ||
|
|
f5660667c8 | ||
|
|
64e225c4aa | ||
|
|
126491fc93 | ||
|
|
ea872d96f8 | ||
|
|
3f7202d89c | ||
|
|
2d3088ba27 | ||
|
|
469b602484 | ||
|
|
aa6f00f927 | ||
|
|
67afd8738b | ||
|
|
98d6170870 | ||
|
|
b2e1e5361f | ||
|
|
f96c80d7a1 | ||
|
|
7ae034d746 | ||
|
|
c409c146bf | ||
|
|
e0a7eb01cc | ||
|
|
3af2136770 | ||
|
|
13a2001a2b | ||
|
|
d6102284a4 | ||
|
|
c56ef8e0da | ||
|
|
bb17609ea3 | ||
|
|
f2f342e14c | ||
|
|
11bd07ee6b | ||
|
|
fa85dcbc4c | ||
|
|
473ae13a03 | ||
|
|
0561a28a4d | ||
|
|
b3467116fe | ||
|
|
e667d10eb8 | ||
|
|
a5bda00cdd | ||
|
|
627bc672bc | ||
|
|
9ef96080a6 | ||
|
|
bf4844e664 | ||
|
|
06f454abcf | ||
|
|
9d4e4a99bc | ||
|
|
b77be76f51 | ||
|
|
3121ed9a95 | ||
|
|
37dfd8fc12 | ||
|
|
de2719b0c5 | ||
|
|
e3d5abc9a2 | ||
|
|
ea7a5da1c1 | ||
|
|
2e063cc2d2 | ||
|
|
943509864d | ||
|
|
2ac228359f | ||
|
|
909f8da2ff | ||
|
|
5a5832394a | ||
|
|
52b60a22fd | ||
|
|
116da64e5c | ||
|
|
9c6c63c167 | ||
|
|
e7284262e4 | ||
|
|
decd9077e4 | ||
|
|
f6c47bf85e | ||
|
|
423191c13b | ||
|
|
7a5928d957 | ||
|
|
bbec3ae7da | ||
|
|
1f7daab677 | ||
|
|
91ab64dda9 | ||
|
|
722705468f | ||
|
|
07c920bad5 | ||
|
|
6d3ef11a7c | ||
|
|
4aabe9d946 | ||
|
|
7247b20686 | ||
|
|
8e8f618a22 | ||
|
|
a50af0ee64 | ||
|
|
436c334f5a | ||
|
|
4f84138ade | ||
|
|
a0d86ac5dc | ||
|
|
d426702213 | ||
|
|
e2be4f1275 | ||
|
|
c97610ad59 | ||
|
|
e8b5845174 | ||
|
|
c295584864 | ||
|
|
36257f73b9 | ||
|
|
9355a5ca24 | ||
|
|
d6447ef311 | ||
|
|
5e2a20fbe0 | ||
|
|
76823f7529 | ||
|
|
96a6a0d980 | ||
|
|
b05701be61 | ||
|
|
962ac97433 | ||
|
|
9491b81d0c | ||
|
|
316f08df08 | ||
|
|
f9554ec761 | ||
|
|
e128b1d750 | ||
|
|
e45efbcfb0 | ||
|
|
847ab96a48 | ||
|
|
1e52f790ad | ||
|
|
9bece712a9 | ||
|
|
6e0678e084 | ||
|
|
579cabdc1a | ||
|
|
9f252dfac4 | ||
|
|
5aad624346 | ||
|
|
23d1109910 | ||
|
|
ae2a72a810 | ||
|
|
ed096c3a1a | ||
|
|
123346ebdb | ||
|
|
8540965696 | ||
|
|
c8568b175b | ||
|
|
1737cbe1a5 | ||
|
|
c81048312d | ||
|
|
ac3afd5695 | ||
|
|
fa84813a37 | ||
|
|
8cd3807100 | ||
|
|
7aeb54d53d | ||
|
|
725ff41fb1 | ||
|
|
d071fe6d0c | ||
|
|
2234a763cb | ||
|
|
d52b65470e | ||
|
|
b63e697934 | ||
|
|
8a036c79c7 | ||
|
|
9a393fa793 | ||
|
|
7614f72df6 | ||
|
|
ef2db78567 | ||
|
|
8708468444 | ||
|
|
cd28a4fbcc | ||
|
|
e49881d1ed | ||
|
|
aa266f9b61 | ||
|
|
ccd3d0a3bf | ||
|
|
69fc367f69 | ||
|
|
a9f24542d5 | ||
|
|
8e4e458a2a | ||
|
|
feec7805af | ||
|
|
19f488095b | ||
|
|
7fc403425d | ||
|
|
ef171bf2af | ||
|
|
b74a6624e3 | ||
|
|
19bf1fe56b | ||
|
|
ea6bb8dca3 | ||
|
|
9d6d3f96b2 | ||
|
|
5967c5d1d5 | ||
|
|
a6017c6ade | ||
|
|
2d3f2667ca | ||
|
|
ed90cadd75 | ||
|
|
91a44980f3 | ||
|
|
034f3c77ce | ||
|
|
5a003a7cbe | ||
|
|
d13d107aea | ||
|
|
4f87796e9c | ||
|
|
837da45f4f | ||
|
|
9e30f05e7d | ||
|
|
098ed6b203 | ||
|
|
c6f9152efe | ||
|
|
2715f47a22 | ||
|
|
90d0b23441 |
12
.jshintrc
@@ -7,8 +7,8 @@
|
||||
// // Enforcing
|
||||
// "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
// "camelcase" : false, // true: Identifiers must be in camelCase
|
||||
// "curly" : true, // true: Require {} for every new block or scope
|
||||
// "eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
@@ -31,7 +31,7 @@
|
||||
// "maxparams" : false, // {int} Max number of formal params allowed per function
|
||||
// "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
|
||||
// "maxstatements" : false, // {int} Max number statements per function
|
||||
"maxcomplexity" : 8, // {int} Max cyclomatic complexity per function
|
||||
"maxcomplexity" : 6, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : 120, // {int} Max number of characters per line
|
||||
//
|
||||
// // Relaxing
|
||||
@@ -89,10 +89,6 @@
|
||||
"after": true,
|
||||
"beforeEach": true,
|
||||
"afterEach": true,
|
||||
"it": true,
|
||||
"suite": true,
|
||||
"suiteSetup": true,
|
||||
"test": true,
|
||||
"suiteTeardown": true
|
||||
"it": true
|
||||
}
|
||||
}
|
||||
|
||||
11
CONTRIBUTING.md
Normal file
@@ -0,0 +1,11 @@
|
||||
Contributing
|
||||
---
|
||||
|
||||
The issue tracker is at [github.com/CartoDB/Windshaft-cartodb](https://github.com/CartoDB/Windshaft-cartodb).
|
||||
|
||||
We love pull requests from everyone, see [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/#contributing).
|
||||
|
||||
|
||||
## Submitting Contributions
|
||||
|
||||
* You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://cartodb.com/contributing).
|
||||
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2014, Vizzuality
|
||||
Copyright (c) 2015, CartoDB
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
||||
24
Makefile
@@ -18,14 +18,26 @@ config.status--test:
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
|
||||
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
|
||||
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")
|
||||
TEST_SUITE_INTEGRATION := $(shell find test/integration -name "*.js")
|
||||
TEST_SUITE_ACCEPTANCE := $(shell find test/acceptance -name "*.js")
|
||||
|
||||
test: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} \
|
||||
test/unit/cartodb/*.js \
|
||||
test/unit/cartodb/cache/model/*.js \
|
||||
test/integration/*.js \
|
||||
test/acceptance/*.js \
|
||||
test/acceptance/cache/*.js
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE)
|
||||
|
||||
test-unit: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_UNIT)
|
||||
|
||||
test-integration: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_INTEGRATION)
|
||||
|
||||
test-acceptance: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_ACCEPTANCE)
|
||||
|
||||
jshint:
|
||||
@echo "***jshint***"
|
||||
|
||||
113
NEWS.md
@@ -1,8 +1,119 @@
|
||||
# Changelog
|
||||
|
||||
## 2.14.0
|
||||
|
||||
Released 2015-09-30
|
||||
|
||||
Summary: this starts using Windshaft as library (aka version 1.0.0), it no longer extends old Windshaft server.
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.0.0](https://github.com/CartoDB/Windshaft/releases/tag/1.0.0)
|
||||
|
||||
New features:
|
||||
- Named tiles: /api/v1/map/named/:name/:layer/:z/:x/:y.:format
|
||||
|
||||
Ported from Windshaft pre-library:
|
||||
- Almost all acceptance tests, some unit and some integration tests
|
||||
- Stats + profiler
|
||||
|
||||
New features:
|
||||
- Named maps MapConfig provider
|
||||
- Base controller with: req2params, send response/error mechanisms
|
||||
- Authentication/Authorization moves to its own API so it can be reused
|
||||
- Surrogate-Key headers for named maps and affected tables
|
||||
|
||||
Improvements:
|
||||
- No more fake requests to emulate map config instantiations
|
||||
- As named maps previews are using named map MapConfigProvider it doesn't need to load the MapConfig
|
||||
- Controllers using Windshaft's backends to request resources through MapConfig providers
|
||||
- Express 4.x, as Windshaft no longer provides an HTTP server, here we start using latest major version of Express.
|
||||
- assert.response implemented using request
|
||||
- All tests validate there are no unexpected keys in Redis and keys requested to be deleted after a test are present
|
||||
- Test suite in Makefile generated with `find`
|
||||
- Image comparison with `mapnik.Image.compare`
|
||||
- Doesn't emit Named map update event on unmodified templates
|
||||
|
||||
TODO:
|
||||
- Named map provider checks on every request if named map has changed to reload it (actually reset it so MapConfig has to be regenerated). See https://github.com/CartoDB/Windshaft-cartodb/commit/f553efa69e83fdf296154ab1b7b49aa08957c04e. This is done this way because when running the Service in a cluster there is no communication between different instances so when a named map gets updated in one of the them the rest is not aware/notified of the change. In the future there should be a mechanism to synch this changes between instance:
|
||||
* endpoint
|
||||
* redis pub/sub
|
||||
* backdoor
|
||||
|
||||
|
||||
## 2.13.0
|
||||
|
||||
Released 2015-09-21
|
||||
|
||||
New features:
|
||||
- Keep x-cache-channel in named map static maps
|
||||
|
||||
|
||||
## 2.12.0
|
||||
|
||||
Released 2015-08-27
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.51.0](https://github.com/CartoDB/Windshaft/releases/tag/0.51.0)
|
||||
|
||||
New features:
|
||||
- Make http and https globalAgent options configurable
|
||||
* If config is not provided it configures them with default values
|
||||
|
||||
|
||||
## 2.11.0
|
||||
|
||||
Released 2015-08-26
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.50.0](https://github.com/CartoDB/Windshaft/releases/tag/0.50.0)
|
||||
|
||||
|
||||
## 2.10.0
|
||||
|
||||
Released 2015-08-18
|
||||
|
||||
New features:
|
||||
- Exposes metatile cache configuration for tilelive-mapnik, see configuration sample files for more information.
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.49.0](https://github.com/CartoDB/Windshaft/releases/tag/0.49.0)
|
||||
|
||||
|
||||
## 2.9.0
|
||||
|
||||
Released 2015-08-06
|
||||
|
||||
New features:
|
||||
- Send memory usage stats
|
||||
|
||||
|
||||
## 2.8.0
|
||||
|
||||
Released 2015-07-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.48.0](https://github.com/CartoDB/Windshaft/releases/tag/0.48.0)
|
||||
|
||||
|
||||
## 2.7.2
|
||||
|
||||
Released 2015-07-14
|
||||
|
||||
Enhancements:
|
||||
- Replaces `CDB_QueryTables` with `CDB_QueryTablesText` to avoid issues with long schema+table names
|
||||
|
||||
|
||||
## 2.7.1
|
||||
|
||||
Released 2015-07-06
|
||||
|
||||
Bug fixes:
|
||||
- redis-mpool `noReadyCheck` and `unwatchOnRelease` options from config and defaulted
|
||||
|
||||
|
||||
## 2.7.0
|
||||
|
||||
Released 2015-07-03
|
||||
Released 2015-07-06
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.47.0](https://github.com/CartoDB/Windshaft/releases/tag/0.47.0)
|
||||
|
||||
@@ -11,8 +11,6 @@ This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api
|
||||
* gets the default geometry type from the cartodb redis store
|
||||
* allows tiles to be styled individually
|
||||
* provides a link to varnish high speed cache
|
||||
* provides a ``infowindow`` endpoint for windshaft (DEPRECATED)
|
||||
* provides a ``map_metadata`` endpoint for windshaft (DEPRECATED)
|
||||
* provides a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
|
||||
|
||||
Requirements
|
||||
@@ -27,7 +25,6 @@ Requirements
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
- For cache control (optional)
|
||||
- CartoDB-SQL-API 1.0.0+
|
||||
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
@@ -95,3 +92,9 @@ Examples
|
||||
--------
|
||||
|
||||
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
|
||||
|
||||
|
||||
Contributing
|
||||
---
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
90
app.js
@@ -1,49 +1,58 @@
|
||||
/*
|
||||
* Windshaft-CartoDB
|
||||
* ===============
|
||||
*
|
||||
* ./app.js [environment]
|
||||
*
|
||||
* environments: [development, production]
|
||||
*/
|
||||
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var ENV;
|
||||
var ENVIRONMENT;
|
||||
if ( process.argv[2] ) {
|
||||
ENV = process.argv[2];
|
||||
ENVIRONMENT = process.argv[2];
|
||||
} else if ( process.env.NODE_ENV ) {
|
||||
ENV = process.env.NODE_ENV;
|
||||
ENVIRONMENT = process.env.NODE_ENV;
|
||||
} else {
|
||||
ENV = 'development';
|
||||
ENVIRONMENT = 'development';
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = ENV;
|
||||
var availableEnvironments = {
|
||||
production: true,
|
||||
staging: true,
|
||||
development: true
|
||||
};
|
||||
|
||||
// sanity check
|
||||
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
|
||||
console.error("\nnode app.js [environment]");
|
||||
console.error("environments: development, production, staging\n");
|
||||
if (!availableEnvironments[ENVIRONMENT]){
|
||||
console.error('node app.js [environment]');
|
||||
console.error('environments: %s', Object.keys(availableEnvironments).join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = ENVIRONMENT;
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require(__dirname + '/config/environments/' + ENV);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
global.environment = require('./config/environments/' + ENVIRONMENT);
|
||||
|
||||
global.log4js = require('log4js');
|
||||
var log4js_config = {
|
||||
appenders: [],
|
||||
replaceConsole:true
|
||||
replaceConsole: true
|
||||
};
|
||||
|
||||
if (global.environment.uv_threadpool_size) {
|
||||
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
|
||||
}
|
||||
|
||||
// set global HTTP and HTTPS agent default configurations
|
||||
// ref https://nodejs.org/api/http.html#http_new_agent_options
|
||||
var agentOptions = _.defaults(global.environment.httpAgent || {}, {
|
||||
keepAlive: false,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: Infinity,
|
||||
maxFreeSockets: 256
|
||||
});
|
||||
http.globalAgent = new http.Agent(agentOptions);
|
||||
https.globalAgent = new https.Agent(agentOptions);
|
||||
|
||||
if ( global.environment.log_filename ) {
|
||||
var logdir = path.dirname(global.environment.log_filename);
|
||||
// See cwd inlog4js.configure call below
|
||||
@@ -65,42 +74,39 @@ if ( global.environment.log_filename ) {
|
||||
global.log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft', unwatchOnRelease: false, noReadyCheck: true }),
|
||||
redisPool = new RedisPool(redisOpts);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
|
||||
// Include cartodb_windshaft only _after_ the "global" variable is set
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
|
||||
var cartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
|
||||
serverOptions = require('./lib/cartodb/server_options')(redisPool);
|
||||
var cartodbWindshaft = require('./lib/cartodb/server');
|
||||
var serverOptions = require('./lib/cartodb/server_options');
|
||||
|
||||
var ws = cartodbWindshaft(serverOptions);
|
||||
|
||||
if (global.statsClient) {
|
||||
redisPool.on('status', function(status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
}
|
||||
var server = cartodbWindshaft(serverOptions);
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good number if you have up to 1024 filedescriptors
|
||||
// 4 is good if you have max 32 filedescriptors
|
||||
// 1 is good if you have max 16 filedescriptors
|
||||
ws.maxConnections = global.environment.maxConnections || 128;
|
||||
var backlog = global.environment.maxConnections || 128;
|
||||
|
||||
ws.listen(global.environment.port, global.environment.host);
|
||||
var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, backlog);
|
||||
|
||||
var version = require("./package").version;
|
||||
|
||||
ws.on('listening', function() {
|
||||
console.log(
|
||||
"Windshaft tileserver %s started on %s:%s (%s)",
|
||||
version, global.environment.host, global.environment.port, ENV
|
||||
);
|
||||
listener.on('listening', function() {
|
||||
console.log(
|
||||
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
|
||||
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
|
||||
);
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
var memoryUsage = process.memoryUsage();
|
||||
Object.keys(memoryUsage).forEach(function(k) {
|
||||
global.statsClient.gauge('windshaft.memory.' + k, memoryUsage[k]);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'development'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -86,8 +89,10 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
@@ -96,6 +101,17 @@ var config = {
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
@@ -146,6 +162,16 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -176,7 +202,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -80,8 +83,10 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
@@ -90,6 +95,17 @@ var config = {
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
@@ -140,6 +156,16 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -170,7 +196,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -80,8 +83,10 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
@@ -90,6 +95,17 @@ var config = {
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
@@ -140,6 +156,16 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -170,7 +196,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'test'
|
||||
,port: 8888
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -80,8 +83,10 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
@@ -90,6 +95,17 @@ var config = {
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
@@ -142,6 +158,16 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -172,7 +198,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: '',
|
||||
|
||||
10
configure
vendored
@@ -20,7 +20,6 @@
|
||||
ENVDIR=config/environments
|
||||
|
||||
PGPORT=
|
||||
SQLAPI_PORT=
|
||||
MAPNIK_VERSION=
|
||||
ENVIRONMENT=development
|
||||
|
||||
@@ -32,7 +31,6 @@ usage() {
|
||||
echo "Configuration:"
|
||||
echo " --help display this help and exit"
|
||||
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM [$PGPORT]"
|
||||
echo " --with-sqlapi-port=NUM access SQL-API server on TCP port NUM [$SQLAPI_PORT]"
|
||||
echo " --with-mapnik-version=STRING set mapnik version string [$MAPNIK_VERSION]"
|
||||
echo " --environment=STRING set output environment name [$ENVIRONMENT]"
|
||||
}
|
||||
@@ -46,9 +44,6 @@ while test -n "$1"; do
|
||||
--with-pgport=*)
|
||||
PGPORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-sqlapi-port=*)
|
||||
SQLAPI_PORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-mapnik-version=*)
|
||||
MAPNIK_VERSION=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
@@ -67,12 +62,8 @@ ENVEX=./${ENVDIR}/${ENVIRONMENT}.js.example
|
||||
if [ -z "$PGPORT" ]; then
|
||||
PGPORT=`node -e "console.log(require('${ENVEX}').postgres.port)"`
|
||||
fi
|
||||
if [ -z "$SQLAPI_PORT" ]; then
|
||||
SQLAPI_PORT=`node -e "console.log(require('${ENVEX}').sqlapi.port)"`
|
||||
fi
|
||||
|
||||
echo "PGPORT: $PGPORT"
|
||||
echo "SQLAPI_PORT: $SQLAPI_PORT"
|
||||
echo "MAPNIK_VERSION: $MAPNIK_VERSION"
|
||||
echo "ENVIRONMENT: $ENVIRONMENT"
|
||||
|
||||
@@ -82,7 +73,6 @@ echo "Writing $o"
|
||||
# See http://austinmatzko.com/2008/04/26/sed-multi-line-search-and-replace/
|
||||
sed -n "1h;1!H;\${;g;s/\(,postgres: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$PGPORT\2/;p;}" < "${ENVEX}" \
|
||||
| sed "s/mapnik_version:.*/mapnik_version: '$MAPNIK_VERSION'/" \
|
||||
| sed -n "1h;1!H;\${;g;s/\(,sqlapi: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$SQLAPI_PORT\2/;p;}" \
|
||||
> "$o"
|
||||
|
||||
STATUSFILE=config.status--${ENVIRONMENT}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
Template maps are layergroup configurations that rather than being
|
||||
fully defined contain variables that can be set to produce a different
|
||||
layergroup configurations (instantiation).
|
||||
|
||||
Template maps are persistent, can only be created and deleted by the
|
||||
CartoDB user showing a valid API_KEY.
|
||||
|
||||
|
||||
# Template format
|
||||
|
||||
A templated layergroup would allow using placeholders
|
||||
in the "cartocss" and "sql" elements in the "option"
|
||||
field of any "layer" of a layergroup configuration
|
||||
(see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification).
|
||||
|
||||
Valid placeholder names start with a letter and can only
|
||||
contain letters, numbers or underscores. They have to be
|
||||
written between ``<%= `` and `` %>`` strings in order to be
|
||||
replaced. Example: ``<%= my_color %>``.
|
||||
|
||||
The set of supported placeholders for a template will need to be
|
||||
explicitly defined specifying type and default value for each.
|
||||
|
||||
**placeholder types**
|
||||
|
||||
Placeholder type will determine the kind of escaping for the
|
||||
associated value. Supported types are:
|
||||
|
||||
* sql_literal (internal single-quotes will be sql-escaped)
|
||||
* sql_ident (internal double-quotes will be sql-escaped)
|
||||
* number (can only contain numerical representation)
|
||||
* css_color (can only contain color names or hex-values)
|
||||
* ... (add more as need arises)
|
||||
|
||||
Placeholder default value will be used when not provided at
|
||||
instantiation time and could be used to test validity of the
|
||||
template by creating a default instance.
|
||||
|
||||
```js
|
||||
// template.json
|
||||
{
|
||||
version: "0.0.1",
|
||||
// there can be at most 1 template with the same name for any user
|
||||
// valid names start with a letter and only contains letter, numbers
|
||||
// or underscores
|
||||
name: "template_name",
|
||||
// embedded authorization certificate
|
||||
auth: {
|
||||
method: "token", // or "open" (the default if no "method" is given)
|
||||
// only (required and non empty) for "token" method
|
||||
valid_tokens: ["auth_token1","auth_token2"]
|
||||
},
|
||||
// Variables not listed here are not substituted
|
||||
// Variable not provided at instantiation time trigger an error
|
||||
// A default is required for optional variables
|
||||
// Type specification is used for quoting, to avoid injections
|
||||
placeholders: {
|
||||
color: {
|
||||
type:"css_color",
|
||||
default:"red"
|
||||
},
|
||||
cartodb_id: {
|
||||
type:"number",
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
// see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Creating a templated map
|
||||
|
||||
You can create a template map with a single call (for simplicity).
|
||||
|
||||
You'd use a POST sending JSON data:
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_id":"@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If a template with the same name exists in the user storage,
|
||||
a 400 response is generated.
|
||||
|
||||
Errors are in this form:
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Updating an existing template
|
||||
|
||||
You can update a template map with a PUT:
|
||||
|
||||
```sh
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template/:template_name?api_key=APIKEY'
|
||||
```
|
||||
A template with the same name will be updated, if any.
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_id":"@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If a template with the same name does NOT exist,
|
||||
a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Listing available templates
|
||||
|
||||
You can get a list of available templates with a GET to ``/template``.
|
||||
A valid api_key is required.
|
||||
|
||||
```sh
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
Or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Getting a specific template
|
||||
|
||||
You can get the definition of a template with a
|
||||
GET to ``/template/:template_name``.
|
||||
A valid api_key is required.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
Or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Instantiating a template map
|
||||
|
||||
You can instantiate a template map passing all required parameters with
|
||||
a POST to ``/template/:template_name``.
|
||||
|
||||
Valid credentials will be needed, if required by the template.
|
||||
|
||||
```js
|
||||
// params.js
|
||||
{
|
||||
color: '#ff0000',
|
||||
cartodb_id: 3
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.js \
|
||||
'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"layergroupid":"docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated":"2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the ``layergroupid`` for fetching tiles and grids as you do
|
||||
normally ( see https://github.com/CartoDB/Windshaft/wiki/Multilayer-API).
|
||||
But you'll still have to show the ``auth_token``, if required by the template.
|
||||
|
||||
### using JSONP
|
||||
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
|
||||
|
||||
```
|
||||
curl 'https://docs.cartodb.com/tiles/template/@template_name/jsonp?auth_token=AUTH_TOKEN&callback=function_name&config=template_params_json'
|
||||
```
|
||||
|
||||
it takes the ``callback`` function (required), ``auth_token`` in case the template needs auth and ``config`` which is the variabñes for the template (in case it has variables). For example config may be created (using javascript)
|
||||
```
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
the response it's in this format:
|
||||
```
|
||||
jQuery17205720721024554223_1390996319118(
|
||||
{
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
}
|
||||
)
|
||||
```
|
||||
# Deleting a template map
|
||||
|
||||
You can delete a templated map with a DELETE to ``/template/:template_name``:
|
||||
|
||||
```sh
|
||||
curl -X DELETE 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response would be issued.
|
||||
Otherwise a 4xx response with this format:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
141
lib/cartodb/api/auth_api.js
Normal file
@@ -0,0 +1,141 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param metadataBackend
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
* @type {AuthApi}
|
||||
*/
|
||||
function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.mapStore = mapStore;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = AuthApi;
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
AuthApi.prototype.authorizedBySigner = function(req, callback) {
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
return callback(null, false); // no signer requested
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
|
||||
this.mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return callback(null, authorized);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param user
|
||||
// @param req express request object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
return callback(null, 0); // no api key, no authorization...
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.metadataBackend.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(err);
|
||||
return val && givenKey === val;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
AuthApi.prototype.authorize = function(req, callback) {
|
||||
var self = this;
|
||||
var user = req.context.user;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.authorizedByAPIKey(user, req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (!authorized) {
|
||||
// not authorized by api_key, check if authorized by signer
|
||||
return self.authorizedBySigner(req, this);
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, authorized) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! authorized ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
return callback(null, true); // authorized so far
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('setDBAuth');
|
||||
}
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -14,7 +14,7 @@ module.exports = QueryTablesApi;
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, callback) {
|
||||
|
||||
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
var query = 'SELECT CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesInQueryRows, callback);
|
||||
};
|
||||
@@ -25,9 +25,9 @@ function handleAffectedTablesInQueryRows(err, rows, callback) {
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var qtables = rows[0].cdb_querytables;
|
||||
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = rows[0].cdb_querytablestext;
|
||||
callback(null, tableNames);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam
|
||||
|
||||
var query = [
|
||||
'WITH querytables AS (',
|
||||
'SELECT * FROM CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
|
||||
'SELECT * FROM CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
|
||||
')',
|
||||
'SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max',
|
||||
'FROM CDB_TableMetadata m',
|
||||
@@ -48,14 +48,14 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam
|
||||
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err || rows.length === 0) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch affected tables and last updated time: ' + msg));
|
||||
callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
var result = rows[0];
|
||||
|
||||
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = result.tablenames;
|
||||
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
@@ -65,6 +65,35 @@ function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getLastUpdatedTime = function (username, tableNames, callback) {
|
||||
if (!Array.isArray(tableNames) || tableNames.length === 0) {
|
||||
return callback(null, 0);
|
||||
}
|
||||
|
||||
var query = [
|
||||
'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max',
|
||||
'FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY[',
|
||||
tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(','),
|
||||
'])'
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleLastUpdatedTimeRows, callback);
|
||||
};
|
||||
|
||||
function handleLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
}
|
||||
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
||||
var lastUpdated = 0;
|
||||
if (rows.length !== 0) {
|
||||
lastUpdated = rows[0].max || 0;
|
||||
}
|
||||
|
||||
return callback(null, lastUpdated*1000);
|
||||
}
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
|
||||
@@ -39,10 +39,9 @@ TablesExtentApi.prototype.getBounds = function (username, tableNames, callback)
|
||||
};
|
||||
|
||||
function handleBoundsResult(err, rows, callback) {
|
||||
if (err){
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
return callback(new Error('could not fetch source tables: ' + msg));
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
|
||||
28
lib/cartodb/api/user_limits_api.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
*
|
||||
* @param metadataBackend
|
||||
* @param options
|
||||
* @constructor
|
||||
* @type {UserLimitsApi}
|
||||
*/
|
||||
function UserLimitsApi(metadataBackend, options) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options || {};
|
||||
this.options.limits = this.options.limits || {};
|
||||
}
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
|
||||
var self = this;
|
||||
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || self.options.limits.render || 0
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
@@ -29,19 +30,21 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
self.metadataBackend.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
if (err) throw err;
|
||||
assert.ifError(err);
|
||||
user_params.user_id = user_id;
|
||||
var dbuser = _.template(auth_user, user_params);
|
||||
_.extend(params, {dbuser:dbuser});
|
||||
|
||||
// skip looking up user_password if postgres_auth_pass
|
||||
// doesn't contain the "user_password" label
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) return null;
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserDBPass(username, this);
|
||||
},
|
||||
function(err, user_password) {
|
||||
if (err) throw err;
|
||||
assert.ifError(err);
|
||||
user_params.user_password = user_password;
|
||||
if ( auth_pass ) {
|
||||
var dbpass = _.template(auth_pass, user_params);
|
||||
@@ -81,12 +84,14 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
|
||||
},
|
||||
function extendParams(err, dbParams){
|
||||
if (err) throw err;
|
||||
assert.ifError(err);
|
||||
// we don't want null values or overwrite a non public user
|
||||
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
|
||||
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
if ( dbParams ) _.extend(params, dbParams);
|
||||
if ( dbParams ) {
|
||||
_.extend(params, dbParams);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
var assert = require('assert');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
@@ -18,15 +19,11 @@ PgQueryRunner.prototype.run = function(username, query, queryHandler, callback)
|
||||
self.pgConnection.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(username, params, this);
|
||||
},
|
||||
function executeQuery(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
var psql = new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
var debug = require('debug')('windshaft:templates');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
@@ -21,7 +23,9 @@ var util = require('util');
|
||||
//
|
||||
//
|
||||
function TemplateMaps(redis_pool, opts) {
|
||||
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
|
||||
if (!(this instanceof TemplateMaps)) {
|
||||
return new TemplateMaps();
|
||||
}
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
@@ -76,13 +80,15 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function executeQuery(err, data) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
|
||||
if ( ! _.isUndefined(redisClient) ) {
|
||||
that.redis_pool.release(db, redisClient);
|
||||
}
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
@@ -92,7 +98,7 @@ var _reValidNameIdentifier = /^[a-z0-9][0-9a-z_\-]*$/i;
|
||||
var _reValidPlaceholderIdentifier = /^[a-z][0-9a-z_]*$/i;
|
||||
// jshint maxcomplexity:15
|
||||
o._checkInvalidTemplate = function(template) {
|
||||
if ( template.version != '0.0.1' ) {
|
||||
if ( template.version !== '0.0.1' ) {
|
||||
return new Error("Unsupported template version " + template.version);
|
||||
}
|
||||
var tplname = template.name;
|
||||
@@ -131,10 +137,12 @@ o._checkInvalidTemplate = function(template) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(auth.valid_tokens) )
|
||||
if ( ! _.isArray(auth.valid_tokens) ) {
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! auth.valid_tokens.length )
|
||||
}
|
||||
if ( ! auth.valid_tokens.length ) {
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + auth.method);
|
||||
@@ -214,9 +222,7 @@ o.addTemplate = function(owner, template, callback) {
|
||||
self._redisCmd('HLEN', [ userTemplatesKey ], this);
|
||||
},
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if ( limit && numberOfTemplates >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates " +
|
||||
"("+ numberOfTemplates + "/" + limit + ")");
|
||||
@@ -224,9 +230,7 @@ o.addTemplate = function(owner, template, callback) {
|
||||
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function validateInstallation(err, wasSet) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if ( ! wasSet ) {
|
||||
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
@@ -259,9 +263,7 @@ o.delTemplate = function(owner, tpl_id, callback) {
|
||||
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
|
||||
},
|
||||
function handleDeletion(err, deleted) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if (!deleted) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
@@ -306,37 +308,38 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
|
||||
var templateName = template.name;
|
||||
|
||||
if ( tpl_id != templateName ) {
|
||||
if ( tpl_id !== templateName ) {
|
||||
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
|
||||
}
|
||||
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
|
||||
var previousTemplate = null;
|
||||
|
||||
step(
|
||||
function getExistingTemplate() {
|
||||
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
|
||||
},
|
||||
function updateTemplate(err, currentTemplate) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!currentTemplate) {
|
||||
function updateTemplate(err, _currentTemplate) {
|
||||
assert.ifError(err);
|
||||
if (!_currentTemplate) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
previousTemplate = _currentTemplate;
|
||||
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function handleTemplateUpdate(err, didSetNewField) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if (didSetNewField) {
|
||||
console.warn('New template created on update operation');
|
||||
debug('New template created on update operation');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
if (self.fingerPrint(JSON.parse(previousTemplate)) !== self.fingerPrint(template)) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, template);
|
||||
@@ -372,7 +375,7 @@ o.getTemplate = function(owner, tpl_id, callback) {
|
||||
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
return JSON.parse(tpl_val);
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
@@ -472,8 +475,12 @@ o.instance = function(template, params) {
|
||||
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
|
||||
for (var i=0; i<layergroup.layers.length; ++i) {
|
||||
var lyropt = layergroup.layers[i].options;
|
||||
if ( lyropt.cartocss ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
|
||||
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
if ( lyropt.cartocss ) {
|
||||
lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
|
||||
}
|
||||
if ( lyropt.sql) {
|
||||
lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
}
|
||||
// Anything else ?
|
||||
}
|
||||
|
||||
24
lib/cartodb/cache/layergroup_affected_tables.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
var LruCache = require('lru-cache');
|
||||
|
||||
function LayergroupAffectedTables() {
|
||||
// dbname + layergroupId -> affected tables cache
|
||||
this.cache = new LruCache({ max: 2000 });
|
||||
}
|
||||
|
||||
module.exports = LayergroupAffectedTables;
|
||||
|
||||
LayergroupAffectedTables.prototype.hasAffectedTables = function(dbName, layergroupId) {
|
||||
return this.cache.has(createKey(dbName, layergroupId));
|
||||
};
|
||||
|
||||
LayergroupAffectedTables.prototype.set = function(dbName, layergroupId, affectedTables) {
|
||||
this.cache.set(createKey(dbName, layergroupId), affectedTables);
|
||||
};
|
||||
|
||||
LayergroupAffectedTables.prototype.get = function(dbName, layergroupId) {
|
||||
return this.cache.get(createKey(dbName, layergroupId));
|
||||
};
|
||||
|
||||
function createKey(dbName, layergroupId) {
|
||||
return dbName + ':' + layergroupId;
|
||||
}
|
||||
24
lib/cartodb/cache/model/database_tables_entry.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
function DatabaseTables(dbName, tableNames) {
|
||||
this.namespace = 't';
|
||||
this.dbName = dbName;
|
||||
this.tableNames = tableNames;
|
||||
}
|
||||
|
||||
module.exports = DatabaseTables;
|
||||
|
||||
|
||||
DatabaseTables.prototype.key = function() {
|
||||
return this.tableNames.map(function(tableName) {
|
||||
return this.namespace + ':' + shortHashKey(this.dbName + ':' + tableName);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
DatabaseTables.prototype.getCacheChannel = function() {
|
||||
return this.dbName + ':' + this.tableNames.join(',');
|
||||
};
|
||||
|
||||
function shortHashKey(target) {
|
||||
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
|
||||
}
|
||||
88
lib/cartodb/cache/named_map_provider_cache.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
|
||||
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
var queue = require('queue-async');
|
||||
|
||||
var LruCache = require("lru-cache");
|
||||
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
this.providerCache = new LruCache({ max: 2000 });
|
||||
}
|
||||
|
||||
module.exports = NamedMapProviderCache;
|
||||
|
||||
NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) {
|
||||
var namedMapKey = createNamedMapKey(user, templateId);
|
||||
var namedMapProviders = this.providerCache.get(namedMapKey) || {};
|
||||
|
||||
var providerKey = createProviderKey(config, authToken, params);
|
||||
if (!namedMapProviders.hasOwnProperty(providerKey)) {
|
||||
namedMapProviders[providerKey] = new NamedMapMapConfigProvider(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.userLimitsApi,
|
||||
this.queryTablesApi,
|
||||
this.namedLayersAdapter,
|
||||
user,
|
||||
templateId,
|
||||
config,
|
||||
authToken,
|
||||
params
|
||||
);
|
||||
this.providerCache.set(namedMapKey, namedMapProviders);
|
||||
|
||||
// early exit, if provider did not exist we just return it
|
||||
return callback(null, namedMapProviders[providerKey]);
|
||||
}
|
||||
|
||||
var namedMapProvider = namedMapProviders[providerKey];
|
||||
|
||||
var self = this;
|
||||
queue(2)
|
||||
.defer(namedMapProvider.getTemplate.bind(namedMapProvider))
|
||||
.defer(this.templateMaps.getTemplate.bind(this.templateMaps), user, templateId)
|
||||
.awaitAll(function templatesQueueDone(err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// We want to reset provider its template has changed
|
||||
// Ideally this should be done in a passive mode where this cache gets notified of template changes
|
||||
var uniqueFingerprints = _.uniq(results.map(self.templateMaps.fingerPrint)).length;
|
||||
if (uniqueFingerprints > 1) {
|
||||
namedMapProvider.reset();
|
||||
}
|
||||
return callback(null, namedMapProvider);
|
||||
});
|
||||
};
|
||||
|
||||
NamedMapProviderCache.prototype.invalidate = function(user, templateId) {
|
||||
this.providerCache.del(createNamedMapKey(user, templateId));
|
||||
};
|
||||
|
||||
function createNamedMapKey(user, templateId) {
|
||||
return user + ':' + templateName(templateId);
|
||||
}
|
||||
|
||||
var providerKey = '{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
|
||||
var providerKeyTpl = dot.template(providerKey);
|
||||
|
||||
function createProviderKey(config, authToken, params) {
|
||||
var tplValues = _.defaults({}, params, {
|
||||
authToken: authToken || '',
|
||||
configHash: NamedMapMapConfigProvider.configHash(config),
|
||||
layer: '',
|
||||
format: '',
|
||||
scale_factor: 1
|
||||
});
|
||||
return providerKeyTpl(tplValues);
|
||||
}
|
||||
14
lib/cartodb/cache/surrogate_keys_cache.js
vendored
@@ -16,9 +16,21 @@ module.exports = SurrogateKeysCache;
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
*/
|
||||
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
|
||||
response.header('Surrogate-Key', cacheObject.key());
|
||||
var newKey = cacheObject.key();
|
||||
response.set('Surrogate-Key', appendSurrogateKey(
|
||||
response.get('Surrogate-Key'),
|
||||
Array.isArray(newKey) ? cacheObject.key().join(' ') : newKey
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
function appendSurrogateKey(currentKey, newKey) {
|
||||
if (!!currentKey) {
|
||||
newKey = currentKey + ' ' + newKey;
|
||||
}
|
||||
return newKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var Windshaft = require('windshaft');
|
||||
var os = require('os');
|
||||
var HealthCheck = require('./monitoring/health_check');
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
|
||||
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
if ( ! process.env.PGAPPNAME )
|
||||
process.env.PGAPPNAME='cartodb_tiler';
|
||||
|
||||
var CartodbWindshaft = function(serverOptions) {
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
if ( global.environment.statsd.prefix ) {
|
||||
var host_token = os.hostname().split('.').reverse().join('.');
|
||||
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
|
||||
}
|
||||
}
|
||||
|
||||
var redisPool = serverOptions.redis.pool ||
|
||||
require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({pool: redisPool});
|
||||
|
||||
var templateMaps = serverOptions.templateMaps;
|
||||
|
||||
// This is for Templated maps
|
||||
//
|
||||
// "named" is the official, "template" is for backward compatibility up to 1.6.x
|
||||
//
|
||||
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
|
||||
|
||||
var surrogateKeysCacheBackends = [];
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
surrogateKeysCacheBackends.push(
|
||||
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
|
||||
);
|
||||
}
|
||||
|
||||
if (!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
|
||||
surrogateKeysCacheBackends.push(
|
||||
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
|
||||
|
||||
function invalidateNamedMap (owner, templateName) {
|
||||
var startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
var logMessage = JSON.stringify({
|
||||
username: owner,
|
||||
type: 'named_map_invalidation',
|
||||
elapsed: Date.now() - startTime,
|
||||
error: !!err ? JSON.stringify(err.message) : undefined
|
||||
});
|
||||
if (err) {
|
||||
console.warn(logMessage);
|
||||
} else {
|
||||
console.info(logMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
// boot
|
||||
var ws = new Windshaft.Server(serverOptions);
|
||||
|
||||
// Override getVersion to include cartodb-specific versions
|
||||
var wsversion = ws.getVersion;
|
||||
ws.getVersion = function() {
|
||||
var version = wsversion();
|
||||
version.windshaft_cartodb = require('../../package.json').version;
|
||||
return version;
|
||||
};
|
||||
|
||||
var ws_sendResponse = ws.sendResponse;
|
||||
// GET routes for which we don't want to request any caching.
|
||||
// POST/PUT/DELETE requests are never cached anyway.
|
||||
var noCacheGETRoutes = [
|
||||
'/',
|
||||
'/version',
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
serverOptions.base_url_mapconfig,
|
||||
serverOptions.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
|
||||
template_baseurl,
|
||||
template_baseurl + '/:template_id',
|
||||
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 NamedMapsController = require('./controllers/named_maps'),
|
||||
namedMapsController = new NamedMapsController(
|
||||
ws,
|
||||
serverOptions,
|
||||
templateMaps,
|
||||
cartoData,
|
||||
template_baseurl,
|
||||
surrogateKeysCache
|
||||
);
|
||||
namedMapsController.register(ws);
|
||||
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(serverOptions.pgQueryRunner);
|
||||
|
||||
var NamedStaticMapsController = require('./controllers/named_static_maps');
|
||||
var namedStaticMapsController = new NamedStaticMapsController(
|
||||
ws,
|
||||
serverOptions,
|
||||
templateMaps,
|
||||
ws.staticMapBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi
|
||||
);
|
||||
namedStaticMapsController.register(ws);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
var healthCheck = new HealthCheck(cartoData, Windshaft.tilelive);
|
||||
ws.get('/health', function(req, res) {
|
||||
var healthConfig = global.environment.health || {};
|
||||
|
||||
if (!!healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
healthCheck.check(healthConfig, function(err, result) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime,
|
||||
result: result
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
res.send(response, ok ? 200 : 503);
|
||||
|
||||
});
|
||||
} else {
|
||||
res.send({enabled: false, ok: true}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
};
|
||||
|
||||
module.exports = CartodbWindshaft;
|
||||
250
lib/cartodb/controllers/base.js
Normal file
@@ -0,0 +1,250 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback'
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
this.authApi = authApi;
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = BaseController;
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
* @param req - standard express request obj. Should have host & table
|
||||
* @param callback
|
||||
*/
|
||||
BaseController.prototype.req2params = function(req, callback){
|
||||
var self = this;
|
||||
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('lzma');
|
||||
}
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
self.req2params(req, callback);
|
||||
} catch (err) {
|
||||
req.profiler.done('req2params');
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = req.context.user;
|
||||
|
||||
if ( req.params.token ) {
|
||||
// Token might match the following patterns:
|
||||
// - {user}@{tpl_id}@{token}:{cache_buster}
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.cache_buster= tksplit[1];
|
||||
}
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) {
|
||||
req.params.signer = user;
|
||||
}
|
||||
else if ( req.params.signer !== user ) {
|
||||
var err = new Error(
|
||||
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
|
||||
);
|
||||
err.http_status = 403;
|
||||
req.profiler.done('req2params');
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
/*var template_hash = */tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('req2params.setup');
|
||||
}
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
self.authApi.authorize(req, this);
|
||||
},
|
||||
function validateAuthorization(err, authorized) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorize');
|
||||
}
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function finishSetup(err) {
|
||||
if ( err ) {
|
||||
req.profiler.done('req2params');
|
||||
return callback(err, req);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
req.profiler.done('req2params');
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
if (req.params.dbhost) {
|
||||
res.set('X-Served-By-DB-Host', req.params.dbhost);
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
|
||||
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
} else {
|
||||
res.send(body);
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
try {
|
||||
// May throw due to dns, see
|
||||
// See http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug("error sending profiling stats: " + err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
label = label || 'UNKNOWN';
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
debug('[%s ERROR] -- %d: %s', label, statusCode, err);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = { errors: [errorMessage(err)] };
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
.replace(/Connection string: '[^']*'\n\s/im, '')
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function findStatusCode(err) {
|
||||
var statusCode;
|
||||
if ( err.http_status ) {
|
||||
statusCode = err.http_status;
|
||||
} else {
|
||||
statusCode = statusFromErrorMessage('' + err);
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
7
lib/cartodb/controllers/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
Layergroup: require('./layergroup'),
|
||||
Map: require('./map'),
|
||||
NamedMaps: require('./named_maps'),
|
||||
NamedMapsAdmin: require('./named_maps_admin'),
|
||||
ServerInfo: require('./server_info')
|
||||
};
|
||||
300
lib/cartodb/controllers/layergroup.js
Normal file
@@ -0,0 +1,300 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
|
||||
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
|
||||
var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
|
||||
/**
|
||||
* @param app
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TileBackend} tileBackend
|
||||
* @param {PreviewBackend} previewBackend
|
||||
* @param {AttributesBackend} attributesBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {QueryTablesApi} queryTablesApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(app, authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, queryTablesApi, layergroupAffectedTables) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.app = app;
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
}
|
||||
|
||||
util.inherits(LayergroupController, BaseController);
|
||||
|
||||
module.exports = LayergroupController;
|
||||
|
||||
|
||||
LayergroupController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), this.tile.bind(this));
|
||||
app.get(app.base_url_mapconfig + '/:token/:z/:x/:y.:format', cors(), this.tile.bind(this));
|
||||
app.get(app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)', cors(), this.layer.bind(this));
|
||||
app.get(app.base_url_mapconfig + '/:token/:layer/attributes/:fid', cors(), this.attributes.bind(this));
|
||||
app.get(app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(),
|
||||
this.center.bind(this));
|
||||
app.get(app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(),
|
||||
this.bbox.bind(this));
|
||||
};
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveFeatureAttributes(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET ATTRIBUTES');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.tile = function(req, res) {
|
||||
req.profiler.start('windshaft.map_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
};
|
||||
|
||||
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.layer = function(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next();
|
||||
}
|
||||
req.profiler.start('windshaft.maplayer_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function mapController$prepareParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function mapController$getTileOrGrid(err) {
|
||||
assert.ifError(err);
|
||||
self.tileBackend.getTile(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
req.params, this
|
||||
);
|
||||
},
|
||||
function mapController$finalize(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// This function is meant for being called as the very last
|
||||
// step by all endpoints serving tiles or grids
|
||||
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
|
||||
var supportedFormats = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true
|
||||
};
|
||||
|
||||
var formatStat = 'invalid';
|
||||
if (req.params.format) {
|
||||
var format = req.params.format.replace('.', '_');
|
||||
if (supportedFormats[format]) {
|
||||
formatStat = format;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
if (matches) {
|
||||
errMsg = 'style'+matches[2]+': ' + matches[1];
|
||||
}
|
||||
err.message = errMsg;
|
||||
|
||||
this.sendError(req, res, err, 'TILE RENDER');
|
||||
global.statsClient.increment('windshaft.tiles.error');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
|
||||
} else {
|
||||
this.sendResponse(req, res, tile, 200, headers);
|
||||
global.statsClient.increment('windshaft.tiles.success');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
|
||||
}
|
||||
};
|
||||
|
||||
LayergroupController.prototype.bbox = function(req, res) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
});
|
||||
};
|
||||
|
||||
LayergroupController.prototype.center = function(req, res) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
});
|
||||
};
|
||||
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.layer = 'all';
|
||||
req.params.format = 'png';
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getImage(err) {
|
||||
assert.ifError(err);
|
||||
if (center) {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
format, width, height, zoom, center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
format, width, height, zoom /* bounds */, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_MAP');
|
||||
} else {
|
||||
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
self.sendResponse(req, res, image, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
|
||||
var self = this;
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if (req.params.cache_buster) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
step(
|
||||
function getAffectedTables() {
|
||||
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
|
||||
},
|
||||
function sendResponse(err, affectedTables) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!!affectedTables) {
|
||||
var tablesCacheEntry = new TablesCacheEntry(dbName, affectedTables);
|
||||
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, tablesCacheEntry);
|
||||
}
|
||||
self.send(req, res, body, status, headers);
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {
|
||||
|
||||
if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
|
||||
return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
step(
|
||||
function extractSQL() {
|
||||
step(
|
||||
function loadFromStore() {
|
||||
self.mapStore.load(layergroupId, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = mapConfig.getLayers()
|
||||
.map(function(lyr) {
|
||||
return lyr.options.sql;
|
||||
})
|
||||
.filter(function(sql) {
|
||||
return !!sql;
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
self.queryTablesApi.getAffectedTablesInQuery(user, sql, this); // in addCacheChannel
|
||||
},
|
||||
function buildCacheChannel(err, tableNames) {
|
||||
assert.ifError(err);
|
||||
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, tableNames);
|
||||
|
||||
return tableNames;
|
||||
},
|
||||
function finish(err, affectedTables) {
|
||||
callback(err, affectedTables);
|
||||
}
|
||||
);
|
||||
};
|
||||
327
lib/cartodb/controllers/map.js
Normal file
@@ -0,0 +1,327 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var windshaft = require('windshaft');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
|
||||
var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
|
||||
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_layergroup_provider');
|
||||
|
||||
/**
|
||||
* @param app
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {QueryTablesApi} queryTablesApi
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(app, authApi, pgConnection, templateMaps, mapBackend, metadataBackend, queryTablesApi,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.app = app;
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
|
||||
module.exports = MapController;
|
||||
|
||||
|
||||
MapController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig, cors(), this.createGet.bind(this));
|
||||
app.post(app.base_url_mapconfig, cors(), this.createPost.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), this.jsonp.bind(this));
|
||||
app.post(app.base_url_templated + '/:template_id', cors(), this.instantiate.bind(this));
|
||||
app.options(app.base_url_mapconfig, cors('Content-Type'));
|
||||
};
|
||||
|
||||
MapController.prototype.createGet = function(req, res){
|
||||
req.profiler.start('windshaft.createmap_get');
|
||||
|
||||
this.create(req, res, function createGet$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if ( ! req.params.config ) {
|
||||
throw new Error('layergroup GET needs a "config" parameter');
|
||||
}
|
||||
return JSON.parse(req.params.config);
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.createPost = function(req, res) {
|
||||
req.profiler.start('windshaft.createmap_post');
|
||||
|
||||
this.create(req, res, function createPost$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error('layergroup POST data must be of type application/json');
|
||||
}
|
||||
return req.body;
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.instantiate = function(req, res) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
|
||||
if (!req.is('application/json')) {
|
||||
return callback(new Error('Template POST data must be of type application/json'));
|
||||
}
|
||||
return callback(null, req.body);
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.jsonp = function(req, res) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
|
||||
var err = null;
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
err = new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
err = new Error('Invalid config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return callback(err, templateParams);
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
var self = this;
|
||||
|
||||
var mapConfig;
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
prepareConfigFn,
|
||||
function beforeLayergroupCreate(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.namedLayersAdapter.getLayers(req.context.user, requestMapConfig.layers, self.pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
requestMapConfig.layers = layers;
|
||||
}
|
||||
return next(null, requestMapConfig, datasource);
|
||||
}
|
||||
);
|
||||
},
|
||||
function createLayergroup(err, requestMapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
mapConfig = new MapConfig(requestMapConfig, datasource || Datasource.EmptyDatasource());
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, req.params,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
var mapConfigProvider;
|
||||
var mapConfig;
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getTemplateParams() {
|
||||
prepareParamsFn(this);
|
||||
},
|
||||
function getTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
self.templateMaps,
|
||||
self.pgConnection,
|
||||
self.userLimitsApi,
|
||||
self.queryTablesApi,
|
||||
self.namedLayersAdapter,
|
||||
cdbuser,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
req.query.auth_token,
|
||||
req.params
|
||||
);
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams/*, context*/) {
|
||||
assert.ifError(err);
|
||||
mapConfig = mapConfig_;
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, rendererParams,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
|
||||
} else {
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
|
||||
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, callback) {
|
||||
var self = this;
|
||||
|
||||
var username = req.context.user;
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err, layergroup);
|
||||
}
|
||||
};
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
_.extend(layergroup, global.environment.serverMetadata);
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asynchronously
|
||||
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
}
|
||||
if ( err ) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var sql = mapconfig.getLayers().map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var layergroupId = layergroup.layergroupid;
|
||||
|
||||
step(
|
||||
function checkCachedAffectedTables() {
|
||||
return self.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId);
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, hasCache) {
|
||||
assert.ifError(err);
|
||||
if (hasCache) {
|
||||
var next = this;
|
||||
var affectedTables = self.layergroupAffectedTables.get(dbName, layergroupId);
|
||||
self.queryTablesApi.getLastUpdatedTime(username, affectedTables, function(err, lastUpdatedTime) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next(null, { affectedTables: affectedTables, lastUpdatedTime: lastUpdatedTime });
|
||||
});
|
||||
} else {
|
||||
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
|
||||
}
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
assert.ifError(err);
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, result.affectedTables);
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + result.lastUpdatedTime;
|
||||
layergroup.last_updated = new Date(result.lastUpdatedTime).toISOString();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
var tableCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
res.set('X-Cache-Channel', tableCacheEntry.getCacheChannel());
|
||||
if (result.affectedTables && result.affectedTables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, tableCacheEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,372 +1,278 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var templateName = require('../template_maps').templateName;
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
function NamedMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache) {
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
|
||||
var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
|
||||
function NamedMapsController(app, authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.templateBaseUrl = templateBaseUrl;
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsController, BaseController);
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
NamedMapsController.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
|
||||
NamedMapsController.prototype.create = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self.app, res, 'POST TEMPLATE')
|
||||
app.get(app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)', cors(), this.tile.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', cors(), this.staticMap.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
NamedMapsController.prototype.update = function(req, res) {
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
var dbName = req.params.dbname;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self.app, res, 'PUT TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
// Get a specific template
|
||||
NamedMapsController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
function sendResponse(err, result) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.log('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self.app, res, 'GET TEMPLATE')
|
||||
);
|
||||
};
|
||||
if (!result || !!result.affectedTables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Delete a specific template
|
||||
NamedMapsController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(result.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(result.lastUpdatedTime);
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
finishFn(self.app, res, 'DELETE TEMPLATE', ['', 204])
|
||||
);
|
||||
};
|
||||
|
||||
// Get a list of owned templates
|
||||
NamedMapsController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
if ( req.profiler ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self.app, res, 'GET TEMPLATE LIST')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.instantiate = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
step(
|
||||
function() {
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
|
||||
self.instantiateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.options = function(req, res, next) {
|
||||
this.app.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
};
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instantiate a template with a json call.
|
||||
* callback query argument is mandatory
|
||||
*/
|
||||
NamedMapsController.prototype.jsonp = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
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');
|
||||
var tablesCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
|
||||
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
|
||||
if (result.affectedTables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, tablesCacheEntry);
|
||||
}
|
||||
}
|
||||
self.instantiateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res);
|
||||
self.send(req, res, resource, 200);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Instantiate a template
|
||||
NamedMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
|
||||
NamedMapsController.prototype.tile = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
var cdbUser = req.context.user;
|
||||
|
||||
var template;
|
||||
var layergroup;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = templateName(req.params.template_id);
|
||||
var auth_token = req.query.auth_token;
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function getTemplate(){
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
function reqParams() {
|
||||
self.req2params(req, 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;
|
||||
}
|
||||
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
this
|
||||
);
|
||||
},
|
||||
function getTile(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
namedMapProvider = _namedMapProvider;
|
||||
self.tileBackend.getTile(namedMapProvider, req.params, this);
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByCert');
|
||||
req.profiler.add(stats);
|
||||
}
|
||||
|
||||
return self.templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = {
|
||||
query: {},
|
||||
params: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.serverOptions.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 ) {
|
||||
return callback(err, { errors: [''+err] });
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED_MAP_TILE');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, headers, namedMapProvider);
|
||||
}
|
||||
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
|
||||
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
|
||||
res.header('X-Layergroup-Id', layergroup.layergroupid);
|
||||
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name));
|
||||
|
||||
callback(null, layergroup);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.finish_instantiation = function(err, response, res) {
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { errors: [''+err] };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = req.context.user;
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.format = 'png';
|
||||
req.params.layer = 'all';
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
this
|
||||
);
|
||||
},
|
||||
function prepareImageOptions(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
namedMapProvider = _namedMapProvider;
|
||||
self.getStaticImageOptions(cdbUser, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function incrementMapViews(err, image, headers, stats) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getMapConfig(function(mapConfigErr, mapConfig) {
|
||||
self.metadataBackend.incMapviewCount(cdbUser, mapConfig.obj().stat_tag, function(sErr) {
|
||||
if (err) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '%s': %s", cdbUser, sErr);
|
||||
}
|
||||
next(err, image, headers, stats);
|
||||
});
|
||||
});
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
|
||||
} else {
|
||||
self.sendResponse(req, res, image, headers, namedMapProvider);
|
||||
}
|
||||
}
|
||||
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
this.app.sendResponse(res, [response, 200]);
|
||||
);
|
||||
};
|
||||
|
||||
var DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
function finishFn(app, res, description, okResponse) {
|
||||
return function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err) {
|
||||
statusCode = 400;
|
||||
response = { errors: ['' + err] };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMapProvider, callback) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
namedMapProvider.getTemplate(this);
|
||||
},
|
||||
function handleTemplateView(err, template) {
|
||||
assert.ifError(err);
|
||||
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
app.sendError(res, response, statusCode, description, err);
|
||||
} else {
|
||||
app.sendResponse(res, okResponse || [response, statusCode]);
|
||||
|
||||
return false;
|
||||
},
|
||||
function estimateBoundsIfNoImageOpts(err, imageOpts) {
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
|
||||
if (err) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.affectedTables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
|
||||
return next(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
function returnCallback(err, imageOpts) {
|
||||
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
|
||||
}
|
||||
};
|
||||
);
|
||||
};
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (authenticated !== 1) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' ) {
|
||||
throw new Error(description);
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return Number.isFinite(view.bounds[prop]);
|
||||
});
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
201
lib/cartodb/controllers/named_maps_admin.js
Normal file
@@ -0,0 +1,201 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
|
||||
|
||||
/**
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @constructor
|
||||
*/
|
||||
function NamedMapsAdminController(templateMaps, authApi, pgConnection) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.templateMaps = templateMaps;
|
||||
this.authApi = authApi;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsAdminController, BaseController);
|
||||
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function(app) {
|
||||
app.post(app.base_url_templated, cors(), this.create.bind(this));
|
||||
app.put(app.base_url_templated + '/:template_id', cors(), this.update.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id', cors(), this.retrieve.bind(this));
|
||||
app.delete(app.base_url_templated + '/:template_id', cors(), this.destroy.bind(this));
|
||||
app.get(app.base_url_templated, cors(), this.list.bind(this));
|
||||
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function getTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
assert.ifError(err);
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function deleteTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
if ( req.profiler ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST')
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status) {
|
||||
return function finish(err, response){
|
||||
if (err) {
|
||||
controller.sendError(req, res, err, description);
|
||||
} else {
|
||||
controller.send(req, res, response, status || 200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (!authenticated) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error(description);
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../template_maps').templateName;
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var _ = require('underscore');
|
||||
|
||||
function NamedStaticMapsController(app, serverOptions, templateMaps, staticMapBackend, surrogateKeysCache,
|
||||
tablesExtentApi) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.staticMapBackend = staticMapBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
}
|
||||
|
||||
module.exports = NamedStaticMapsController;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
NamedStaticMapsController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', this.named.bind(this));
|
||||
};
|
||||
|
||||
NamedStaticMapsController.prototype.named = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
var cdbUser = cdbRequest.userByReq(req);
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
var template;
|
||||
var layergroupConfig;
|
||||
var layergroupId;
|
||||
var fakeReq;
|
||||
var cacheChannel;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.app.req2params(req, this);
|
||||
},
|
||||
function getTemplate(err) {
|
||||
assert.ifError(err);
|
||||
self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this);
|
||||
},
|
||||
function checkExists(err, tpl) {
|
||||
assert.ifError(err);
|
||||
if (!tpl) {
|
||||
var notFoundErr = new Error(
|
||||
"Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found"
|
||||
);
|
||||
notFoundErr.http_status = 404;
|
||||
throw notFoundErr;
|
||||
}
|
||||
return tpl;
|
||||
},
|
||||
function checkAuthorized(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
var authorizationFailedErr = new Error('Failed to authorize template');
|
||||
authorizationFailedErr.http_status = 403;
|
||||
throw authorizationFailedErr;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
var unauthorizedErr = new Error('Unauthorized template instantiation');
|
||||
unauthorizedErr.http_status = 403;
|
||||
throw unauthorizedErr;
|
||||
}
|
||||
|
||||
return tpl;
|
||||
},
|
||||
function prepareParams(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
template = tpl;
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch (e) {
|
||||
throw new Error('malformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return templateParams;
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(template, templateParams);
|
||||
},
|
||||
function prepareLayergroup(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
layergroupConfig = layergroup;
|
||||
fakeReq = {
|
||||
query: {},
|
||||
params: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.serverOptions.setDBParams(cdbUser, fakeReq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
assert.ifError(err);
|
||||
self.app.createLayergroup(layergroupConfig, fakeReq, this);
|
||||
},
|
||||
function prepareResponse(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
|
||||
// added by createLayergroup
|
||||
cacheChannel = res.header('X-Cache-Channel');
|
||||
res.removeHeader('X-Cache-Channel');
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name));
|
||||
|
||||
layergroupId = layergroup.layergroupid.split(":")[0];
|
||||
|
||||
return null;
|
||||
},
|
||||
function staticImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
getStaticImageOptions(template, this);
|
||||
},
|
||||
function estimateBounds(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
var defaultZoomCenter = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
var dbTables = cacheChannel.split(':');
|
||||
if (dbTables.length <= 1 || dbTables[1].length === 0) {
|
||||
return defaultZoomCenter;
|
||||
}
|
||||
|
||||
var tableNames = dbTables[1].split(',');
|
||||
if (tableNames.length === 0) {
|
||||
return defaultZoomCenter;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
self.tablesExtentApi.getBounds(cdbUser, tableNames, function(err, result) {
|
||||
next(null, result || defaultZoomCenter);
|
||||
});
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var staticImageReq = {
|
||||
headers: _.clone(fakeReq.headers),
|
||||
params: _.extend(_.clone(fakeReq.params), {
|
||||
token: layergroupId,
|
||||
format: req.params.format
|
||||
})
|
||||
};
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (!err.error) {
|
||||
err.error = err.message;
|
||||
}
|
||||
self.app.sendError(res, err, self.app.findStatusCode(err), 'STATIC_VIZ_MAP', err);
|
||||
} else {
|
||||
res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
self.app.sendResponse(res, [image, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getStaticImageOptions(template, callback) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
return callback(null, zoomCenter);
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return callback(null, bounds);
|
||||
}
|
||||
}
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return !!view.bounds[prop];
|
||||
});
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
56
lib/cartodb/controllers/server_info.js
Normal file
@@ -0,0 +1,56 @@
|
||||
var windshaft = require('windshaft');
|
||||
var HealthCheck = require('../monitoring/health_check');
|
||||
|
||||
var WELCOME_MSG = "This is the CartoDB Maps API, " +
|
||||
"see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html";
|
||||
|
||||
|
||||
var versions = {
|
||||
windshaft: windshaft.version,
|
||||
grainstore: windshaft.grainstore.version(),
|
||||
node_mapnik: windshaft.mapnik.version,
|
||||
mapnik: windshaft.mapnik.versions.mapnik,
|
||||
windshaft_cartodb: require('../../../package.json').version
|
||||
};
|
||||
|
||||
function ServerInfoController() {
|
||||
this.healthConfig = global.environment.health || {};
|
||||
this.healthCheck = new HealthCheck(global.environment.disabled_file);
|
||||
}
|
||||
|
||||
module.exports = ServerInfoController;
|
||||
|
||||
ServerInfoController.prototype.register = function(app) {
|
||||
app.get('/health', this.health.bind(this));
|
||||
app.get('/', this.welcome.bind(this));
|
||||
app.get('/version', this.version.bind(this));
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.welcome = function(req, res) {
|
||||
res.status(200).send(WELCOME_MSG);
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.version = function(req, res) {
|
||||
res.status(200).send(versions);
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.health = function(req, res) {
|
||||
if (!!this.healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
this.healthCheck.check(function(err) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
res.status(ok ? 200 : 503).send(response);
|
||||
|
||||
});
|
||||
} else {
|
||||
res.status(200).send({enabled: false, ok: true});
|
||||
}
|
||||
};
|
||||
11
lib/cartodb/middleware/cors.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = function cors(extraHeaders) {
|
||||
return function(req, res, next) {
|
||||
var baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
|
||||
if(extraHeaders) {
|
||||
baseHeaders += ", " + extraHeaders;
|
||||
}
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.set("Access-Control-Allow-Headers", baseHeaders);
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -14,12 +14,11 @@ CdbRequest.prototype.userByReq = function(req) {
|
||||
}
|
||||
var mat = host.match(this.RE_USER_FROM_HOST);
|
||||
if ( ! mat ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' does not match hostname '" + host + "'");
|
||||
global.logger.error("Pattern '%s' does not match hostname '%s'", this.RE_USER_FROM_HOST, host);
|
||||
return;
|
||||
}
|
||||
// console.log("Matches: "); console.dir(mat);
|
||||
if ( mat.length !== 2 ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' gave unexpected matches against '" + host + "': ", mat);
|
||||
global.logger.error("Pattern '%s' gave unexpected matches against '%s': %s", this.RE_USER_FROM_HOST, host, mat);
|
||||
return;
|
||||
}
|
||||
return mat[1];
|
||||
|
||||
29
lib/cartodb/models/layergroup_token.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @param {String} token might match the following pattern: {user}@{tpl_id}@{token}:{cache_buster}
|
||||
*/
|
||||
function parse(token) {
|
||||
var signer, cacheBuster;
|
||||
|
||||
var tokenSplit = token.split(':');
|
||||
|
||||
token = tokenSplit[0];
|
||||
if (tokenSplit.length > 1) {
|
||||
cacheBuster = tokenSplit[1];
|
||||
}
|
||||
|
||||
tokenSplit = token.split('@');
|
||||
if ( tokenSplit.length > 1 ) {
|
||||
signer = tokenSplit.shift();
|
||||
if ( tokenSplit.length > 1 ) {
|
||||
/*var template_hash = */tokenSplit.shift(); // unused
|
||||
}
|
||||
token = tokenSplit.shift();
|
||||
}
|
||||
|
||||
return {
|
||||
token: token,
|
||||
signer: signer,
|
||||
cacheBuster: cacheBuster
|
||||
};
|
||||
}
|
||||
module.exports.parse = parse;
|
||||
48
lib/cartodb/models/mapconfig/create_layergroup_provider.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var MapStoreMapConfigProvider = require('./map_store_provider');
|
||||
|
||||
/**
|
||||
* @param {MapConfig} mapConfig
|
||||
* @param {String} user
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {Object} params
|
||||
* @constructor
|
||||
* @type {CreateLayergroupMapConfigProvider}
|
||||
*/
|
||||
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, params) {
|
||||
this.mapConfig = mapConfig;
|
||||
this.user = user;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.params = params;
|
||||
this.cacheBuster = params.cache_buster || 0;
|
||||
}
|
||||
|
||||
module.exports = CreateLayergroupMapConfigProvider;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var self = this;
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
context.limits = renderLimits;
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
return callback(err, self.mapConfig, self.params, context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getKey = MapStoreMapConfigProvider.prototype.getKey;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getCacheBuster = MapStoreMapConfigProvider.prototype.getCacheBuster;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey;
|
||||
77
lib/cartodb/models/mapconfig/map_store_provider.js
Normal file
@@ -0,0 +1,77 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var dot = require('dot');
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
* @param {MapStore} mapStore
|
||||
* @param {String} user
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {Object} params
|
||||
* @constructor
|
||||
* @type {MapStoreMapConfigProvider}
|
||||
*/
|
||||
function MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params) {
|
||||
this.mapStore = mapStore;
|
||||
this.user = user;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.params = params;
|
||||
this.token = params.token;
|
||||
this.cacheBuster = params.cache_buster || 0;
|
||||
}
|
||||
|
||||
module.exports = MapStoreMapConfigProvider;
|
||||
|
||||
MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var self = this;
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
context.limits = renderLimits;
|
||||
return null;
|
||||
},
|
||||
function loadMapConfig(err) {
|
||||
assert.ifError(err);
|
||||
self.mapStore.load(self.token, this);
|
||||
},
|
||||
function finish(err, mapConfig) {
|
||||
return callback(err, mapConfig, self.params, context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
MapStoreMapConfigProvider.prototype.getKey = function() {
|
||||
return this.createKey(false);
|
||||
};
|
||||
|
||||
MapStoreMapConfigProvider.prototype.getCacheBuster = function() {
|
||||
return this.cacheBuster;
|
||||
};
|
||||
|
||||
MapStoreMapConfigProvider.prototype.filter = function(key) {
|
||||
var regex = new RegExp('^' + this.createKey(true) + '.*');
|
||||
return key && key.match(regex);
|
||||
};
|
||||
|
||||
// Configure bases for cache keys suitable for string interpolation
|
||||
var baseKey = '{{=it.dbname}}:{{=it.token}}';
|
||||
var rendererKey = baseKey + ':{{=it.dbuser}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
|
||||
|
||||
var baseKeyTpl = dot.template(baseKey);
|
||||
var rendererKeyTpl = dot.template(rendererKey);
|
||||
|
||||
MapStoreMapConfigProvider.prototype.createKey = function(base) {
|
||||
var tplValues = _.defaults({}, this.params, {
|
||||
dbname: '',
|
||||
token: '',
|
||||
dbuser: '',
|
||||
format: '',
|
||||
layer: '',
|
||||
scale_factor: 1
|
||||
});
|
||||
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
|
||||
};
|
||||
266
lib/cartodb/models/mapconfig/named_map_provider.js
Normal file
@@ -0,0 +1,266 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
var dot = require('dot');
|
||||
var step = require('step');
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
var templateName = require('../../backends/template_maps').templateName;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @type {NamedMapMapConfigProvider}
|
||||
*/
|
||||
function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi, queryTablesApi, namedLayersAdapter,
|
||||
owner, templateId, config, authToken, params) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.namedLayersAdapter = namedLayersAdapter;
|
||||
|
||||
this.owner = owner;
|
||||
this.templateName = templateName(templateId);
|
||||
this.config = config;
|
||||
this.authToken = authToken;
|
||||
this.params = params;
|
||||
|
||||
this.cacheBuster = Date.now();
|
||||
|
||||
// use template after call to mapConfig
|
||||
this.template = null;
|
||||
|
||||
this.affectedTablesAndLastUpdate = null;
|
||||
|
||||
// providing
|
||||
this.err = null;
|
||||
this.mapConfig = null;
|
||||
this.rendererParams = null;
|
||||
this.context = {};
|
||||
}
|
||||
|
||||
module.exports = NamedMapMapConfigProvider;
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
if (!!this.err || this.mapConfig !== null) {
|
||||
return callback(this.err, this.mapConfig, this.rendererParams, this.context);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var mapConfig = null;
|
||||
var datasource = null;
|
||||
var rendererParams;
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
self.getTemplate(this);
|
||||
},
|
||||
function prepareParams(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
self.template = tpl;
|
||||
|
||||
var templateParams = {};
|
||||
if (self.config) {
|
||||
try {
|
||||
templateParams = _.isString(self.config) ? JSON.parse(self.config) : self.config;
|
||||
} catch (e) {
|
||||
throw new Error('malformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return templateParams;
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(self.template, templateParams);
|
||||
},
|
||||
function prepareLayergroup(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.namedLayersAdapter.getLayers(self.owner, _mapConfig.layers, self.pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
_mapConfig.layers = layers;
|
||||
}
|
||||
return next(null, _mapConfig, datasource);
|
||||
}
|
||||
);
|
||||
},
|
||||
function beforeLayergroupCreate(err, _mapConfig, _datasource) {
|
||||
assert.ifError(err);
|
||||
mapConfig = _mapConfig;
|
||||
datasource = _datasource;
|
||||
rendererParams = _.extend({}, self.params, {
|
||||
user: self.owner
|
||||
});
|
||||
self.setDBParams(self.owner, rendererParams, this);
|
||||
},
|
||||
function prepareContextLimits(err) {
|
||||
assert.ifError(err);
|
||||
self.userLimitsApi.getRenderLimits(self.owner, this);
|
||||
},
|
||||
function cacheAndReturnMapConfig(err, renderLimits) {
|
||||
self.err = err;
|
||||
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, datasource);
|
||||
self.rendererParams = rendererParams;
|
||||
self.context.limits = renderLimits || {};
|
||||
return callback(self.err, self.mapConfig, self.rendererParams, self.context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getTemplate = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if (!!this.err || this.template !== null) {
|
||||
return callback(this.err, this.template);
|
||||
}
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
self.templateMaps.getTemplate(self.owner, self.templateName, this);
|
||||
},
|
||||
function checkExists(err, tpl) {
|
||||
assert.ifError(err);
|
||||
if (!tpl) {
|
||||
var notFoundErr = new Error(
|
||||
"Template '" + self.templateName + "' of user '" + self.owner + "' not found"
|
||||
);
|
||||
notFoundErr.http_status = 404;
|
||||
throw notFoundErr;
|
||||
}
|
||||
return tpl;
|
||||
},
|
||||
function checkAuthorized(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(tpl, self.authToken);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
var authorizationFailedErr = new Error('Failed to authorize template');
|
||||
authorizationFailedErr.http_status = 403;
|
||||
throw authorizationFailedErr;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
var unauthorizedErr = new Error('Unauthorized template instantiation');
|
||||
unauthorizedErr.http_status = 403;
|
||||
throw unauthorizedErr;
|
||||
}
|
||||
|
||||
return tpl;
|
||||
},
|
||||
function cacheAndReturnTemplate(err, template) {
|
||||
self.err = err;
|
||||
self.template = template;
|
||||
return callback(self.err, self.template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getKey = function() {
|
||||
return this.createKey(false);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getCacheBuster = function() {
|
||||
return this.cacheBuster;
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.reset = function() {
|
||||
this.template = null;
|
||||
|
||||
this.affectedTablesAndLastUpdate = null;
|
||||
|
||||
this.err = null;
|
||||
this.mapConfig = null;
|
||||
|
||||
this.cacheBuster = Date.now();
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.filter = function(key) {
|
||||
var regex = new RegExp('^' + this.createKey(true) + '.*');
|
||||
return key && key.match(regex);
|
||||
};
|
||||
|
||||
// Configure bases for cache keys suitable for string interpolation
|
||||
var baseKey = '{{=it.dbname}}:{{=it.owner}}:{{=it.templateName}}';
|
||||
var rendererKey = baseKey + ':{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
|
||||
|
||||
var baseKeyTpl = dot.template(baseKey);
|
||||
var rendererKeyTpl = dot.template(rendererKey);
|
||||
|
||||
NamedMapMapConfigProvider.prototype.createKey = function(base) {
|
||||
var tplValues = _.defaults({}, this.params, {
|
||||
dbname: '',
|
||||
owner: this.owner,
|
||||
templateName: this.templateName,
|
||||
authToken: this.authToken || '',
|
||||
configHash: configHash(this.config),
|
||||
layer: '',
|
||||
scale_factor: 1
|
||||
});
|
||||
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
|
||||
};
|
||||
|
||||
function configHash(config) {
|
||||
if (!config) {
|
||||
return '';
|
||||
}
|
||||
return crypto.createHash('md5').update(JSON.stringify(config)).digest('hex').substring(0,8);
|
||||
}
|
||||
|
||||
module.exports.configHash = configHash;
|
||||
|
||||
NamedMapMapConfigProvider.prototype.setDBParams = function(cdbuser, params, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getTemplateName = function() {
|
||||
return this.templateName;
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getAffectedTablesAndLastUpdatedTime = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if (this.affectedTablesAndLastUpdate !== null) {
|
||||
return callback(null, this.affectedTablesAndLastUpdate);
|
||||
}
|
||||
|
||||
step(
|
||||
function getMapConfig() {
|
||||
self.getMapConfig(this);
|
||||
},
|
||||
function getSql(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
return mapConfig.getLayers().map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
},
|
||||
function getAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(self.owner, sql, this);
|
||||
},
|
||||
function finish(err, result) {
|
||||
self.affectedTablesAndLastUpdate = result;
|
||||
return callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
var Datasource = require('windshaft').Datasource;
|
||||
var Datasource = require('windshaft').model.Datasource;
|
||||
|
||||
function MapConfigNamedLayersAdapter(templateMaps) {
|
||||
this.templateMaps = templateMaps;
|
||||
@@ -11,6 +11,10 @@ module.exports = MapConfigNamedLayersAdapter;
|
||||
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!layers) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var adaptLayersQueue = queue(layers.length);
|
||||
|
||||
function adaptLayer(layer, done) {
|
||||
|
||||
@@ -1,45 +1,19 @@
|
||||
var fs = require('fs');
|
||||
var step = require('step');
|
||||
|
||||
function HealthCheck(metadataBackend, tilelive) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.tilelive = tilelive;
|
||||
function HealthCheck(disableFile) {
|
||||
this.disableFile = disableFile;
|
||||
}
|
||||
|
||||
module.exports = HealthCheck;
|
||||
|
||||
|
||||
HealthCheck.prototype.check = function(config, callback) {
|
||||
|
||||
var result = {
|
||||
redis: {
|
||||
ok: false
|
||||
},
|
||||
mapnik: {
|
||||
ok: false
|
||||
},
|
||||
tile: {
|
||||
ok: false
|
||||
}
|
||||
};
|
||||
|
||||
step(
|
||||
function getManualDisable() {
|
||||
fs.readFile(global.environment.disabled_file, this);
|
||||
},
|
||||
function handleDisabledFile(err, data) {
|
||||
var next = this;
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
if (!!data) {
|
||||
err = new Error(data);
|
||||
err.http_status = 503;
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
function handleResult(err) {
|
||||
callback(err, result);
|
||||
HealthCheck.prototype.check = function(callback) {
|
||||
fs.readFile(this.disableFile, function handleDisabledFile(err, data) {
|
||||
var disabledError = null;
|
||||
if (!err) {
|
||||
disabledError = new Error(data || 'Unknown error');
|
||||
disabledError.http_status = 503;
|
||||
}
|
||||
);
|
||||
return callback(disabledError);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<Map
|
||||
background-color="#c33"
|
||||
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
|
||||
</Map>
|
||||
322
lib/cartodb/server.js
Normal file
@@ -0,0 +1,322 @@
|
||||
var express = require('express');
|
||||
var bodyParser = require('body-parser');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
var controller = require('./controllers');
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
|
||||
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
var StatsClient = require('./stats/client');
|
||||
var Profiler = require('./stats/profiler_proxy');
|
||||
var RendererStatsReporter = require('./stats/reporter/renderer');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
var mapnik = windshaft.mapnik;
|
||||
|
||||
var TemplateMaps = require('./backends/template_maps.js');
|
||||
var QueryTablesApi = require('./api/query_tables_api');
|
||||
var UserLimitsApi = require('./api/user_limits_api');
|
||||
var AuthApi = require('./api/auth_api');
|
||||
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
|
||||
var NamedMapProviderCache = require('./cache/named_map_provider_cache');
|
||||
var PgQueryRunner = require('./backends/pg_query_runner');
|
||||
var PgConnection = require('./backends/pg_connection');
|
||||
|
||||
var CdbRequest = require('./models/cdb_request');
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
|
||||
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
|
||||
var redisPool = new RedisPool(_.defaults(global.environment.redis, {
|
||||
name: 'windshaft:server',
|
||||
unwatchOnRelease: false,
|
||||
noReadyCheck: true
|
||||
}));
|
||||
|
||||
redisPool.on('status', function(status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
var userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
render: serverOptions.renderer.mapnik.limits.render || 0
|
||||
}
|
||||
});
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends(serverOptions));
|
||||
|
||||
function invalidateNamedMap (owner, templateName) {
|
||||
var startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
var logMessage = JSON.stringify({
|
||||
username: owner,
|
||||
type: 'named_map_invalidation',
|
||||
elapsed: Date.now() - startTime,
|
||||
error: !!err ? JSON.stringify(err.message) : undefined
|
||||
});
|
||||
if (err) {
|
||||
global.logger.warn(logMessage);
|
||||
} else {
|
||||
global.logger.info(logMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
serverOptions.grainstore.mapnik_version = mapnikVersion(serverOptions);
|
||||
|
||||
validateOptions(serverOptions);
|
||||
|
||||
bootstrapFonts(serverOptions);
|
||||
|
||||
// initialize express server
|
||||
var app = bootstrap(serverOptions);
|
||||
// Extend windshaft with all the elements of the options object
|
||||
_.extend(app, serverOptions);
|
||||
|
||||
var mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: serverOptions.grainstore.default_layergroup_ttl
|
||||
});
|
||||
|
||||
var onTileErrorStrategy;
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var rendererFactory = new windshaft.renderer.Factory({
|
||||
onTileErrorStrategy: onTileErrorStrategy,
|
||||
mapnik: {
|
||||
redisPool: redisPool,
|
||||
grainstore: serverOptions.grainstore,
|
||||
mapnik: serverOptions.renderer.mapnik
|
||||
},
|
||||
http: serverOptions.renderer.http
|
||||
});
|
||||
|
||||
// initialize render cache
|
||||
var rendererCacheOpts = _.defaults(serverOptions.renderCache || {}, {
|
||||
ttl: 60000, // 60 seconds TTL by default
|
||||
statsInterval: 60000 // reports stats every milliseconds defined here
|
||||
});
|
||||
var rendererCache = new windshaft.cache.RendererCache(rendererFactory, rendererCacheOpts);
|
||||
var rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
|
||||
rendererStatsReporter.start();
|
||||
|
||||
var attributesBackend = new windshaft.backend.Attributes(mapStore);
|
||||
var previewBackend = new windshaft.backend.Preview(rendererCache);
|
||||
var tileBackend = new windshaft.backend.Tile(rendererCache);
|
||||
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
|
||||
var mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
|
||||
|
||||
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
|
||||
var namedMapProviderCache = new NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi);
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
|
||||
});
|
||||
|
||||
var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps);
|
||||
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(pgQueryRunner);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
app.all('*', function(req, res, next) {
|
||||
req.context.user = cdbRequest.userByReq(req);
|
||||
next();
|
||||
});
|
||||
|
||||
new controller.Layergroup(
|
||||
app,
|
||||
authApi,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
attributesBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
queryTablesApi,
|
||||
layergroupAffectedTablesCache
|
||||
).register(app);
|
||||
|
||||
new controller.Map(
|
||||
app,
|
||||
authApi,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
queryTablesApi,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
app,
|
||||
authApi,
|
||||
pgConnection,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi,
|
||||
metadataBackend
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMapsAdmin(templateMaps, authApi, pgConnection).register(app);
|
||||
|
||||
new controller.ServerInfo().register(app);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
function validateOptions(opts) {
|
||||
if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) {
|
||||
throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs");
|
||||
}
|
||||
|
||||
// Be nice and warn if configured mapnik version is != instaled mapnik version
|
||||
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
|
||||
console.warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapFonts(opts) {
|
||||
// Set carto renderer configuration for MMLStore
|
||||
opts.grainstore.carto_env = opts.grainstore.carto_env || {};
|
||||
var cenv = opts.grainstore.carto_env;
|
||||
cenv.validation_data = cenv.validation_data || {};
|
||||
if ( ! cenv.validation_data.fonts ) {
|
||||
mapnik.register_system_fonts();
|
||||
mapnik.register_default_fonts();
|
||||
cenv.validation_data.fonts = _.keys(mapnik.fontFiles());
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrap(opts) {
|
||||
var app;
|
||||
if (_.isObject(opts.https)) {
|
||||
// use https if possible
|
||||
app = express.createServer(opts.https);
|
||||
} else {
|
||||
// fall back to http by default
|
||||
app = express();
|
||||
}
|
||||
app.enable('jsonp callback');
|
||||
app.disable('x-powered-by');
|
||||
app.disable('etag');
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
|
||||
req.context = req.context || {};
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: global.statsClient,
|
||||
profile: opts.useProfiler
|
||||
});
|
||||
|
||||
if (global.environment && global.environment.api_hostname) {
|
||||
res.set('X-Served-By-Host', global.environment.api_hostname);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// temporary measure until we upgrade to newer version expressjs so we can check err.status
|
||||
app.use(function(err, req, res, next) {
|
||||
if (err) {
|
||||
if (err.name === 'SyntaxError') {
|
||||
res.status(400).json({ errors: [err.name + ': ' + err.message] });
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
setupLogger(app, opts);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function setupLogger(app, opts) {
|
||||
if (global.log4js && opts.log_format) {
|
||||
var loggerOpts = {
|
||||
// Allowing for unbuffered logging is mainly
|
||||
// used to avoid hanging during unit testing.
|
||||
// TODO: provide an explicit teardown function instead,
|
||||
// releasing any event handler or timer set by
|
||||
// this component.
|
||||
buffer: !opts.unbuffered_logging,
|
||||
// optional log format
|
||||
format: opts.log_format
|
||||
};
|
||||
app.use(global.log4js.connectLogger(global.log4js.getLogger(), _.defaults(loggerOpts, {level: 'info'})));
|
||||
}
|
||||
}
|
||||
|
||||
function surrogateKeysCacheBackends(serverOptions) {
|
||||
var cacheBackends = [];
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
cacheBackends.push(
|
||||
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
|
||||
);
|
||||
}
|
||||
|
||||
if (serverOptions.fastly &&
|
||||
!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
|
||||
cacheBackends.push(
|
||||
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
return cacheBackends;
|
||||
}
|
||||
|
||||
function mapnikVersion(opts) {
|
||||
return opts.grainstore.mapnik_version || mapnik.versions.mapnik;
|
||||
}
|
||||
@@ -1,53 +1,34 @@
|
||||
var os = require('os');
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var QueryTablesApi = require('./api/query_tables_api');
|
||||
var PgQueryRunner = require('./backends/pg_query_runner');
|
||||
var PgConnection = require('./backends/pg_connection');
|
||||
var TemplateMaps = require('./template_maps.js');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter');
|
||||
var CdbRequest = require('./models/cdb_request');
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
statsInterval: 60000,
|
||||
mapnik: {
|
||||
poolSize: 8,
|
||||
metatile: 2,
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false,
|
||||
limits: {}
|
||||
},
|
||||
http: {}
|
||||
});
|
||||
|
||||
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
|
||||
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
if ( global.environment.statsd.prefix ) {
|
||||
var host_token = os.hostname().split('.').reverse().join('.');
|
||||
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
|
||||
}
|
||||
}
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback'
|
||||
];
|
||||
|
||||
module.exports = function(redisPool) {
|
||||
redisPool = redisPool || new RedisPool(_.extend(global.environment.redis, {name: 'windshaft:server_options'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({ pool: redisPool });
|
||||
var lzmaWorker = new LZMA();
|
||||
var pgConnection = new PgConnection(cartoData);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
statsInterval: 60000,
|
||||
mapnik: {
|
||||
poolSize: 8,
|
||||
metatile: 2,
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false,
|
||||
limits: {}
|
||||
},
|
||||
http: {}
|
||||
});
|
||||
|
||||
var me = {
|
||||
module.exports = {
|
||||
bind: {
|
||||
port: global.environment.port,
|
||||
host: global.environment.host
|
||||
},
|
||||
// This is for inline maps and table maps
|
||||
base_url: global.environment.base_url_legacy || '/tiles/:table',
|
||||
|
||||
@@ -61,6 +42,8 @@ module.exports = function(redisPool) {
|
||||
//
|
||||
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
|
||||
|
||||
base_url_templated: global.environment.base_url_templated || '(?:/maps/named|/tiles/template)',
|
||||
|
||||
grainstore: {
|
||||
map: {
|
||||
// TODO: allow to specify in configuration
|
||||
@@ -79,9 +62,11 @@ module.exports = function(redisPool) {
|
||||
},
|
||||
renderer: {
|
||||
mapnik: rendererConfig.mapnik,
|
||||
torque: rendererConfig.torque,
|
||||
http: rendererConfig.http
|
||||
},
|
||||
redis: global.environment.redis,
|
||||
// Do not send unwatch on release. See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
redis: _.extend(global.environment.redis, {unwatchOnRelease: false}),
|
||||
enable_cors: global.environment.enable_cors,
|
||||
varnish_host: global.environment.varnish.host,
|
||||
varnish_port: global.environment.varnish.port,
|
||||
@@ -92,560 +77,4 @@ module.exports = function(redisPool) {
|
||||
cache_enabled: global.environment.cache_enabled,
|
||||
log_format: global.environment.log_format,
|
||||
useProfiler: global.environment.useProfiler
|
||||
};
|
||||
|
||||
// Do not send unwatch on release
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
me.redis.unwatchOnRelease = false;
|
||||
|
||||
// Re-use redisPool
|
||||
me.redis.pool = redisPool;
|
||||
|
||||
// Re-use pgConnection
|
||||
me.pgConnection = pgConnection;
|
||||
// Re-use pgQueryRunner
|
||||
me.pgQueryRunner = pgQueryRunner;
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
me.templateMaps = templateMaps;
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
/* This whole block is about generating X-Cache-Channel { */
|
||||
|
||||
// TODO: review lifetime of elements of this cache
|
||||
// NOTE: by-token indices should only be dropped when
|
||||
// the corresponding layegroup is dropped, because
|
||||
// we have no SQL after layer creation.
|
||||
me.channelCache = {};
|
||||
|
||||
me.buildCacheChannel = function (dbName, tableNames){
|
||||
return dbName + ':' + tableNames.join(',');
|
||||
};
|
||||
|
||||
me.generateCacheChannel = function(app, req, callback){
|
||||
// Build channelCache key
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = [ dbName, req.params.token ].join(':');
|
||||
|
||||
// no token means no tables associated
|
||||
if (!req.params.token) {
|
||||
return callback(null, this.buildCacheChannel(dbName, []));
|
||||
}
|
||||
|
||||
step(
|
||||
function checkCached() {
|
||||
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
|
||||
return callback(null, me.channelCache[cacheKey]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function extractSQL(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
// TODO: cached cache channel for token-based access should
|
||||
// be constructed at renderer cache creation time
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/152
|
||||
if ( ! app.mapStore ) {
|
||||
throw new Error('missing channel cache for token ' + req.params.token);
|
||||
}
|
||||
var mapStore = app.mapStore;
|
||||
step(
|
||||
function loadFromStore() {
|
||||
mapStore.load(req.params.token, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('mapStore_load');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = mapConfig.getLayers()
|
||||
.map(function(lyr) {
|
||||
return lyr.options.sql;
|
||||
})
|
||||
.filter(function(sql) {
|
||||
return !!sql;
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
queryTablesApi.getAffectedTablesInQuery(cdbRequest.userByReq(req), sql, this); // in addCacheChannel
|
||||
},
|
||||
function buildCacheChannel(err, tableNames) {
|
||||
assert.ifError(err);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('affectedTables');
|
||||
}
|
||||
|
||||
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
|
||||
return cacheChannel;
|
||||
},
|
||||
function finish(err, cacheChannel) {
|
||||
callback(err, cacheChannel);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Set the cache chanel info to invalidate the cache on the frontend server
|
||||
//
|
||||
// @param req The request object.
|
||||
// The function will have no effect unless req.res exists.
|
||||
// It is expected that req.params contains 'table' and 'dbname'
|
||||
//
|
||||
// @param cb function(err, channel) will be called when ready.
|
||||
// the channel parameter will be null if nothing was added
|
||||
//
|
||||
me.addCacheChannel = function(app, req, cb) {
|
||||
// skip non-GET requests, or requests for which there's no response
|
||||
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
|
||||
if (req.profiler) {
|
||||
req.profiler.start('addCacheChannel');
|
||||
}
|
||||
var res = req.res;
|
||||
if ( req.params.token ) {
|
||||
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
|
||||
} else {
|
||||
var ttl = global.environment.varnish.ttl || 86400;
|
||||
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
|
||||
}
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if ( req.params.cache_buster ) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
// FIXME: store lastModified in the cache channel instead
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.header('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
me.generateCacheChannel(app, req, function(err, channel){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('generateCacheChannel');
|
||||
req.profiler.end();
|
||||
}
|
||||
if ( ! err ) {
|
||||
res.header('X-Cache-Channel', channel);
|
||||
cb(null, channel);
|
||||
} else {
|
||||
console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err ));
|
||||
// TODO: evaluate if we should bubble up the error instead
|
||||
cb(null, 'ERROR');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
me.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
me.renderCache.beforeRendererCreate = function(req, callback) {
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
var rendererOptions = {};
|
||||
|
||||
step(
|
||||
function getLimits(err) {
|
||||
assert.ifError(err);
|
||||
cartoData.getTilerRenderLimit(user, this);
|
||||
},
|
||||
function handleTilerLimits(err, renderLimit) {
|
||||
assert.ifError(err);
|
||||
rendererOptions.limits = {
|
||||
cacheOnTimeout: rendererConfig.mapnik.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || rendererConfig.mapnik.limits.render || 0
|
||||
};
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, rendererOptions);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
|
||||
mapConfigNamedLayersAdapter.getLayers(cdbRequest.userByReq(req), requestMapConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
return callback(null, requestMapConfig, datasource);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
|
||||
var token = response.layergroupid;
|
||||
|
||||
var username = cdbRequest.userByReq(req);
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
var serverMetadata = global.environment.serverMetadata;
|
||||
if (serverMetadata) {
|
||||
_.extend(response, serverMetadata);
|
||||
}
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asyncronously
|
||||
cartoData.incMapviewCount(username, mapconfig.stat_tag, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
}
|
||||
if ( err ) {
|
||||
console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var sql = mapconfig.layers.map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = dbName + ':' + token;
|
||||
|
||||
step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
assert.ifError(err);
|
||||
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
|
||||
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
|
||||
|
||||
var res = req.res;
|
||||
if (res) {
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
res.header('Last-Modified', (new Date()).toUTCString());
|
||||
res.header('X-Cache-Channel', cacheChannel);
|
||||
}
|
||||
|
||||
res.header('X-Layergroup-Id', response.layergroupid);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/* X-Cache-Channel generation } */
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
me.authorizedBySigner = function(req, callback) {
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
return callback(null, null); // no signer requested
|
||||
}
|
||||
|
||||
var signer = req.params.signer;
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
|
||||
var mapStore = req.app.mapStore;
|
||||
if (!mapStore) {
|
||||
throw new Error('Unable to retrieve map configuration token');
|
||||
}
|
||||
|
||||
mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return callback(null, authorized ? signer : null);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
me.authorizedByAPIKey = function(req, callback)
|
||||
{
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
callback(null, 0); // no api key, no authorization...
|
||||
return;
|
||||
}
|
||||
//console.log("given ApiKey: " + givenKey);
|
||||
var user = cdbRequest.userByReq(req);
|
||||
step(
|
||||
function (){
|
||||
cartoData.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(err);
|
||||
return ( val && givenKey == val ) ? 1 : 0;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
me.authorize = function(req, callback) {
|
||||
var that = this;
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function (){
|
||||
that.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (authorized !== 1) {
|
||||
// not authorized by api_key,
|
||||
// check if authorized by signer
|
||||
that.authorizedBySigner(req, this);
|
||||
return;
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, signed_by){
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! signed_by ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
callback(null, true); // authorized so far
|
||||
return;
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
pgConnection.setDBAuth(signed_by, req.params, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('setDBAuth');
|
||||
}
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.setDBParams = function(cdbuser, params, callback) {
|
||||
step(
|
||||
function setAuth() {
|
||||
pgConnection.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
pgConnection.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// jshint maxcomplexity:10
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
* @param req - standard express request obj. Should have host & table
|
||||
* @param callback
|
||||
*/
|
||||
me.req2params = function(req, callback){
|
||||
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('lzma');
|
||||
}
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
me.req2params(req, callback);
|
||||
} catch (err) {
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
if ( req.params.token ) {
|
||||
//console.log("Request parameters include token " + req.params.token);
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.cache_buster= tksplit[1];
|
||||
}
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) {
|
||||
req.params.signer = user;
|
||||
}
|
||||
else if ( req.params.signer !== user ) {
|
||||
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' +
|
||||
user + '"');
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
/*var template_hash = */tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('req2params.setup');
|
||||
}
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
me.authorize(req, this);
|
||||
},
|
||||
function gatekeep(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorize');
|
||||
}
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function finishSetup(err) {
|
||||
if ( err ) { callback(err, req); return; }
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return me;
|
||||
};
|
||||
|
||||
73
lib/cartodb/stats/client.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var _ = require('underscore');
|
||||
var debug = require('debug')('windshaft:stats_client');
|
||||
var StatsD = require('node-statsd').StatsD;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Returns an StatsD instance or an stub object that replicates the StatsD public interface so there is no need to
|
||||
* keep checking if the stats_client is instantiated or not.
|
||||
*
|
||||
* The first call to this method implies all future calls will use the config specified in the very first call.
|
||||
*
|
||||
* TODO: It's far from ideal to use make this a singleton, improvement desired.
|
||||
* We proceed this way to be able to use StatsD from several places sharing one single StatsD instance.
|
||||
*
|
||||
* @param config Configuration for StatsD, if undefined it will return an stub
|
||||
* @returns {StatsD|Object}
|
||||
*/
|
||||
getInstance: function(config) {
|
||||
|
||||
if (!this.instance) {
|
||||
|
||||
var instance;
|
||||
|
||||
if (config) {
|
||||
instance = new StatsD(config);
|
||||
instance.last_error = { msg: '', count: 0 };
|
||||
instance.socket.on('error', function (err) {
|
||||
var last_err = instance.last_error;
|
||||
var last_msg = last_err.msg;
|
||||
var this_msg = '' + err;
|
||||
if (this_msg !== last_msg) {
|
||||
debug("statsd client socket error: " + err);
|
||||
instance.last_error.count = 1;
|
||||
instance.last_error.msg = this_msg;
|
||||
} else {
|
||||
++last_err.count;
|
||||
if (!last_err.interval) {
|
||||
instance.last_error.interval = setInterval(function () {
|
||||
var count = instance.last_error.count;
|
||||
if (count > 1) {
|
||||
debug("last statsd client socket error repeated " + count + " times");
|
||||
instance.last_error.count = 1;
|
||||
clearInterval(instance.last_error.interval);
|
||||
instance.last_error.interval = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var stubFunc = function (stat, value, sampleRate, callback) {
|
||||
if (_.isFunction(callback)) {
|
||||
callback(null, 0);
|
||||
}
|
||||
};
|
||||
instance = {
|
||||
timing: stubFunc,
|
||||
increment: stubFunc,
|
||||
decrement: stubFunc,
|
||||
gauge: stubFunc,
|
||||
unique: stubFunc,
|
||||
set: stubFunc,
|
||||
sendAll: stubFunc,
|
||||
send: stubFunc
|
||||
};
|
||||
}
|
||||
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
};
|
||||
53
lib/cartodb/stats/profiler_proxy.js
Normal file
@@ -0,0 +1,53 @@
|
||||
var Profiler = require('step-profiler');
|
||||
|
||||
/**
|
||||
* Proxy to encapsulate node-step-profiler module so there is no need to check if there is an instance
|
||||
*/
|
||||
function ProfilerProxy(opts) {
|
||||
this.profile = !!opts.profile;
|
||||
|
||||
this.profiler = null;
|
||||
if (!!opts.profile) {
|
||||
this.profiler = new Profiler({statsd_client: opts.statsd_client});
|
||||
}
|
||||
}
|
||||
|
||||
ProfilerProxy.prototype.done = function(what) {
|
||||
if (this.profile) {
|
||||
this.profiler.done(what);
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.end = function() {
|
||||
if (this.profile) {
|
||||
this.profiler.end();
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.start = function(what) {
|
||||
if (this.profile) {
|
||||
this.profiler.start(what);
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.add = function(what) {
|
||||
if (this.profile) {
|
||||
this.profiler.add(what || {});
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.sendStats = function() {
|
||||
if (this.profile) {
|
||||
this.profiler.sendStats();
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.toString = function() {
|
||||
return this.profile ? this.profiler.toString() : "";
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.toJSONString = function() {
|
||||
return this.profile ? this.profiler.toJSONString() : "{}";
|
||||
};
|
||||
|
||||
module.exports = ProfilerProxy;
|
||||
82
lib/cartodb/stats/reporter/renderer.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// - Reports stats about:
|
||||
// * Total number of renderers
|
||||
// * For mapnik renderers:
|
||||
// - the mapnik-pool status: count, unused and waiting
|
||||
// - the internally cached objects: png and grid
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
function RendererStatsReporter(rendererCache, statsInterval) {
|
||||
this.rendererCache = rendererCache;
|
||||
this.statsInterval = statsInterval || 6e4;
|
||||
this.renderersStatsIntervalId = null;
|
||||
}
|
||||
|
||||
module.exports = RendererStatsReporter;
|
||||
|
||||
RendererStatsReporter.prototype.start = function() {
|
||||
var self = this;
|
||||
this.renderersStatsIntervalId = setInterval(function() {
|
||||
var rendererCacheEntries = self.rendererCache.renderers;
|
||||
|
||||
if (!rendererCacheEntries) {
|
||||
return null;
|
||||
}
|
||||
|
||||
global.statsClient.gauge('windshaft.rendercache.count', _.keys(rendererCacheEntries).length);
|
||||
|
||||
var renderersStats = _.reduce(rendererCacheEntries, function(_rendererStats, cacheEntry) {
|
||||
var stats = cacheEntry.renderer && cacheEntry.renderer.getStats && cacheEntry.renderer.getStats();
|
||||
if (!stats) {
|
||||
return _rendererStats;
|
||||
}
|
||||
|
||||
_rendererStats.pool.count += stats.pool.count;
|
||||
_rendererStats.pool.unused += stats.pool.unused;
|
||||
_rendererStats.pool.waiting += stats.pool.waiting;
|
||||
|
||||
_rendererStats.cache.grid += stats.cache.grid;
|
||||
_rendererStats.cache.png += stats.cache.png;
|
||||
|
||||
return _rendererStats;
|
||||
},
|
||||
{
|
||||
pool: {
|
||||
count: 0,
|
||||
unused: 0,
|
||||
waiting: 0
|
||||
},
|
||||
cache: {
|
||||
png: 0,
|
||||
grid: 0
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
global.statsClient.gauge('windshaft.mapnik-cache.png', renderersStats.cache.png);
|
||||
global.statsClient.gauge('windshaft.mapnik-cache.grid', renderersStats.cache.grid);
|
||||
|
||||
global.statsClient.gauge('windshaft.mapnik-pool.count', renderersStats.pool.count);
|
||||
global.statsClient.gauge('windshaft.mapnik-pool.unused', renderersStats.pool.unused);
|
||||
global.statsClient.gauge('windshaft.mapnik-pool.waiting', renderersStats.pool.waiting);
|
||||
}, this.statsInterval);
|
||||
|
||||
this.rendererCache.on('err', rendererCacheErrorListener);
|
||||
this.rendererCache.on('gc', gcTimingListener);
|
||||
};
|
||||
|
||||
function rendererCacheErrorListener() {
|
||||
global.statsClient.increment('windshaft.rendercache.error');
|
||||
}
|
||||
|
||||
function gcTimingListener(gcTime) {
|
||||
global.statsClient.timing('windshaft.rendercache.gc', gcTime);
|
||||
}
|
||||
|
||||
RendererStatsReporter.prototype.stop = function() {
|
||||
this.rendererCache.removeListener('err', rendererCacheErrorListener);
|
||||
this.rendererCache.removeListener('gc', gcTimingListener);
|
||||
|
||||
clearInterval(this.renderersStatsIntervalId);
|
||||
this.renderersStatsIntervalId = null;
|
||||
};
|
||||
2471
npm-shrinkwrap.json
generated
16
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.7.0",
|
||||
"version": "2.14.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -22,23 +22,29 @@
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"express": "~4.13.3",
|
||||
"body-parser": "~1.14.0",
|
||||
"debug": "~2.2.0",
|
||||
"step-profiler": "~0.2.1",
|
||||
"node-statsd": "~0.0.7",
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "0.47.0",
|
||||
"step": "~0.0.5",
|
||||
"windshaft": "~1.0.0",
|
||||
"step": "~0.0.6",
|
||||
"queue-async": "~1.0.7",
|
||||
"request": "~2.9.203",
|
||||
"request": "~2.62.0",
|
||||
"cartodb-redis": "~0.13.0",
|
||||
"cartodb-psql": "~0.4.0",
|
||||
"fastly-purge": "~1.0.0",
|
||||
"redis-mpool": "~0.4.0",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~1.3.7",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.3.6",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~1.3.0",
|
||||
"nock": "~2.11.0",
|
||||
"jshint": "~2.6.0",
|
||||
"redis": "~0.8.6",
|
||||
"strftime": "~0.8.2",
|
||||
|
||||
@@ -24,7 +24,10 @@ cleanup() {
|
||||
return;
|
||||
fi
|
||||
fi
|
||||
redis-cli -p ${REDIS_PORT} info stats
|
||||
redis-cli -p ${REDIS_PORT} info keyspace
|
||||
echo "Killing test redis pid ${PID_REDIS}"
|
||||
#kill ${PID_REDIS_MONITOR}
|
||||
kill ${PID_REDIS}
|
||||
fi
|
||||
if test x"$OPT_DROP_PGSQL" = xyes; then
|
||||
@@ -118,6 +121,9 @@ cd -
|
||||
|
||||
PATH=node_modules/.bin/:$PATH
|
||||
|
||||
#redis-cli -p ${REDIS_PORT} monitor > /tmp/windshaft-cartodb.redis.log &
|
||||
#PID_REDIS_MONITOR=$!
|
||||
|
||||
if test x"$OPT_COVERAGE" = xyes; then
|
||||
echo "Running tests with coverage"
|
||||
./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u tdd -t 5000 ${TESTS}
|
||||
|
||||
123
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
@@ -1,28 +1,28 @@
|
||||
require(__dirname + '/../../support/test_helper');
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var redis = require('redis');
|
||||
var step = require('step');
|
||||
var FastlyPurge = require('fastly-purge');
|
||||
var _ = require('underscore');
|
||||
|
||||
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
|
||||
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/server');
|
||||
|
||||
|
||||
describe('templates surrogate keys', function() {
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
|
||||
// Enable Varnish purge for tests
|
||||
var varnishHost = global.environment.varnish.host;
|
||||
global.environment.varnish.host = '127.0.0.1';
|
||||
var varnishPurgeEnabled = global.environment.varnish.purge_enabled;
|
||||
global.environment.varnish.purge_enabled = true;
|
||||
var varnishHost = serverOptions.varnish_host;
|
||||
serverOptions.varnish_host = '127.0.0.1';
|
||||
var varnishPurgeEnabled = serverOptions.varnish_purge_enabled;
|
||||
serverOptions.varnish_purge_enabled = true;
|
||||
|
||||
var fastlyConfig = global.environment.fastly;
|
||||
var fastlyConfig = serverOptions.fastly;
|
||||
var FAKE_FASTLY_API_KEY = 'fastly-api-key';
|
||||
var FAKE_FASTLY_SERVICE_ID = 'fake-service-id';
|
||||
global.environment.fastly = {
|
||||
serverOptions.fastly = {
|
||||
enabled: true,
|
||||
// the fastly api key
|
||||
apiKey: FAKE_FASTLY_API_KEY,
|
||||
@@ -30,35 +30,40 @@ describe('templates surrogate keys', function() {
|
||||
serviceId: FAKE_FASTLY_SERVICE_ID
|
||||
};
|
||||
|
||||
var serverOptions = require('../../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var templateOwner = 'localhost',
|
||||
templateName = 'acceptance',
|
||||
expectedTemplateId = templateName,
|
||||
template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
var templateOwner = 'localhost';
|
||||
var templateName = 'acceptance';
|
||||
var expectedTemplateId = templateName;
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
expectedBody = { template_id: expectedTemplateId };
|
||||
layergroup: {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
var templateUpdated = _.extend({}, template, {layergroup: {layers: [{
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: 'red'
|
||||
}
|
||||
}]} });
|
||||
var expectedBody = { template_id: expectedTemplateId };
|
||||
|
||||
var varnishHttpUrl = [
|
||||
'http://', global.environment.varnish.host, ':', global.environment.varnish.http_port
|
||||
'http://', serverOptions.varnish_host, ':', serverOptions.varnish_http_port
|
||||
].join('');
|
||||
|
||||
var cacheEntryKey = new NamedMapsCacheEntry(templateOwner, templateName).key();
|
||||
@@ -70,10 +75,10 @@ describe('templates surrogate keys', function() {
|
||||
|
||||
after(function(done) {
|
||||
serverOptions.varnish_purge_enabled = false;
|
||||
global.environment.varnish.host = varnishHost;
|
||||
global.environment.varnish.purge_enabled = varnishPurgeEnabled;
|
||||
serverOptions.varnish_host = varnishHost;
|
||||
serverOptions.varnish_purge_enabled = varnishPurgeEnabled;
|
||||
|
||||
global.environment.fastly = fastlyConfig;
|
||||
serverOptions.fastly = fastlyConfig;
|
||||
|
||||
nock.restore();
|
||||
done();
|
||||
@@ -148,7 +153,7 @@ describe('templates surrogate keys', function() {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
data: JSON.stringify(templateUpdated)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
@@ -157,7 +162,9 @@ describe('templates surrogate keys', function() {
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
setTimeout(function() {
|
||||
next(null, res);
|
||||
}, 50);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -177,14 +184,7 @@ describe('templates surrogate keys', function() {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.keys("map_*|localhost", function(err, keys) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.del(keys, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
testHelper.deleteRedisKeys({'map_tpl|localhost': 0}, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -228,7 +228,9 @@ describe('templates surrogate keys', function() {
|
||||
status: 204
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
setTimeout(function() {
|
||||
next(null, res);
|
||||
}, 50);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -255,6 +257,15 @@ describe('templates surrogate keys', function() {
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(503, '');
|
||||
|
||||
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
|
||||
.post(fastlyPurgePath)
|
||||
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
|
||||
.matchHeader('Fastly-Soft-Purge', 1)
|
||||
.matchHeader('Accept', 'application/json')
|
||||
.reply(200, {
|
||||
status:'ok'
|
||||
});
|
||||
|
||||
step(
|
||||
function createTemplateToUpdate() {
|
||||
createTemplate(this);
|
||||
@@ -270,7 +281,7 @@ describe('templates surrogate keys', function() {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
data: JSON.stringify(templateUpdated)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
@@ -279,7 +290,9 @@ describe('templates surrogate keys', function() {
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
setTimeout(function() {
|
||||
next(null, res);
|
||||
}, 50);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -291,6 +304,7 @@ describe('templates surrogate keys', function() {
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
|
||||
assert.equal(scope.pendingMocks().length, 0);
|
||||
assert.equal(fastlyScope.pendingMocks().length, 0);
|
||||
|
||||
return null;
|
||||
},
|
||||
@@ -298,14 +312,7 @@ describe('templates surrogate keys', function() {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.keys("map_*|localhost", function(err, keys) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.del(keys, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
testHelper.deleteRedisKeys({'map_tpl|localhost': 0}, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
require(__dirname + '/../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var fs = require('fs');
|
||||
|
||||
var metadataBackend = {};
|
||||
var tilelive = {};
|
||||
var HealthCheck = require('../../lib/cartodb/monitoring/health_check');
|
||||
var healthCheck = new HealthCheck(metadataBackend, tilelive);
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
describe('health checks', function () {
|
||||
|
||||
function resetHealthConfig() {
|
||||
function enableHealthConfig() {
|
||||
global.environment.health = {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
|
||||
function disableHealthConfig() {
|
||||
global.environment.health = {
|
||||
enabled: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,65 +28,89 @@ describe('health checks', function () {
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(enableHealthConfig);
|
||||
afterEach(disableHealthConfig);
|
||||
|
||||
var RESPONSE_OK = {
|
||||
status: 200
|
||||
};
|
||||
|
||||
var RESPONSE_FAIL = {
|
||||
status: 503
|
||||
};
|
||||
|
||||
it('returns 200 and ok=true with enabled configuration', function (done) {
|
||||
resetHealthConfig();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
assert.response(server, healthCheckRequest, RESPONSE_OK, function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(parsed.ok);
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(parsed.ok);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('error if disabled file exists', function(done) {
|
||||
var fs = require('fs');
|
||||
var errorMessage = "Maintenance";
|
||||
|
||||
var readFileFn = fs.readFile;
|
||||
fs.readFile = function(filename, callback) {
|
||||
callback(null, "Maintenance");
|
||||
};
|
||||
|
||||
healthCheck.check(null, function(err/*, result*/) {
|
||||
assert.equal(err.message, "Maintenance");
|
||||
assert.equal(err.http_status, 503);
|
||||
done();
|
||||
fs.readFile = readFileFn;
|
||||
});
|
||||
|
||||
});
|
||||
var readFileFn = fs.readFile;
|
||||
fs.readFile = function(filename, callback) {
|
||||
callback(null, errorMessage);
|
||||
};
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(server, healthCheckRequest, RESPONSE_FAIL, function(res, err) {
|
||||
fs.readFile = readFileFn;
|
||||
|
||||
assert.ok(!err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(!parsed.ok);
|
||||
assert.equal(parsed.err, errorMessage);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('error if disabled file exists but has no content', function(done) {
|
||||
var readFileFn = fs.readFile;
|
||||
fs.readFile = function(filename, callback) {
|
||||
callback(null, '');
|
||||
};
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(server, healthCheckRequest, RESPONSE_FAIL, function(res, err) {
|
||||
fs.readFile = readFileFn;
|
||||
|
||||
assert.ok(!err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(!parsed.ok);
|
||||
assert.equal(parsed.err, 'Unknown error');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('not err if disabled file does not exists', function(done) {
|
||||
resetHealthConfig();
|
||||
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
|
||||
|
||||
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
assert.response(server, healthCheckRequest, RESPONSE_OK, function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.equal(parsed.enabled, true);
|
||||
assert.equal(parsed.ok, true);
|
||||
assert.equal(parsed.enabled, true);
|
||||
assert.equal(parsed.ok, true);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
require('../support/test_helper');
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('render limits', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
after(function(done) {
|
||||
redisClient.keys("map_style|*", function(err, matches) {
|
||||
redisClient.del(matches, function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var server;
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
server = new CartodbWindshaft(serverOptions());
|
||||
keysToDelete = {};
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
});
|
||||
|
||||
var keysToDelete = [];
|
||||
afterEach(function(done) {
|
||||
redisClient.DEL(keysToDelete, function() {
|
||||
keysToDelete = [];
|
||||
done();
|
||||
});
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var user = 'localhost';
|
||||
@@ -81,7 +74,7 @@ describe('render limits', function() {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
keysToDelete.push(userLimitsKey);
|
||||
keysToDelete[userLimitsKey] = 5;
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
@@ -135,6 +128,8 @@ describe('render limits', function() {
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
@@ -155,6 +150,8 @@ describe('render limits', function() {
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
@@ -197,6 +194,8 @@ describe('render limits', function() {
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
@@ -246,6 +245,8 @@ describe('render limits', function() {
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
@@ -265,6 +266,8 @@ describe('render limits', function() {
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
|
||||
@@ -6,32 +6,45 @@ var strftime = require('strftime');
|
||||
var redis_stats_db = 5;
|
||||
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20;
|
||||
var IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions());
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var TablesCacheEntry = require('../../lib/cartodb/cache/model/database_tables_entry');
|
||||
|
||||
['/api/v1/map', '/user/localhost/api/v1/map'].forEach(function(layergroup_url) {
|
||||
|
||||
var suiteName = 'multilayer:postgres=layergroup_url=' + layergroup_url;
|
||||
suite(suiteName, function() {
|
||||
describe(suiteName, function() {
|
||||
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var cdbQueryTablesFromPostgresEnabledValue = true;
|
||||
|
||||
var redis_client = redis.createClient(global.environment.redis.port);
|
||||
var expected_last_updated_epoch = 1234567890123; // this is hard-coded into SQLAPIEmu
|
||||
var expected_last_updated = new Date(expected_last_updated_epoch).toISOString();
|
||||
|
||||
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
|
||||
var test_database = test_user + '_db';
|
||||
|
||||
test("layergroup with 2 layers, each with its style", function(done) {
|
||||
it("layergroup with 2 layers, each with its style", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -67,17 +80,14 @@ suite(suiteName, function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.equal(parsedBody.last_updated, expected_last_updated);
|
||||
if ( expected_token ) {
|
||||
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
|
||||
}
|
||||
else expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
|
||||
expected_token = parsedBody.layergroupid;
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
|
||||
@@ -122,7 +132,7 @@ suite(suiteName, function() {
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/170
|
||||
function do_get_tile_nosignature(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '/localhost@' + expected_token + ':cb0/0/0/0.png',
|
||||
@@ -139,7 +149,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid_layer0(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json',
|
||||
@@ -156,7 +166,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid_layer1(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json',
|
||||
@@ -172,26 +182,15 @@ suite(suiteName, function() {
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
var errors = [];
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( errors.length ) done(new Error(errors));
|
||||
else done(null);
|
||||
});
|
||||
});
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("should include serverMedata in the response", function(done) {
|
||||
it("should include serverMedata in the response", function(done) {
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -217,6 +216,8 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
done();
|
||||
}
|
||||
@@ -224,19 +225,25 @@ suite(suiteName, function() {
|
||||
});
|
||||
|
||||
|
||||
test("get creation requests has cache", function(done) {
|
||||
it("get creation requests has cache", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, the_geom_webmercator from test_table',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
} },
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, the_geom_webmercator from test_table_2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
@@ -250,33 +257,26 @@ suite(suiteName, function() {
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
helper.checkCache(res);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
var errors = [];
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( errors.length ) done(new Error(errors));
|
||||
else done(null);
|
||||
});
|
||||
});
|
||||
helper.checkSurrogateKey(res, new TablesCacheEntry('test_windshaft_cartodb_user_1_db', [
|
||||
'public.test_table',
|
||||
'public.test_table_2'
|
||||
]).key().join(' '));
|
||||
|
||||
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("get creation has no cache if sql is bogus", function(done) {
|
||||
it("get creation has no cache if sql is bogus", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
@@ -298,7 +298,7 @@ suite(suiteName, function() {
|
||||
});
|
||||
});
|
||||
|
||||
test("get creation has no cache if cartocss is not valid", function(done) {
|
||||
it("get creation has no cache if cartocss is not valid", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
@@ -321,7 +321,7 @@ suite(suiteName, function() {
|
||||
});
|
||||
});
|
||||
|
||||
test("layergroup can hold substitution tokens", function(done) {
|
||||
it("layergroup can hold substitution tokens", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -353,13 +353,15 @@ suite(suiteName, function() {
|
||||
if ( expected_token ) {
|
||||
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
|
||||
}
|
||||
else expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
else {
|
||||
expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
}
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_tile1(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb10/1/0/0.png',
|
||||
@@ -398,7 +400,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_tile4(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb11/4/0/0.png',
|
||||
@@ -437,7 +439,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid1(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/0/1/0/0.grid.json',
|
||||
@@ -454,7 +456,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid4(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/0/4/0/0.grid.json',
|
||||
@@ -463,32 +465,19 @@ suite(suiteName, function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
|
||||
function(err/*, similarity*/) {
|
||||
next(err);
|
||||
});
|
||||
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2, next);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
var errors = [];
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( errors.length ) done(new Error(errors));
|
||||
else done(null);
|
||||
});
|
||||
});
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("layergroup creation raises mapviews counter", function(done) {
|
||||
it("layergroup creation raises mapviews counter", function(done) {
|
||||
var layergroup = {
|
||||
stat_tag: 'random_tag',
|
||||
version: '1.0.0',
|
||||
@@ -505,19 +494,22 @@ suite(suiteName, function() {
|
||||
var redis_stats_client = redis.createClient(global.environment.redis.port);
|
||||
var expected_token; // will be set on first post and checked on second
|
||||
var now = strftime("%Y%m%d", new Date());
|
||||
var errors = [];
|
||||
step(
|
||||
function clean_stats()
|
||||
{
|
||||
var next = this;
|
||||
redis_stats_client.select(redis_stats_db, function(err) {
|
||||
if ( err ) next(err);
|
||||
else redis_stats_client.del(statskey+':global', next);
|
||||
if ( err ) {
|
||||
next(err);
|
||||
}
|
||||
else {
|
||||
redis_stats_client.del(statskey+':global', next);
|
||||
}
|
||||
});
|
||||
},
|
||||
function do_post_1(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url,
|
||||
@@ -531,12 +523,12 @@ suite(suiteName, function() {
|
||||
});
|
||||
},
|
||||
function check_global_stats_1(err, val) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(val, 1, "Expected score of " + now + " in " + statskey + ":global to be 1, got " + val);
|
||||
redis_stats_client.zscore(statskey+':stat_tag:random_tag', now, this);
|
||||
},
|
||||
function check_tag_stats_1_do_post_2(err, val) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(val, 1, "Expected score of " + now + " in " + statskey + ":stat_tag:" + layergroup.stat_tag +
|
||||
" to be 1, got " + val);
|
||||
var next = this;
|
||||
@@ -553,39 +545,32 @@ suite(suiteName, function() {
|
||||
},
|
||||
function check_global_stats_2(err, val)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(val, 2, "Expected score of " + now + " in " + statskey + ":global to be 2, got " + val);
|
||||
redis_stats_client.zscore(statskey+':stat_tag:' + layergroup.stat_tag, now, this);
|
||||
},
|
||||
function check_tag_stats_2(err, val)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(val, 2, "Expected score of " + now + " in " + statskey + ":stat_tag:" + layergroup.stat_tag +
|
||||
" to be 2, got " + val);
|
||||
return 1;
|
||||
},
|
||||
function cleanup_map_style(err) {
|
||||
if ( err ) errors.push('' + err);
|
||||
var next = this;
|
||||
// trip epoch
|
||||
expected_token = expected_token.split(':')[0];
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
redis_client.del(matches, next);
|
||||
});
|
||||
},
|
||||
function cleanup_stats(err) {
|
||||
if ( err ) errors.push('' + err);
|
||||
redis_client.del([statskey+':global', statskey+':stat_tag:'+layergroup.stat_tag], this);
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) errors.push('' + err);
|
||||
if ( errors.length ) done(new Error(errors.join(',')));
|
||||
else done(null);
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
// trip epoch
|
||||
expected_token = expected_token.split(':')[0];
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete[statskey+':stat_tag:'+layergroup.stat_tag] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("layergroup creation fails if CartoCSS is bogus", function(done) {
|
||||
it("layergroup creation fails if CartoCSS is bogus", function(done) {
|
||||
var layergroup = {
|
||||
stat_tag: 'random_tag',
|
||||
version: '1.0.0',
|
||||
@@ -614,7 +599,7 @@ suite(suiteName, function() {
|
||||
|
||||
// Also tests that server doesn't crash:
|
||||
// see http://github.com/CartoDB/Windshaft-cartodb/issues/109
|
||||
test("layergroup creation fails if sql is bogus", function(done) {
|
||||
it("layergroup creation fails if sql is bogus", function(done) {
|
||||
var layergroup = {
|
||||
stat_tag: 'random_tag',
|
||||
version: '1.0.0',
|
||||
@@ -641,7 +626,7 @@ suite(suiteName, function() {
|
||||
});
|
||||
});
|
||||
|
||||
test("layergroup with 2 private-table layers", function(done) {
|
||||
it("layergroup with 2 private-table layers", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -678,13 +663,15 @@ suite(suiteName, function() {
|
||||
if ( expected_token ) {
|
||||
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
|
||||
}
|
||||
else expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
else {
|
||||
expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
}
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
|
||||
@@ -705,7 +692,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid_layer0(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json?map_key=1234',
|
||||
@@ -718,7 +705,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid_layer1(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json?map_key=1234',
|
||||
@@ -732,7 +719,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_tile_unauth(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
|
||||
@@ -748,7 +735,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid_layer0_unauth(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json',
|
||||
@@ -763,7 +750,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_grid_layer1_unauth(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json',
|
||||
@@ -777,26 +764,16 @@ suite(suiteName, function() {
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
var errors = [];
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( errors.length ) done(new Error(errors));
|
||||
else done(null);
|
||||
});
|
||||
});
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/152
|
||||
test("x-cache-channel still works for GETs after tiler restart", function(done) {
|
||||
it("x-cache-channel still works for GETs after tiler restart", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -823,19 +800,21 @@ suite(suiteName, function() {
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_post(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.equal(parsedBody.last_updated, expected_last_updated);
|
||||
if ( expected_token ) {
|
||||
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
|
||||
}
|
||||
else expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
else {
|
||||
expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function do_get0(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
|
||||
@@ -845,7 +824,7 @@ suite(suiteName, function() {
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check0(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
|
||||
@@ -857,14 +836,14 @@ suite(suiteName, function() {
|
||||
return null;
|
||||
},
|
||||
function do_restart_server(err/*, res*/) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
// hack simulating restart...
|
||||
server = new CartodbWindshaft(serverOptions());
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
return null;
|
||||
},
|
||||
function do_get1(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
|
||||
@@ -874,7 +853,7 @@ suite(suiteName, function() {
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check1(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
|
||||
@@ -886,26 +865,16 @@ suite(suiteName, function() {
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
var errors = [];
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( errors.length ) done(new Error(errors.join(',')));
|
||||
else done(null);
|
||||
});
|
||||
});
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/cartodb/Windshaft-cartodb/issues/81
|
||||
test("invalid text-name in CartoCSS", function(done) {
|
||||
it("invalid text-name in CartoCSS", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
@@ -933,7 +902,7 @@ suite(suiteName, function() {
|
||||
});
|
||||
});
|
||||
|
||||
test("quotes CartoCSS", function(done) {
|
||||
it("quotes CartoCSS", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
@@ -958,12 +927,14 @@ suite(suiteName, function() {
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/87
|
||||
test("exponential notation in CartoCSS filter values", function(done) {
|
||||
it("exponential notation in CartoCSS filter values", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [
|
||||
@@ -981,12 +952,14 @@ suite(suiteName, function() {
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/93
|
||||
test("accepts unused directives", function(done) {
|
||||
it("accepts unused directives", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
@@ -1024,7 +997,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
|
||||
@@ -1042,28 +1015,17 @@ suite(suiteName, function() {
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
var errors = [];
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( errors.length ) done(new Error(errors));
|
||||
else done(null);
|
||||
});
|
||||
});
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/91
|
||||
// and https://github.com/CartoDB/Windshaft-cartodb/issues/38
|
||||
test("tiles for private tables can be fetched with api_key", function(done) {
|
||||
var errors = [];
|
||||
it("tiles for private tables can be fetched with api_key", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
@@ -1087,7 +1049,7 @@ suite(suiteName, function() {
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function check_result(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
@@ -1104,7 +1066,7 @@ suite(suiteName, function() {
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?api_key=1234',
|
||||
@@ -1114,43 +1076,29 @@ suite(suiteName, function() {
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function check_get_tile(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
return null;
|
||||
},
|
||||
function cleanup(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
if ( ! expected_token ) return null;
|
||||
var next = this;
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
next();
|
||||
});
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) {
|
||||
errors.push(err.message);
|
||||
console.log("Error: " + err);
|
||||
}
|
||||
if ( errors.length ) done(new Error(errors));
|
||||
else done(null);
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// SQL strings can be of arbitrary length, when using POST
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/111
|
||||
test("sql string can be very long", function(done){
|
||||
it("sql string can be very long", function(done){
|
||||
var long_val = 'pretty';
|
||||
for (var i=0; i<1024; ++i) long_val += ' long';
|
||||
for (var i=0; i<1024; ++i) {
|
||||
long_val += ' long';
|
||||
}
|
||||
long_val += ' string';
|
||||
var sql = "SELECT ";
|
||||
for (i=0; i<16; ++i)
|
||||
for (i=0; i<16; ++i) {
|
||||
sql += "'" + long_val + "'::text as pretty_long_field_name_" + i + ", ";
|
||||
}
|
||||
sql += "cartodb_id, the_geom_webmercator FROM gadm4 g";
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -1162,8 +1110,7 @@ suite(suiteName, function() {
|
||||
} }
|
||||
]
|
||||
};
|
||||
var errors = [];
|
||||
var expected_token;
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
@@ -1178,7 +1125,7 @@ suite(suiteName, function() {
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function check_result(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var token_components = parsedBody.layergroupid.split(':');
|
||||
@@ -1186,28 +1133,19 @@ suite(suiteName, function() {
|
||||
return null;
|
||||
},
|
||||
function cleanup(err) {
|
||||
if ( err ) errors.push('' + err);
|
||||
if ( ! expected_token ) return null;
|
||||
var next = this;
|
||||
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
|
||||
if ( err ) errors.push(err.message);
|
||||
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
|
||||
redis_client.del(matches, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
next();
|
||||
});
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) errors.push('' + err);
|
||||
if ( errors.length ) done(new Error(errors.join(',')));
|
||||
else done(null);
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/133
|
||||
test("MapConfig with mapnik layer and no cartocss", function(done) {
|
||||
it("MapConfig with mapnik layer and no cartocss", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -1232,7 +1170,7 @@ suite(suiteName, function() {
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_post(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
|
||||
@@ -1249,7 +1187,7 @@ suite(suiteName, function() {
|
||||
|
||||
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/167
|
||||
test("lack of response from sql-api will result in a timeout", function(done) {
|
||||
it("lack of response from sql-api will result in a timeout", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -1274,7 +1212,7 @@ suite(suiteName, function() {
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_post(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
|
||||
@@ -1308,25 +1246,29 @@ suite(suiteName, function() {
|
||||
status: 200
|
||||
};
|
||||
|
||||
test("cache control for layergroup default value", function(done) {
|
||||
it("cache control for layergroup default value", function(done) {
|
||||
global.environment.varnish.layergroupTtl = null;
|
||||
|
||||
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
|
||||
function(res) {
|
||||
assert.equal(res.headers['cache-control'], 'public,max-age=86400,must-revalidate');
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("cache control for layergroup uses configuration for max-age", function(done) {
|
||||
it("cache control for layergroup uses configuration for max-age", function(done) {
|
||||
var layergroupTtl = 300;
|
||||
global.environment.varnish.layergroupTtl = layergroupTtl;
|
||||
|
||||
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
|
||||
function(res) {
|
||||
assert.equal(res.headers['cache-control'], 'public,max-age=' + layergroupTtl + ',must-revalidate');
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
@@ -1334,7 +1276,7 @@ suite(suiteName, function() {
|
||||
});
|
||||
|
||||
|
||||
test("it's not possible to override authorization with a crafted layergroup", function(done) {
|
||||
it("it's not possible to override authorization with a crafted layergroup", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
@@ -1376,25 +1318,6 @@ suite(suiteName, function() {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
suiteTeardown(function(done) {
|
||||
|
||||
// This test will add map_style records, like
|
||||
// 'map_style|null|publicuser|my_table',
|
||||
redis_client.keys("map_style|*", function(err, matches) {
|
||||
redis_client.del(matches, function() {
|
||||
redis_client.select(5, function() {
|
||||
redis_client.keys("user:localhost:mapviews*", function(err, matches) {
|
||||
redis_client.del(matches, function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
|
||||
var redis = require('redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
@@ -16,15 +16,14 @@ describe('tests from old api translated to multilayer', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
after(function(done) {
|
||||
// This test will add map_style records, like
|
||||
// 'map_style|null|publicuser|my_table',
|
||||
redisClient.keys("map_style|*", function(err, matches) {
|
||||
redisClient.del(matches, function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var wadusSql = 'select 1 as cartodb_id, null::geometry as the_geom_webmercator';
|
||||
@@ -109,6 +108,10 @@ describe('tests from old api translated to multilayer', function() {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
@@ -131,6 +134,10 @@ describe('tests from old api translated to multilayer', function() {
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:cartodb250user:mapviews:global'] = 5;
|
||||
|
||||
global.environment.postgres.host = backupDBHost;
|
||||
done();
|
||||
}
|
||||
@@ -154,6 +161,9 @@ describe('tests from old api translated to multilayer', function() {
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:cartodb250user:mapviews:global'] = 5;
|
||||
|
||||
global.environment.postgres_auth_pass = backupDBPass;
|
||||
done();
|
||||
}
|
||||
@@ -185,6 +195,9 @@ describe('tests from old api translated to multilayer', function() {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
@@ -251,6 +264,9 @@ describe('tests from old api translated to multilayer', function() {
|
||||
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
@@ -316,9 +332,13 @@ describe('tests from old api translated to multilayer', function() {
|
||||
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
// TODO when affected tables query makes the request to fail layergroup should be removed
|
||||
keysToDelete['map_cfg|f14693f2d7b6dcf4629724b3d1efe22d'] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["Error: could not fetch affected tables and last updated time: fake error message"]
|
||||
errors: ["Error: could not fetch affected tables or last updated time: fake error message"]
|
||||
});
|
||||
|
||||
done();
|
||||
@@ -340,13 +360,17 @@ describe('tests from old api translated to multilayer', function() {
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('failed to query database for affected tables'), [], callback);
|
||||
};
|
||||
|
||||
// reset internal cacheChannel cache
|
||||
serverOptions.channelCache = {};
|
||||
server.layergroupAffectedTablesCache.cache.reset();
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
|
||||
var step = require('step');
|
||||
|
||||
@@ -95,7 +97,17 @@ describe('named_layers', function() {
|
||||
}
|
||||
};
|
||||
|
||||
before(function(done) {
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
test_helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
beforeEach(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
|
||||
if (err) {
|
||||
@@ -112,6 +124,23 @@ describe('named_layers', function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, templateName, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for non-existing template name', function(done) {
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
@@ -265,6 +294,9 @@ describe('named_layers', function() {
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -398,6 +430,9 @@ describe('named_layers', function() {
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
@@ -435,7 +470,7 @@ describe('named_layers', function() {
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
var next = this;
|
||||
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
|
||||
templateMaps.delTemplate(username, privateTableTemplateName, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
@@ -526,6 +561,9 @@ describe('named_layers', function() {
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
@@ -563,7 +601,7 @@ describe('named_layers', function() {
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
var next = this;
|
||||
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
|
||||
templateMaps.delTemplate(username, privateTableTemplateNameInteractivity, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
@@ -707,6 +745,9 @@ describe('named_layers', function() {
|
||||
assert.equal(parsedBody.metadata.layers[3].type, 'mapnik');
|
||||
assert.equal(parsedBody.metadata.layers[4].type, 'torque');
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -716,22 +757,118 @@ describe('named_layers', function() {
|
||||
|
||||
});
|
||||
|
||||
it('should work with named tiles', function(done) {
|
||||
|
||||
|
||||
after(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
var namedTilesTemplateName = 'named_tiles_template';
|
||||
var namedTilesTemplate = {
|
||||
version: '0.0.1',
|
||||
name: namedTilesTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
namedMapLayer,
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table_private_1',
|
||||
cartocss: '#layer { marker-fill: #cc3300; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
|
||||
};
|
||||
|
||||
step(
|
||||
function createTemplate() {
|
||||
templateMaps.addTemplate(username, namedTilesTemplate, this);
|
||||
},
|
||||
function createLayergroup(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
throw err;
|
||||
}
|
||||
templateMaps.delTemplate(username, templateName, function(err) {
|
||||
return done(err);
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map/named/' + namedTilesTemplateName + '?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
assert.equal(parsedBody.metadata.layers[0].type, 'mapnik');
|
||||
assert.equal(parsedBody.metadata.layers[1].type, 'mapnik');
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupId + '/all/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function handleTileResponse(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
test_helper.checkCache(res);
|
||||
return true;
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
var next = this;
|
||||
templateMaps.delTemplate(username, namedTilesTemplateName, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
269
test/acceptance/named_maps_authentication.js
Normal file
@@ -0,0 +1,269 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var querystring = require('querystring');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
describe('named maps authentication', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var templateName = 'valid_template';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var tokenAuthTemplateName = 'auth_valid_template';
|
||||
var tokenAuthTemplate = {
|
||||
version: '0.0.1',
|
||||
name: tokenAuthTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
placeholders: {
|
||||
color: {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var namedMapLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName,
|
||||
config: {},
|
||||
auth_tokens: []
|
||||
}
|
||||
};
|
||||
|
||||
var nestedNamedMapTemplateName = 'nested_template';
|
||||
var nestedNamedMapTemplate = {
|
||||
version: '0.0.1',
|
||||
name: nestedNamedMapTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
namedMapLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function (done) {
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, template, function (err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, templateName, function (err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getNamedTile(name, z, x, y, options, callback) {
|
||||
|
||||
var url = '/api/v1/map/named/' + name + '/all/' + [z,x,y].join('/') + '.png';
|
||||
if (options.params) {
|
||||
url = url + '?' + querystring.stringify(options.params);
|
||||
}
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
},
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
var statusCode = options.status || 200;
|
||||
|
||||
var expectedResponse = {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
requestOptions,
|
||||
expectedResponse,
|
||||
function (res, err) {
|
||||
var img;
|
||||
if (!err && res.headers['content-type'] === 'image/png') {
|
||||
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
return callback(err, res, img);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
describe('tiles', function() {
|
||||
it('should return a 404 error for nonexistent template name', function (done) {
|
||||
var nonexistentName = 'nonexistent';
|
||||
getNamedTile(nonexistentName, 0, 0, 0, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 if properly authorized', function(done) {
|
||||
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { params: { auth_token: 'valid1' } }, function(err, res, img) {
|
||||
assert.equal(img.width(), 256);
|
||||
assert.equal(img.height(), 256);
|
||||
|
||||
assert.ok(!err);
|
||||
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getStaticMap(name, options, callback) {
|
||||
|
||||
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
|
||||
if (options.params) {
|
||||
url = url + '?' + querystring.stringify(options.params);
|
||||
}
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
},
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
var statusCode = options.status || 200;
|
||||
|
||||
var expectedResponse = {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
requestOptions,
|
||||
expectedResponse,
|
||||
function (res, err) {
|
||||
var img;
|
||||
if (!err && res.headers['content-type'] === 'image/png') {
|
||||
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
return callback(err, res, img);
|
||||
}
|
||||
);
|
||||
}
|
||||
describe('static maps', function() {
|
||||
it('should return a 404 error for nonexistent template name', function (done) {
|
||||
var nonexistentName = 'nonexistent';
|
||||
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 if properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res, img) {
|
||||
assert.ok(!err);
|
||||
|
||||
assert.equal(img.width(), 640);
|
||||
assert.equal(img.height(), 480);
|
||||
|
||||
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
|
||||
test_helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
140
test/acceptance/named_maps_cache.js
Normal file
@@ -0,0 +1,140 @@
|
||||
require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
|
||||
describe('named maps provider cache', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var username = 'localhost';
|
||||
var templateName = 'template_with_color';
|
||||
|
||||
var IMAGE_TOLERANCE = 20;
|
||||
|
||||
function createTemplate(color) {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
placeholders: {
|
||||
color: {
|
||||
type: "css_color",
|
||||
default: color
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; marker-line-color: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(function (done) {
|
||||
templateMaps.delTemplate(username, templateName, done);
|
||||
});
|
||||
|
||||
function getNamedTile(options, callback) {
|
||||
if (!callback) {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var url = '/api/v1/map/named/' + templateName + '/all/' + [0,0,0].join('/') + '.png';
|
||||
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
},
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
var statusCode = options.statusCode || 200;
|
||||
|
||||
var expectedResponse = {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server, requestOptions, expectedResponse, function (res, err) {
|
||||
var img;
|
||||
if (statusCode === 200) {
|
||||
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
return callback(err, res, img);
|
||||
});
|
||||
}
|
||||
|
||||
function previewFixture(color) {
|
||||
return './test/fixtures/provider/populated_places_simple_reduced-' + color + '.png';
|
||||
}
|
||||
|
||||
var colors = ['red', 'red', 'green', 'blue'];
|
||||
colors.forEach(function(color) {
|
||||
it('should return an image estimating its bounds based on dataset', function (done) {
|
||||
templateMaps.addTemplate(username, createTemplate(color), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getNamedTile(function(err, res, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to use template from named map provider after template deletion', function (done) {
|
||||
var color = 'black';
|
||||
templateMaps.addTemplate(username, createTemplate(color), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getNamedTile(function(err, res, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, function(err) {
|
||||
assert.ok(!err);
|
||||
|
||||
templateMaps.delTemplate(username, templateName, function (err) {
|
||||
assert.ok(!err);
|
||||
|
||||
getNamedTile({ statusCode: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template 'template_with_color' of user 'localhost' not found"] }
|
||||
);
|
||||
|
||||
// add template again so it's clean in afterEach
|
||||
templateMaps.addTemplate(username, createTemplate(color), done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
166
test/acceptance/named_maps_static_view.js
Normal file
@@ -0,0 +1,166 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
|
||||
describe('named maps static view', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var username = 'localhost';
|
||||
var templateName = 'template_with_view';
|
||||
|
||||
var IMAGE_TOLERANCE = 20;
|
||||
|
||||
function createTemplate(view) {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
placeholders: {
|
||||
color: {
|
||||
type: "css_color",
|
||||
default: "#cc3300"
|
||||
}
|
||||
},
|
||||
view: view,
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(function (done) {
|
||||
templateMaps.delTemplate(username, templateName, done);
|
||||
});
|
||||
|
||||
function getStaticMap(callback) {
|
||||
|
||||
var url = '/api/v1/map/static/named/' + templateName + '/640/480.png';
|
||||
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
},
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
var expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
};
|
||||
|
||||
// this could be removed once named maps are invalidated, otherwise you hits the cache
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(server, requestOptions, expectedResponse, function (res, err) {
|
||||
testHelper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
|
||||
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function previewFixture(version) {
|
||||
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
|
||||
}
|
||||
|
||||
it('should return an image estimating its bounds based on dataset', function (done) {
|
||||
templateMaps.addTemplate(username, createTemplate(), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('estimated'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an image using view zoom + center', function (done) {
|
||||
var view = {
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an image using view bounds', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an image using view zoom + center when bounds are also present', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
},
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
110
test/acceptance/named_maps_stats.js
Normal file
@@ -0,0 +1,110 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var querystring = require('querystring');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
describe('named maps preview stats', function() {
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var statTag = 'wadus_viz';
|
||||
var templateName = 'with_stats';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
stat_tag: statTag,
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(function (done) {
|
||||
templateMaps.addTemplate(username, template, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
templateMaps.delTemplate(username, templateName, done);
|
||||
});
|
||||
|
||||
function getStaticMap(name, options, callback) {
|
||||
|
||||
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
|
||||
if (options.params) {
|
||||
url = url + '?' + querystring.stringify(options.params);
|
||||
}
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
},
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
var statusCode = options.status || 200;
|
||||
|
||||
var expectedResponse = {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
requestOptions,
|
||||
expectedResponse,
|
||||
function (res, err) {
|
||||
var img;
|
||||
if (!err && res.headers['content-type'] === 'image/png') {
|
||||
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
return callback(err, res, img);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('should return 200 if properly authorized', function(done) {
|
||||
getStaticMap(templateName, { params: { auth_token: 'valid1' } }, function(err, res, img) {
|
||||
assert.ok(!err);
|
||||
|
||||
assert.equal(img.width(), 640);
|
||||
assert.equal(img.height(), 480);
|
||||
|
||||
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, templateName).key());
|
||||
var redisKeysToDelete = { 'user:localhost:mapviews:global': 5 };
|
||||
redisKeysToDelete['user:localhost:mapviews:stat_tag:' + statTag] = 5;
|
||||
test_helper.deleteRedisKeys(redisKeysToDelete, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,189 +0,0 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var querystring = require('querystring');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
describe('named static maps', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var templateName = 'valid_template';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var tokenAuthTemplateName = 'auth_valid_template';
|
||||
var tokenAuthTemplate = {
|
||||
version: '0.0.1',
|
||||
name: tokenAuthTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
placeholders: {
|
||||
color: {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var namedMapLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName,
|
||||
config: {},
|
||||
auth_tokens: []
|
||||
}
|
||||
};
|
||||
|
||||
var nestedNamedMapTemplateName = 'nested_template';
|
||||
var nestedNamedMapTemplate = {
|
||||
version: '0.0.1',
|
||||
name: nestedNamedMapTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
namedMapLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, template, function (err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, templateName, function (err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getStaticMap(name, options, callback) {
|
||||
|
||||
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
|
||||
if (options.params) {
|
||||
url = url + '?' + querystring.stringify(options.params);
|
||||
}
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
};
|
||||
|
||||
console.log(url);
|
||||
|
||||
var statusCode = options.status || 200;
|
||||
|
||||
var expectedResponse = {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
requestOptions,
|
||||
expectedResponse,
|
||||
function (res, err) {
|
||||
return callback(err, res);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('should return a 404 error for nonexistent template name', function (done) {
|
||||
var nonexistentName = 'nonexistent';
|
||||
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.error, "Template '" + nonexistentName + "' of user '" + username + "' not found");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.error, 'Unauthorized template instantiation');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 if properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
303
test/acceptance/ported/attributes.js
Normal file
@@ -0,0 +1,303 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
|
||||
describe('attributes', function() {
|
||||
|
||||
var server = cartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var test_mapconfig_1 = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'mapnik', options: {
|
||||
sql: "select 1 as id, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
|
||||
cartocss: '#style { }',
|
||||
cartocss_version: '2.0.1'
|
||||
} },
|
||||
{ type: 'mapnik', options: {
|
||||
sql: "select 1 as i, 6 as n, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
|
||||
attributes: { id:'i', columns: ['n'] },
|
||||
cartocss: '#style { }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
function checkCORSHeaders(res) {
|
||||
assert.equal(
|
||||
res.headers['access-control-allow-headers'],
|
||||
'X-Requested-With, X-Prototype-Version, X-CSRF-Token'
|
||||
);
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
}
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("can only be fetched from layer having an attributes spec", function(done) {
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(test_mapconfig_1)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// CORS headers should be sent with response
|
||||
// from layergroup creation via POST
|
||||
checkCORSHeaders(res);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if ( expected_token ) {
|
||||
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
|
||||
} else {
|
||||
expected_token = parsedBody.layergroupid;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function do_get_attr_0(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/attributes/1',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_error_0(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(
|
||||
res.statusCode,
|
||||
400,
|
||||
res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' )
|
||||
);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.errors[0], "Layer 0 has no exposed attributes");
|
||||
return null;
|
||||
},
|
||||
function do_get_attr_1(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/1',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_attr_1(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {"n":6});
|
||||
return null;
|
||||
},
|
||||
function do_get_attr_1_404(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/-666',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_attr_1_404(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/0 features.*identified by fid -666/), msg);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft/issues/131
|
||||
it("are checked at map creation time", function(done) {
|
||||
|
||||
// clone the mapconfig test
|
||||
var mapconfig = JSON.parse(JSON.stringify(test_mapconfig_1));
|
||||
// append unexistant attribute name
|
||||
mapconfig.layers[1].options.sql = 'SELECT * FROM test_table';
|
||||
mapconfig.layers[1].options.attributes.id = 'unexistant';
|
||||
mapconfig.layers[1].options.attributes.columns = ['cartodb_id'];
|
||||
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(mapconfig)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var msg = parsed.errors[0];
|
||||
assert.equal(msg, 'column "unexistant" does not exist');
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("can be used with jsonp", function(done) {
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(test_mapconfig_1)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// CORS headers should be sent with response
|
||||
// from layergroup creation via POST
|
||||
checkCORSHeaders(res);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if ( expected_token ) {
|
||||
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
|
||||
} else {
|
||||
expected_token = parsedBody.layergroupid;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function do_get_attr_0(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token +
|
||||
'/0/attributes/1?callback=test',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_error_0(err, res) {
|
||||
assert.ifError(err);
|
||||
// jsonp errors should be returned with HTTP status 200
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
assert.equal(
|
||||
res.body,
|
||||
'/**/ typeof test === \'function\' && test({"errors":["Layer 0 has no exposed attributes"]});'
|
||||
);
|
||||
return null;
|
||||
},
|
||||
function do_get_attr_1(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/1',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_attr_1(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {"n":6});
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Test that you cannot write to the database from an attributes tile request
|
||||
//
|
||||
// Test for http://github.com/CartoDB/Windshaft/issues/130
|
||||
//
|
||||
it("database access is read-only", function(done) {
|
||||
|
||||
// clone the mapconfig test
|
||||
var mapconfig = JSON.parse(JSON.stringify(test_mapconfig_1));
|
||||
mapconfig.layers[1].options.sql +=
|
||||
", test_table_inserter(st_setsrid(st_point(0,0),4326),'write') as w";
|
||||
mapconfig.layers[1].options.attributes.columns.push('w');
|
||||
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(mapconfig)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
// TODO: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var msg = parsed.errors[0];
|
||||
assert.equal(msg, "cannot execute INSERT in a read-only transaction");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
105
test/acceptance/ported/blend.js
Normal file
@@ -0,0 +1,105 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend png renderer', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
function plainTorqueMapConfig(plainColor) {
|
||||
return {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: plainColor
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced " +
|
||||
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' buffer-size:0;',
|
||||
' -torque-frame-count:1;',
|
||||
' -torque-animation-duration:30;',
|
||||
' -torque-time-attribute:"cartodb_id";',
|
||||
' -torque-aggregation-function:"count(cartodb_id)";',
|
||||
' -torque-resolution:1;',
|
||||
' -torque-data-aggregation:linear;',
|
||||
'}',
|
||||
'#populated_places_simple_reduced{',
|
||||
' comp-op: multiply;',
|
||||
' marker-fill-opacity: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-width: 0;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: rectangle;',
|
||||
' marker-width: 3;',
|
||||
' marker-fill: #FFCC00;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
tile: {
|
||||
z: 2,
|
||||
x: 2,
|
||||
y: 1,
|
||||
layer: 'all',
|
||||
format: 'png'
|
||||
},
|
||||
plainColor: 'white'
|
||||
},
|
||||
{
|
||||
tile: {
|
||||
z: 2,
|
||||
x: 1,
|
||||
y: 1,
|
||||
layer: 'all',
|
||||
format: 'png'
|
||||
},
|
||||
plainColor: '#339900'
|
||||
}
|
||||
];
|
||||
|
||||
function blendPngFixture(zxy) {
|
||||
return './test/fixtures/blend/blend-plain-torque-' + zxy.join('.') + '.png';
|
||||
}
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
var tileRequest = testScenario.tile;
|
||||
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
|
||||
it('tile all/' + zxy.join('/') + '.png', function (done) {
|
||||
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
166
test/acceptance/ported/blend_filtering.js
Normal file
@@ -0,0 +1,166 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend layer filtering', function() {
|
||||
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
|
||||
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
|
||||
response.writeHead(200);
|
||||
response.write(file, "binary");
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
httpRendererResourcesServer.listen(8033, done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: '#fabada'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
|
||||
subdomains: ['abcd']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'SELECT * FROM populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
|
||||
cartocss_version: '2.3.0',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' buffer-size:0;',
|
||||
' -torque-frame-count:1;',
|
||||
' -torque-animation-duration:30;',
|
||||
' -torque-time-attribute:"cartodb_id";',
|
||||
' -torque-aggregation-function:"count(cartodb_id)";',
|
||||
' -torque-resolution:1;',
|
||||
' -torque-data-aggregation:linear;',
|
||||
'}',
|
||||
'#populated_places_simple_reduced{',
|
||||
' comp-op: multiply;',
|
||||
' marker-fill-opacity: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-width: 0;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: rectangle;',
|
||||
' marker-width: 3;',
|
||||
' marker-fill: #FFCC00;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
|
||||
subdomains: ['abcd']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced " +
|
||||
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' buffer-size:0;',
|
||||
' -torque-frame-count:1;',
|
||||
' -torque-animation-duration:30;',
|
||||
' -torque-time-attribute:"cartodb_id";',
|
||||
' -torque-aggregation-function:"count(cartodb_id)";',
|
||||
' -torque-resolution:1;',
|
||||
' -torque-data-aggregation:linear;',
|
||||
'}',
|
||||
'#populated_places_simple_reduced{',
|
||||
' comp-op: multiply;',
|
||||
' marker-fill-opacity: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-width: 0;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: rectangle;',
|
||||
' marker-width: 3;',
|
||||
' marker-fill: #FFCC00;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var filteredLayersSuite = [
|
||||
[2, 2],
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[1, 2],
|
||||
[2, 1], // ordering doesn't matter
|
||||
[0, 3],
|
||||
[1, 3],
|
||||
[1, 2, 5],
|
||||
[1, 2, 3, 4]
|
||||
];
|
||||
|
||||
function blendPngFixture(layers) {
|
||||
return './test/fixtures/blend/blend-filtering-layers-' + layers.join('.') + '-zxy-1.0.0.png';
|
||||
}
|
||||
|
||||
filteredLayersSuite.forEach(function(filteredLayers) {
|
||||
var layerFilter = filteredLayers.join(',');
|
||||
var tileRequest = {
|
||||
z: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: layerFilter,
|
||||
format: 'png'
|
||||
};
|
||||
|
||||
it('should filter on ' + layerFilter + '/1/0/0.png', function (done) {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
147
test/acceptance/ported/blend_http_fallback.js
Normal file
@@ -0,0 +1,147 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend http fallback', function() {
|
||||
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
if (request.url.match(/^\/error404\//)) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
} else {
|
||||
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
|
||||
if (request.url.match(/^\/dark\//)) {
|
||||
filename = __dirname + '/../../fixtures/http/dark_nolabels-1-0-0.png';
|
||||
}
|
||||
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
|
||||
response.writeHead(200);
|
||||
response.write(file, "binary");
|
||||
response.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
httpRendererResourcesServer.listen(8033, done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'plain', // <- 0
|
||||
options: {
|
||||
color: '#fabada'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'http', // <- 1
|
||||
options: {
|
||||
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
|
||||
subdomains: ['light']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'http', // <- 2
|
||||
options: {
|
||||
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
|
||||
subdomains: ['dark']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'http', // <- 3
|
||||
options: {
|
||||
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
|
||||
subdomains: ['error404']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik', // <- 4
|
||||
options: {
|
||||
sql: 'SELECT * FROM populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
|
||||
cartocss_version: '2.3.0',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var filteredLayersSuite = [
|
||||
['all'], // layers displayed: 2 + 4, skipping 3 as it fails
|
||||
[0, 4],
|
||||
[0, 3], // skips layer 3 as it fails
|
||||
[1, 2],
|
||||
[1, 3],
|
||||
[2, 3],
|
||||
[3, 4]
|
||||
];
|
||||
|
||||
function blendPngFixture(layers) {
|
||||
return './test/fixtures/blend/http_fallback/blend-layers-' + layers.join('.') + '-zxy-1.0.0.png';
|
||||
}
|
||||
|
||||
filteredLayersSuite.forEach(function(filteredLayers) {
|
||||
var layerFilter = filteredLayers.join(',');
|
||||
var tileRequest = {
|
||||
z: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: layerFilter,
|
||||
format: 'png'
|
||||
};
|
||||
|
||||
it('should fallback on http error while blending layers ' + layerFilter + '/1/0/0.png', function (done) {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep failing when http layer is requested individually', function(done) {
|
||||
var tileRequest = {
|
||||
z: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 3,
|
||||
format: 'png'
|
||||
};
|
||||
var expectedResponse = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
testClient.getTileLayer(mapConfig, tileRequest, expectedResponse, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {
|
||||
errors: [
|
||||
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
|
||||
]
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
95
test/acceptance/ported/blend_http_timeout.js
Normal file
@@ -0,0 +1,95 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var serverOptions = require('./support/ported_server_options');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe.skip('blend http client timeout', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
|
||||
subdomains: ['light']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'SELECT * FROM populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
|
||||
cartocss_version: '2.3.0',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var oldHttpRendererTimeout;
|
||||
var httpRendererTimeout = 100;
|
||||
|
||||
var slowHttpRendererResourcesServer;
|
||||
|
||||
before(function(done) {
|
||||
oldHttpRendererTimeout = serverOptions.renderer.http.timeout;
|
||||
serverOptions.renderer.http.timeout = httpRendererTimeout;
|
||||
|
||||
// Start a server to test external resources
|
||||
slowHttpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
setTimeout(function() {
|
||||
var filename = __dirname + '/../fixtures/http/light_nolabels-1-0-0.png';
|
||||
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
|
||||
response.writeHead(200);
|
||||
response.write(file, "binary");
|
||||
response.end();
|
||||
});
|
||||
}, httpRendererTimeout * 2);
|
||||
});
|
||||
slowHttpRendererResourcesServer.listen(8033, done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
serverOptions.renderer.http.timeout = oldHttpRendererTimeout;
|
||||
slowHttpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
it('should fail to render when http layer times out', function(done) {
|
||||
var options = {
|
||||
statusCode: 400,
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
serverOptions: serverOptions
|
||||
};
|
||||
testClient.withLayergroup(mapConfig, options, function(err, requestTile, finish) {
|
||||
var tileUrl = '/all/0/0/0.png';
|
||||
requestTile(tileUrl, options, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.ok(parsedBody.errors);
|
||||
assert.ok(parsedBody.errors.length);
|
||||
assert.equal(parsedBody.errors[0], 'Unable to fetch http tile: http://127.0.0.1:8033/light/0/0/0.png');
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
129
test/acceptance/ported/external_resources.js
Normal file
@@ -0,0 +1,129 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var fs = require('fs');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var http = require('http');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var nock = require('nock');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('external resources', function() {
|
||||
|
||||
var res_serv; // resources server
|
||||
var res_serv_status = { numrequests:0 }; // status of resources server
|
||||
var res_serv_port = 8033; // FIXME: make configurable ?
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
nock.enableNetConnect('127.0.0.1');
|
||||
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
// Start a server to test external resources
|
||||
res_serv = http.createServer( function(request, response) {
|
||||
++res_serv_status.numrequests;
|
||||
var filename = __dirname + '/../../fixtures/markers' + request.url;
|
||||
fs.readFile(filename, "binary", function(err, file) {
|
||||
if ( err ) {
|
||||
response.writeHead(404, {'Content-Type': 'text/plain'});
|
||||
response.write("404 Not Found\n");
|
||||
} else {
|
||||
response.writeHead(200);
|
||||
response.write(file, "binary");
|
||||
}
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
res_serv.listen(res_serv_port, done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
|
||||
// Close the resources server
|
||||
res_serv.close(done);
|
||||
});
|
||||
|
||||
function imageCompareFn(fixture, done) {
|
||||
return function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
it("basic external resource", function(done) {
|
||||
|
||||
var circleStyle = "#test_table_3 { marker-file: url('http://127.0.0.1:" + res_serv_port +
|
||||
"/circle.svg'); marker-transform:'scale(0.2)'; }";
|
||||
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_3', circleStyle), 13, 4011, 3088,
|
||||
imageCompareFn('test_table_13_4011_3088_svg1.png', done));
|
||||
});
|
||||
|
||||
it("different external resource", function(done) {
|
||||
|
||||
var squareStyle = "#test_table_3 { marker-file: url('http://127.0.0.1:" + res_serv_port +
|
||||
"/square.svg'); marker-transform:'scale(0.2)'; }";
|
||||
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_3', squareStyle), 13, 4011, 3088,
|
||||
imageCompareFn('test_table_13_4011_3088_svg2.png', done));
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft/issues/107
|
||||
it("external resources get localized on renderer creation if not locally cached", function(done) {
|
||||
|
||||
var options = {
|
||||
serverOptions: PortedServerOptions
|
||||
};
|
||||
|
||||
var externalResourceStyle = "#test_table_3{marker-file: url('http://127.0.0.1:" + res_serv_port +
|
||||
"/square.svg'); marker-transform:'scale(0.2)'; }";
|
||||
|
||||
var externalResourceMapConfig = testClient.defaultTableMapConfig('test_table_3', externalResourceStyle);
|
||||
|
||||
testClient.createLayergroup(externalResourceMapConfig, options, function() {
|
||||
var externalResourceRequestsCount = res_serv_status.numrequests;
|
||||
|
||||
testClient.createLayergroup(externalResourceMapConfig, options, function() {
|
||||
assert.equal(res_serv_status.numrequests, externalResourceRequestsCount);
|
||||
|
||||
// reset resources cache
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
|
||||
externalResourceMapConfig = testClient.defaultTableMapConfig('test_table_3 ', externalResourceStyle);
|
||||
|
||||
testClient.createLayergroup(externalResourceMapConfig, options, function() {
|
||||
assert.equal(res_serv_status.numrequests, externalResourceRequestsCount + 1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("referencing unexistant external resources returns an error", function(done) {
|
||||
var url = "http://127.0.0.1:" + res_serv_port + "/notfound.png";
|
||||
var style = "#test_table_3{marker-file: url('" + url + "'); marker-transform:'scale(0.2)'; }";
|
||||
|
||||
var mapConfig = testClient.defaultTableMapConfig('test_table_3', style);
|
||||
|
||||
testClient.createLayergroup(mapConfig, { statusCode: 400 }, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), {
|
||||
errors: ["Unable to download '" + url + "' for 'style0' (server returned 404)"]
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !!! "," !!!!!! "," !!!!!!! "," !!!!!!!! "," !!!!!!!!! "," !!!!!!!! "," !!!!!!! "," !!!!!! "," !!! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":2}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !! "," !!! "," !! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":4}}}
|
||||
BIN
test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png
Normal file
|
After Width: | Height: | Size: 582 B |
BIN
test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png
Normal file
|
After Width: | Height: | Size: 895 B |
96
test/acceptance/ported/limits.js
Normal file
@@ -0,0 +1,96 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var serverOptions = require('./support/ported_server_options');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe.skip('render limits', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var limitsConfig;
|
||||
var onTileErrorStrategy;
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
limitsConfig = serverOptions.renderer.mapnik.limits;
|
||||
serverOptions.renderer.mapnik.limits = {
|
||||
render: 50,
|
||||
cacheOnTimeout: false
|
||||
};
|
||||
onTileErrorStrategy = serverOptions.renderer.onTileErrorStrategy;
|
||||
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
|
||||
callback(err, tile, headers, stats);
|
||||
};
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
serverOptions.renderer.mapnik.limits = limitsConfig;
|
||||
serverOptions.renderer.onTileErrorStrategy = onTileErrorStrategy;
|
||||
});
|
||||
|
||||
var slowQuery = 'select pg_sleep(1), * from test_table limit 2';
|
||||
var slowQueryMapConfig = testClient.singleLayerMapConfig(slowQuery);
|
||||
|
||||
it('slow query/render returns with 400 status', function(done) {
|
||||
var options = {
|
||||
statusCode: 400,
|
||||
serverOptions: serverOptions
|
||||
};
|
||||
testClient.createLayergroup(slowQueryMapConfig, options, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ["Render timed out"] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses onTileErrorStrategy to handle error and modify response', function(done) {
|
||||
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
|
||||
var fixture = __dirname + '/../../fixtures/limits/fallback.png';
|
||||
fs.readFile(fixture, {encoding: 'binary'}, function(err, img) {
|
||||
callback(null, img, {'Content-Type': 'image/png'}, {});
|
||||
});
|
||||
};
|
||||
var options = {
|
||||
statusCode: 200,
|
||||
contentType: 'image/png',
|
||||
serverOptions: serverOptions
|
||||
};
|
||||
testClient.createLayergroup(slowQueryMapConfig, options, function(err, res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a fallback tile that was modified via onTileErrorStrategy', function(done) {
|
||||
var fixtureImage = './test/fixtures/limits/fallback.png';
|
||||
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
|
||||
fs.readFile(fixtureImage, {encoding: null}, function(err, img) {
|
||||
callback(null, img, {'Content-Type': 'image/png'}, {});
|
||||
});
|
||||
};
|
||||
var options = {
|
||||
statusCode: 200,
|
||||
contentType: 'image/png',
|
||||
serverOptions: serverOptions
|
||||
};
|
||||
testClient.withLayergroup(slowQueryMapConfig, options, function(err, requestTile, finish) {
|
||||
var tileUrl = '/0/0/0.png';
|
||||
requestTile(tileUrl, options, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
1281
test/acceptance/ported/multilayer.js
Normal file
465
test/acceptance/ported/multilayer_error_cases.js
Normal file
@@ -0,0 +1,465 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('multilayer error cases', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("post layergroup with wrong Content-Type", function(done) {
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {"errors":["layergroup POST data must be of type application/json"]});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("post layergroup with no layers", function(done) {
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' }
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {"errors":["Missing layers array from layergroup config"]});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("post layergroup jsonp errors are returned with 200 status", function(done) {
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup?callback=test',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' }
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(
|
||||
res.body,
|
||||
'/**/ typeof test === \'function\' && test({"errors":["Missing layers array from layergroup config"]});'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft/issues/154
|
||||
it("mapnik tokens cannot be used with attributes service", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, 1 as n, the_geom, !bbox! as b from test_table limit 1',
|
||||
cartocss: '#layer { marker-fill:red }',
|
||||
cartocss_version: '2.0.1',
|
||||
attributes: { id:'cartodb_id', columns:['n'] },
|
||||
geom_column: 'the_geom'
|
||||
} }
|
||||
]
|
||||
};
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json; charset=utf-8' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check(err, res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/Attribute service cannot be activated/), msg);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("layergroup with no cartocss_version", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom, 50, 0) as the_geom from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
geom_column: 'the_geom'
|
||||
} }
|
||||
]
|
||||
};
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {errors:["Missing cartocss_version for layer 0 options"]});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("sql/cartocss combination errors", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [{ options: {
|
||||
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#layer [missing=1] { line-width:16; }',
|
||||
geom_column: 'the_geom'
|
||||
}}]
|
||||
};
|
||||
ServerOptions.afterLayergroupCreateCalls = 0;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
try {
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
// See http://github.com/CartoDB/Windshaft/issues/159
|
||||
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
assert.ok(error.match(/column "missing" does not exist/m), error);
|
||||
// cannot check for error starting with style0 until a new enough mapnik
|
||||
// is used: https://github.com/mapnik/mapnik/issues/1924
|
||||
//assert.ok(error.match(/^style0/), "Error doesn't start with style0: " + error);
|
||||
// TODO: check which layer introduced the problem ?
|
||||
done();
|
||||
} catch (err) { done(err); }
|
||||
});
|
||||
});
|
||||
|
||||
it("sql/interactivity combination error", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#layer { line-width:16; }',
|
||||
interactivity: 'i',
|
||||
geom_column: 'the_geom'
|
||||
}},
|
||||
{ options: {
|
||||
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#layer { line-width:16; }',
|
||||
geom_column: 'the_geom'
|
||||
}},
|
||||
{ options: {
|
||||
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#layer { line-width:16; }',
|
||||
interactivity: 'missing',
|
||||
geom_column: 'the_geom'
|
||||
}}
|
||||
]
|
||||
};
|
||||
ServerOptions.afterLayergroupCreateCalls = 0;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
try {
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
// See http://github.com/CartoDB/Windshaft/issues/159
|
||||
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
assert.ok(error.match(/column "missing" does not exist/m), error);
|
||||
// TODO: check which layer introduced the problem ?
|
||||
done();
|
||||
} catch (err) { done(err); }
|
||||
});
|
||||
});
|
||||
|
||||
it("blank CartoCSS error", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#style { line-width:16 }',
|
||||
interactivity: 'i',
|
||||
geom_column: 'the_geom'
|
||||
}},
|
||||
{ options: {
|
||||
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '',
|
||||
interactivity: 'i',
|
||||
geom_column: 'the_geom'
|
||||
}}
|
||||
]
|
||||
};
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
try {
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
assert.ok(error.match(/^style1: CartoCSS is empty/), error);
|
||||
done();
|
||||
} catch (err) { done(err); }
|
||||
});
|
||||
});
|
||||
|
||||
it("Invalid mapnik-geometry-type CartoCSS error", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#style [mapnik-geometry-type=bogus] { line-width:16 }',
|
||||
geom_column: 'the_geom'
|
||||
}},
|
||||
{ options: {
|
||||
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
|
||||
cartocss_version: '2.0.2',
|
||||
cartocss: '#style [mapnik-geometry-type=bogus] { line-width:16 }',
|
||||
geom_column: 'the_geom'
|
||||
}}
|
||||
]
|
||||
};
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
try {
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
// carto-0.9.3 used to say "Failed to parse expression",
|
||||
// carto-0.9.5 says "not a valid keyword"
|
||||
assert.ok(error.match(/^style0:.*(Failed|not a valid)/), error);
|
||||
// TODO: check which layer introduced the problem ?
|
||||
done();
|
||||
} catch (err) { done(err); }
|
||||
});
|
||||
});
|
||||
|
||||
it("post'ing style with non existent column in filter returns 400 with error", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select * from test_table limit 1',
|
||||
cartocss: '#test_table::outline[address="one"], [address="two"] { marker-fill: red; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: [ 'cartodb_id' ],
|
||||
geom_column: 'the_geom'
|
||||
} },
|
||||
{ options: {
|
||||
sql: 'select * from test_big_poly limit 1',
|
||||
cartocss: '#test_big_poly { marker-fill:blue }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: [ 'cartodb_id' ],
|
||||
geom_column: 'the_geom'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
assert.ok(error.match(/column "address" does not exist/m), error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft/issues/31
|
||||
it('bogus sql raises 400 status code', function(done) {
|
||||
var bogusSqlMapConfig = testClient.singleLayerMapConfig('BOGUS FROM test_table');
|
||||
testClient.createLayergroup(bogusSqlMapConfig, { statusCode: 400 }, function(err, res) {
|
||||
assert.ok(/syntax error/.test(res.body), "Unexpected error: " + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('bogus sql raises 200 status code for jsonp', function(done) {
|
||||
var bogusSqlMapConfig = testClient.singleLayerMapConfig('bogus');
|
||||
var options = {
|
||||
method: 'GET',
|
||||
callbackName: 'test',
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript; charset=utf-8'
|
||||
}
|
||||
};
|
||||
testClient.createLayergroup(bogusSqlMapConfig, options, function(err, res) {
|
||||
assert.ok(
|
||||
/^\/\*\*\/ typeof test === 'function' && test\(/.test(res.body),
|
||||
"Body start expected callback name: " + res.body
|
||||
);
|
||||
assert.ok(/syntax error/.test(res.body), "Unexpected error: " + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query not selecting the_geom raises 200 status code for jsonp instead of 404', function(done) {
|
||||
var noGeomMapConfig = testClient.singleLayerMapConfig('select null::geometry the_geom_wadus');
|
||||
var options = {
|
||||
method: 'GET',
|
||||
callbackName: 'test',
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript; charset=utf-8'
|
||||
}
|
||||
};
|
||||
testClient.createLayergroup(noGeomMapConfig, options, function(err, res) {
|
||||
assert.ok(
|
||||
/^\/\*\*\/ typeof test === 'function' && test\(/.test(res.body),
|
||||
"Body start expected callback name: " + res.body
|
||||
);
|
||||
assert.ok(/column.*does not exist/.test(res.body), "Unexpected error: " + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("query with no geometry field returns 400 status", function(done){
|
||||
var noGeometrySqlMapConfig = testClient.singleLayerMapConfig('SELECT 1');
|
||||
testClient.createLayergroup(noGeometrySqlMapConfig, { statusCode: 400 }, function(err, res) {
|
||||
assert.ok(/column.*does not exist/.test(res.body), "Unexpected error: " + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("bogus style should raise 400 status", function(done){
|
||||
var bogusStyleMapConfig = testClient.defaultTableMapConfig('test_table', '#test_table{xxxxx;}');
|
||||
testClient.createLayergroup(bogusStyleMapConfig, { method: 'GET', statusCode: 400 }, done);
|
||||
});
|
||||
|
||||
var defaultErrorExpectedResponse = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
it('should raise 400 error for out of bounds layer index', function(done){
|
||||
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
|
||||
|
||||
testClient.getGrid(mapConfig, 1, 13, 4011, 3088, defaultErrorExpectedResponse, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ["Layer '1' not found in layergroup"] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// OPTIONS LAYERGROUP
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
it("nonexistent layergroup token error", function(done) {
|
||||
step(
|
||||
function do_get_tile(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/deadbeef/0/0/0/0.grid.json',
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkResponse(err, res) {
|
||||
assert.ifError(err);
|
||||
// FIXME: should be 404
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ':' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {"errors": ["Invalid or nonexistent map configuration token 'deadbeef'"]});
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('error 400 on json syntax error', function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.1',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select the_geom from test_table limit 1',
|
||||
cartocss: '#layer { marker-fill:red }'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json; charset=utf-8' },
|
||||
data: '{' + JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, { errors: ['SyntaxError: Unexpected token {'] });
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
377
test/acceptance/ported/multilayer_interactivity.js
Normal file
@@ -0,0 +1,377 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var _ = require('underscore');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('multilayer interactivity and layers order', function() {
|
||||
|
||||
var server = cartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
function layerType(layer) {
|
||||
return layer.type || 'undefined';
|
||||
}
|
||||
|
||||
function testInteractivityLayersOrderScenario(testScenario) {
|
||||
it(testScenario.desc, function(done) {
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: testScenario.layers
|
||||
};
|
||||
|
||||
PortedServerOptions.afterLayergroupCreateCalls = 0;
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
//status: 200, don't use status here to have a more meaningful error message
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(response) {
|
||||
assert.equal(
|
||||
response.statusCode,
|
||||
200,
|
||||
'Expected status code 200, got ' + response.statusCode +
|
||||
'\n\tResponse body: ' + response.body +
|
||||
'\n\tLayer types: ' + layergroup.layers.map(layerType).join(', ')
|
||||
);
|
||||
|
||||
// assert.equal(PortedServerOptions.afterLayergroupCreateCalls, 1);
|
||||
|
||||
var layergroupResponse = JSON.parse(response.body);
|
||||
assert.ok(layergroupResponse);
|
||||
|
||||
var layergroupId = layergroupResponse.layergroupid;
|
||||
assert.ok(layergroupId);
|
||||
assert.equal(layergroupResponse.metadata.layers.length, layergroup.layers.length);
|
||||
|
||||
// check layers metadata at least match in number
|
||||
var layersMetadata = layergroupResponse.metadata.layers;
|
||||
assert.equal(layersMetadata.length, layergroup.layers.length);
|
||||
for (var i = 0, len = layersMetadata.length; i < len; i++) {
|
||||
assert.equal(
|
||||
getLayerTypeFn(layersMetadata[i].type),
|
||||
getLayerTypeFn(layergroup.layers[i].type)
|
||||
);
|
||||
}
|
||||
// check torque metadata at least match in number
|
||||
var torqueLayers = layergroup.layers.filter(function(layer) { return layer.type === 'torque'; });
|
||||
if (torqueLayers.length) {
|
||||
assert.equal(Object.keys(layergroupResponse.metadata.torque).length, torqueLayers.length);
|
||||
}
|
||||
|
||||
var keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
var cartocssVersion = '2.3.0';
|
||||
var cartocss = '#layer { line-width:16; }';
|
||||
var sql = "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom";
|
||||
var sqlWadus = "select 1 as wadus, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom";
|
||||
|
||||
var httpLayer = {
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
subdomains: ['a','b','c']
|
||||
}
|
||||
};
|
||||
|
||||
var torqueLayer = {
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "select 1 id, '1970-01-02'::date d, 'POINT(0 0)'::geometry the_geom_webmercator",
|
||||
cartocss: [
|
||||
"Map {",
|
||||
"-torque-frame-count:2;",
|
||||
"-torque-resolution:3;",
|
||||
"-torque-time-attribute:d;",
|
||||
"-torque-aggregation-function:'count(id)';",
|
||||
"}"
|
||||
].join(' '),
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikInteractivityLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'i',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var cartodbLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var cartodbInteractivityLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'i',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var cartodbWadusInteractivityLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sqlWadus,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'wadus',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var noTypeLayer = {
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var noTypeInteractivityLayer = {
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'i',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
};
|
||||
|
||||
var allLayers = [
|
||||
httpLayer,
|
||||
torqueLayer,
|
||||
mapnikLayer,
|
||||
mapnikInteractivityLayer,
|
||||
cartodbLayer,
|
||||
cartodbInteractivityLayer,
|
||||
cartodbWadusInteractivityLayer,
|
||||
noTypeLayer,
|
||||
noTypeInteractivityLayer
|
||||
];
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'one layer, no interactivity',
|
||||
layers: [cartodbLayer]
|
||||
},
|
||||
{
|
||||
desc: 'one layer with interactivity',
|
||||
layers: [cartodbInteractivityLayer]
|
||||
},
|
||||
{
|
||||
desc: 'two layers, interactivity mix',
|
||||
layers: [
|
||||
cartodbLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'two layers, different interactivity columns',
|
||||
layers: [
|
||||
cartodbWadusInteractivityLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mix of no interactivity with interactivity',
|
||||
layers: [
|
||||
cartodbInteractivityLayer,
|
||||
cartodbLayer,
|
||||
cartodbWadusInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'interactivity layers + torque',
|
||||
layers: [
|
||||
cartodbWadusInteractivityLayer,
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'torque + interactivity layers',
|
||||
layers: [
|
||||
torqueLayer,
|
||||
cartodbWadusInteractivityLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'interactivity + torque + interactivity',
|
||||
layers: [
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'http + mix of [no] interactivity layers',
|
||||
layers: [
|
||||
httpLayer,
|
||||
cartodbInteractivityLayer,
|
||||
cartodbLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'http + interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'http + interactivity + torque + no interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbLayer,
|
||||
torqueLayer,
|
||||
cartodbWadusInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mapnik type – two layers, interactivity mix',
|
||||
layers: [
|
||||
mapnikLayer,
|
||||
mapnikInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mapnik type – mix of no interactivity with interactivity',
|
||||
layers: [
|
||||
cartodbInteractivityLayer,
|
||||
cartodbLayer,
|
||||
mapnikInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mapnik type – interactivity layers + torque',
|
||||
layers: [
|
||||
mapnikInteractivityLayer,
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mapnik type – http + interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
mapnikInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'no type – two layers, interactivity mix',
|
||||
layers: [
|
||||
noTypeLayer,
|
||||
noTypeInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'no type – mix of no interactivity with interactivity',
|
||||
layers: [
|
||||
noTypeInteractivityLayer,
|
||||
noTypeLayer,
|
||||
mapnikInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'no type – interactivity layers + torque',
|
||||
layers: [
|
||||
noTypeLayer,
|
||||
noTypeInteractivityLayer,
|
||||
torqueLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'no type – http + interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
noTypeInteractivityLayer,
|
||||
torqueLayer,
|
||||
noTypeInteractivityLayer
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var chaosScenariosSize = 25;
|
||||
var chaosScenarios = [];
|
||||
for (var i = 0; i < chaosScenariosSize; i++) {
|
||||
// Underscore.js' sample method uses Fisher-Yates shuffle internally, see http://bost.ocks.org/mike/shuffle/
|
||||
var randomLayers = _.sample(allLayers, _.random(1, allLayers.length));
|
||||
chaosScenarios.push({
|
||||
desc: 'chaos scenario – layer types: ' + randomLayers.map(layerType).join(', '),
|
||||
layers: randomLayers
|
||||
});
|
||||
}
|
||||
|
||||
testScenarios.forEach(testInteractivityLayersOrderScenario);
|
||||
chaosScenarios.forEach(testInteractivityLayersOrderScenario);
|
||||
|
||||
});
|
||||
|
||||
158
test/acceptance/ported/raster.js
Normal file
@@ -0,0 +1,158 @@
|
||||
var testHelper =require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('raster', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
function checkCORSHeaders(res) {
|
||||
assert.equal(res.headers['access-control-allow-headers'], 'X-Requested-With, X-Prototype-Version, X-CSRF-Token');
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
}
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("can render raster for valid mapconfig", function(done) {
|
||||
|
||||
var mapconfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{ type: 'mapnik', options: {
|
||||
sql: "select ST_AsRaster(" +
|
||||
" ST_MakeEnvelope(-100,-40, 100, 40, 4326), " +
|
||||
" 1.0, -1.0, '8BUI', 127) as rst",
|
||||
geom_column: 'rst',
|
||||
geom_type: 'raster',
|
||||
cartocss: '#layer { raster-opacity:1.0 }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(mapconfig)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// CORS headers should be sent with response
|
||||
// from layergroup creation via POST
|
||||
checkCORSHeaders(res);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if ( expected_token ) {
|
||||
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
|
||||
} else {
|
||||
expected_token = parsedBody.layergroupid;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_response(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(res.headers['content-type'], "image/png");
|
||||
var next = this;
|
||||
assert.imageEqualsFile(res.body,
|
||||
'./test/fixtures/raster_gray_rect.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
try {
|
||||
assert.ifError(err);
|
||||
next();
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("raster geom type does not allow interactivity", function(done) {
|
||||
|
||||
var mapconfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: [
|
||||
"select 1 id,",
|
||||
"ST_AsRaster(ST_MakeEnvelope(-100, -40, 100, 40, 4326), 1.0, -1.0, '8BUI', 127) as rst"
|
||||
].join(' '),
|
||||
geom_column: 'rst',
|
||||
geom_type: 'raster',
|
||||
cartocss: '#layer { raster-opacity: 1.0 }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'id'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(mapconfig)
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ok(!err);
|
||||
checkCORSHeaders(res);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, { errors: [ 'Mapnik raster layers do not support interactivity' ] });
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
64
test/acceptance/ported/regressions.js
Normal file
@@ -0,0 +1,64 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('regressions', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft/issues/65
|
||||
it("#65 catching non-Error exception doesn't kill the backend", function(done) {
|
||||
var mapConfig = testClient.defaultTableMapConfig('test_table');
|
||||
testClient.withLayergroup(mapConfig, function(err, requestTile, finish) {
|
||||
var options = {
|
||||
statusCode: 400,
|
||||
contentType: 'application/json; charset=utf-8'
|
||||
};
|
||||
requestTile('/0/0/0.png?testUnexpectedError=1', options, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), { "errors": ["test unexpected error"] });
|
||||
finish(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test that you cannot write to the database from a tile request
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft/issues/130
|
||||
// [x] Needs a fix on the mapnik side: https://github.com/mapnik/mapnik/pull/2143
|
||||
//
|
||||
it("#130 database access is read-only", function(done) {
|
||||
|
||||
var writeSqlMapConfig = testClient.singleLayerMapConfig(
|
||||
'select st_point(0,0) as the_geom, * from test_table_inserter(st_setsrid(st_point(0,0),4326),\'write\')'
|
||||
);
|
||||
|
||||
var expectedResponse = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getTile(writeSqlMapConfig, 0, 0, 0, expectedResponse, function(err, res) {
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.ok(parsedBody.errors);
|
||||
assert.equal(parsedBody.errors.length, 1);
|
||||
assert.ok(parsedBody.errors[0].match(/read-only transaction/), 'read-only error message expected');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
138
test/acceptance/ported/retina.js
Normal file
@@ -0,0 +1,138 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('retina support', function() {
|
||||
|
||||
var layergroupId = null;
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function(done) {
|
||||
keysToDelete = {'user:localhost:mapviews:global': 5};
|
||||
|
||||
var retinaSampleMapConfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'SELECT * FROM populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
|
||||
cartocss_version: '2.3.0',
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(retinaSampleMapConfig)
|
||||
},
|
||||
{
|
||||
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err, 'Failed to create layergroup');
|
||||
|
||||
layergroupId = JSON.parse(res.body).layergroupid;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
afterEach(function(done) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
|
||||
function testRetinaImage(scaleFactor, responseHead, assertFn) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/0/0' + scaleFactor + '.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
},
|
||||
responseHead,
|
||||
assertFn
|
||||
);
|
||||
}
|
||||
|
||||
function testValidImageDimmensions(scaleFactor, imageSize, done) {
|
||||
testRetinaImage(scaleFactor,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
|
||||
|
||||
var image = new mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
|
||||
assert.equal(image.width(), imageSize);
|
||||
assert.equal(image.height(), imageSize);
|
||||
done();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('image dimensions when scale factor is not defined', function(done) {
|
||||
testValidImageDimmensions('', 256, done);
|
||||
});
|
||||
|
||||
it('image dimensions when scale factor = @1x', function(done) {
|
||||
testValidImageDimmensions('@1x', 256, done);
|
||||
});
|
||||
|
||||
it('image dimensions when scale factor = @2x', function(done) {
|
||||
testValidImageDimmensions('@2x', 512, done);
|
||||
});
|
||||
|
||||
it('error when scale factor is not enabled', function(done) {
|
||||
|
||||
var scaleFactor = '@4x';
|
||||
|
||||
testRetinaImage(scaleFactor,
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ["Tile with specified resolution not found"] } );
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
170
test/acceptance/ported/server.js
Normal file
@@ -0,0 +1,170 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('server', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET INVALID
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
it("get call to server returns 200", function(done){
|
||||
assert.response(server, {
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
},{
|
||||
// FIXME: shouldn't this be a 404 ?
|
||||
status: 200
|
||||
}, function() { done(); } );
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET VERSION
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
it("get /version returns versions", function(done){
|
||||
assert.response(server, {
|
||||
url: '/version',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
|
||||
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
|
||||
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
|
||||
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
|
||||
// TODO: check actual versions ?
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET GRID
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
it("grid jsonp", function(done){
|
||||
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
|
||||
testClient.getGridJsonp(mapConfig, 0, 13, 4011, 3088, 'jsonp_test', function(err, res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(res.headers['content-type'], 'text/javascript; charset=utf-8');
|
||||
var didRunJsonCallback = false;
|
||||
var response = {};
|
||||
// jshint ignore:start
|
||||
function jsonp_test(body) {
|
||||
response = body;
|
||||
didRunJsonCallback = true;
|
||||
}
|
||||
eval(res.body);
|
||||
// jshint ignore:end
|
||||
assert.ok(didRunJsonCallback);
|
||||
assert.utfgridEqualsFile(response, './test/fixtures/test_table_13_4011_3088.grid.json', 2, done);
|
||||
});
|
||||
});
|
||||
|
||||
it("get'ing a json with default style and single interactivity should return a grid", function(done){
|
||||
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
|
||||
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
|
||||
var expected_json = {
|
||||
"1":{"name":"Hawai"},
|
||||
"2":{"name":"El Estocolmo"},
|
||||
"3":{"name":"El Rey del Tallarín"},
|
||||
"4":{"name":"El Lacón"},
|
||||
"5":{"name":"El Pico"}
|
||||
};
|
||||
assert.deepEqual(JSON.parse(res.body).data, expected_json);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("get'ing a json with default style and no interactivity should return an error", function(done){
|
||||
var mapConfig = testClient.singleLayerMapConfig('select * from test_table');
|
||||
var expectedResponse = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, expectedResponse, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), {"errors":["Tileset has no interactivity"]});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("get grid jsonp error is returned with 200 status", function(done){
|
||||
var mapConfig = testClient.singleLayerMapConfig('select * from test_table');
|
||||
var expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript; charset=utf-8'
|
||||
}
|
||||
};
|
||||
testClient.getGridJsonp(mapConfig, 0, 13, 4011, 3088, 'test', expectedResponse, function(err, res) {
|
||||
assert.ok(res.body.match(/"errors":/), 'missing error in response: ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See http://github.com/Vizzuality/Windshaft/issues/50
|
||||
it("get'ing a json with no data should return an empty grid", function(done){
|
||||
var query = 'select * from test_table limit 0';
|
||||
var mapConfig = testClient.singleLayerMapConfig(query, null, null, 'name');
|
||||
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
|
||||
assert.utfgridEqualsFile(res.body, './test/fixtures/test_table_13_4011_3088_empty.grid.json', 2, done);
|
||||
});
|
||||
});
|
||||
|
||||
// Another test for http://github.com/Vizzuality/Windshaft/issues/50
|
||||
it("get'ing a json with no data but interactivity should return an empty grid", function(done){
|
||||
var query = 'SELECT * FROM test_table limit 0';
|
||||
var mapConfig = testClient.singleLayerMapConfig(query, null, null, 'cartodb_id');
|
||||
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
|
||||
assert.utfgridEqualsFile(res.body, './test/fixtures/test_table_13_4011_3088_empty.grid.json', 2, done);
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/67
|
||||
it("get'ing a solid grid while changing interactivity fields", function(done){
|
||||
var query = 'SELECT * FROM test_big_poly';
|
||||
var style211 = "#test_big_poly{polygon-fill:blue;}"; // for solid
|
||||
var mapConfigName = testClient.singleLayerMapConfig(query, style211, null, 'name');
|
||||
testClient.getGrid(mapConfigName, 0, 3, 2, 2, function(err, res) {
|
||||
var expected_data = { "1":{"name":"west"} };
|
||||
assert.deepEqual(JSON.parse(res.body).data, expected_data);
|
||||
|
||||
var mapConfigCartodbId = testClient.singleLayerMapConfig(query, style211, null, 'cartodb_id');
|
||||
testClient.getGrid(mapConfigCartodbId, 0, 3, 2, 2, function(err, res) {
|
||||
var expected_data = { "1":{"cartodb_id":"1"} };
|
||||
assert.deepEqual(JSON.parse(res.body).data, expected_data);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
261
test/acceptance/ported/server_gettile.js
Normal file
@@ -0,0 +1,261 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var semver = require('semver');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('server_gettile', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
function imageCompareFn(fixture, done) {
|
||||
return function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET TILE
|
||||
// --{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
it("get'ing a tile with default style should return an expected tile", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 13, 4011, 3088,
|
||||
imageCompareFn('test_table_13_4011_3088.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("response of get tile can be served by renderer cache", function(done) {
|
||||
var tileUrl = '/13/4011/3088.png';
|
||||
var lastXwc;
|
||||
var mapConfig = testClient.defaultTableMapConfig('test_table');
|
||||
testClient.withLayergroup(mapConfig, function (err, requestTile, finish) {
|
||||
requestTile(tileUrl, function (err, res) {
|
||||
var xwc = res.headers['x-windshaft-cache'];
|
||||
assert.ok(xwc);
|
||||
assert.ok(xwc > 0);
|
||||
lastXwc = xwc;
|
||||
|
||||
requestTile(tileUrl, function (err, res) {
|
||||
var xwc = res.headers['x-windshaft-cache'];
|
||||
assert.ok(xwc);
|
||||
assert.ok(xwc > 0);
|
||||
assert.ok(xwc >= lastXwc);
|
||||
|
||||
requestTile(tileUrl + '?cache_buster=wadus', function (err, res) {
|
||||
var xwc = res.headers['x-windshaft-cache'];
|
||||
assert.ok(!xwc);
|
||||
|
||||
finish(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not choke when queries end with a semicolon", function(done){
|
||||
testClient.getTile(testClient.singleLayerMapConfig('SELECT * FROM test_table limit 2;'), 0, 0, 0, done);
|
||||
});
|
||||
|
||||
it("should not choke when sql ends with a semicolon and some blanks", function(done){
|
||||
testClient.getTile(testClient.singleLayerMapConfig('SELECT * FROM test_table limit 2; \t\n'), 0, 0, 0, done);
|
||||
});
|
||||
|
||||
it("should not strip quoted semicolons within an sql query", function(done){
|
||||
testClient.getTile(
|
||||
testClient.singleLayerMapConfig("SELECT * FROM test_table where name != ';\n'"), 0, 0, 0, done
|
||||
);
|
||||
});
|
||||
|
||||
it("getting two tiles with same configuration uses renderer cache", function(done) {
|
||||
|
||||
var imageFixture = './test/fixtures/test_table_13_4011_3088_styled.png';
|
||||
var tileUrl = '/13/4011/3088.png';
|
||||
var mapConfig = testClient.defaultTableMapConfig(
|
||||
'test_table',
|
||||
'#test_table{marker-fill: blue;marker-line-color: black;}'
|
||||
);
|
||||
|
||||
function validateLayergroup(res) {
|
||||
// cache is hit because we create a renderer to validate the map config
|
||||
assert.ok(!res.headers.hasOwnProperty('x-windshaft-cache'), "Did hit renderer cache on first time");
|
||||
}
|
||||
|
||||
testClient.withLayergroup(mapConfig, validateLayergroup, function(err, requestTile, finish) {
|
||||
|
||||
requestTile(tileUrl, function(err, res) {
|
||||
assert.ok(res.headers.hasOwnProperty('x-windshaft-cache'), "Did not hit renderer cache on second time");
|
||||
assert.ok(res.headers['x-windshaft-cache'] >= 0);
|
||||
|
||||
assert.imageEqualsFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var test_style_black_200 = "#test_table{marker-fill:black;marker-line-color:black;marker-width:5}";
|
||||
var test_style_black_210 = "#test_table{marker-fill:black;marker-line-color:black;marker-width:10}";
|
||||
|
||||
it("get'ing a tile with url specified 2.0.0 style should return an expected tile", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table', test_style_black_200, '2.0.0'),
|
||||
13, 4011, 3088, imageCompareFn('test_table_13_4011_3088_styled_black.png', done));
|
||||
});
|
||||
|
||||
it("get'ing a tile with url specified 2.1.0 style should return an expected tile", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table', test_style_black_210, '2.1.0'),
|
||||
13, 4011, 3088, imageCompareFn('test_table_13_4011_3088_styled_black.png', done));
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft/issues/99
|
||||
it("unused directives are tolerated", function(done){
|
||||
var style = "#test_table{point-transform: 'scale(100)';}";
|
||||
var sql = "SELECT 1 as cartodb_id, 'SRID=4326;POINT(0 0)'::geometry as the_geom";
|
||||
testClient.getTile(testClient.singleLayerMapConfig(sql, style), 0, 0, 0,
|
||||
imageCompareFn('test_default_mapnik_point.png', done));
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft/issues/100
|
||||
var test_strictness = function(done) {
|
||||
var nonStrictMapConfig = testClient.singleLayerMapConfig(
|
||||
"SELECT 1 as cartodb_id, 'SRID=3857;POINT(666 666)'::geometry as the_geom",
|
||||
"#test_table{point-transform: 'scale(100)';}"
|
||||
);
|
||||
testClient.withLayergroup(nonStrictMapConfig, function(err, requestTile, finish) {
|
||||
var options = {
|
||||
statusCode: 400,
|
||||
contentType: 'application/json; charset=utf-8'
|
||||
};
|
||||
requestTile('/0/0/0.png?strict=1', options, function() {
|
||||
finish(done);
|
||||
});
|
||||
});
|
||||
};
|
||||
var test_strict_lbl = "unused directives are not tolerated if strict";
|
||||
if ( semver.satisfies(mapnik.versions.mapnik, '2.3.x') ) {
|
||||
// Strictness handling changed in 2.3.x, possibly a bug: see http://github.com/mapnik/mapnik/issues/2301
|
||||
it.skip('[skipped due to http://github.com/mapnik/mapnik/issues/2301]' + test_strict_lbl, test_strictness);
|
||||
}
|
||||
else {
|
||||
it(test_strict_lbl, test_strictness);
|
||||
}
|
||||
|
||||
it('high cpu regression with mapnik <2.3.x', function(done) {
|
||||
var sql = [
|
||||
"SELECT 'my polygon name here' AS name,",
|
||||
"st_envelope(st_buffer(st_transform(",
|
||||
"st_setsrid(st_makepoint(-26.6592894004,49.7990296995),4326),3857),10000000)) AS the_geom",
|
||||
"FROM generate_series(-6,6) x",
|
||||
"UNION ALL",
|
||||
"SELECT 'my marker name here' AS name,",
|
||||
" st_transform(st_setsrid(st_makepoint(49.6042060319,-49.0522997372),4326),3857) AS the_geom",
|
||||
"FROM generate_series(-6,6) x"
|
||||
].join(' ');
|
||||
|
||||
var style = [
|
||||
'#test_table {marker-fill:#ff7;',
|
||||
' marker-max-error:0.447492761618;',
|
||||
' marker-line-opacity:0.659371340628;',
|
||||
' marker-allow-overlap:true;',
|
||||
' polygon-fill:green;',
|
||||
' marker-spacing:0.0;',
|
||||
' marker-width:4.0;',
|
||||
' marker-height:18.0;',
|
||||
' marker-opacity:0.942312062822;',
|
||||
' line-color:green;',
|
||||
' line-gamma:0.945973211092;',
|
||||
' line-cap:square;',
|
||||
' polygon-opacity:0.12576055992;',
|
||||
' marker-type:arrow;',
|
||||
' polygon-gamma:0.46354913107;',
|
||||
' line-dasharray:33,23;',
|
||||
' line-join:bevel;',
|
||||
' marker-placement:line;',
|
||||
' line-width:1.0;',
|
||||
' marker-line-color:#ff7;',
|
||||
' line-opacity:0.39403752154;',
|
||||
' marker-line-width:3.0;',
|
||||
'}'
|
||||
].join('');
|
||||
|
||||
testClient.getTile(testClient.singleLayerMapConfig(sql, style), 13, 4011, 3088, done);
|
||||
});
|
||||
|
||||
// https://github.com/CartoDB/Windshaft-cartodb/issues/316
|
||||
it('should return errors with better formatting', function(done) {
|
||||
var mapConfig = {
|
||||
"version": "1.0.1",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 20,
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.1.1',
|
||||
"sql": "SELECT null::geometry AS the_geom",
|
||||
"cartocss": [
|
||||
'@water: #cdd2d4;',
|
||||
'Map {',
|
||||
'\tbackground-color: @water;',
|
||||
'\tbufferz-size: 256;',
|
||||
'}',
|
||||
'@landmass_fill: lighten(#e3e3dc, 8%);'
|
||||
].join('\n')
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.1.1',
|
||||
"sql": "SELECT the_geom FROM false_background_zoomed('!scale_denominator!', !bbox!) AS _",
|
||||
"cartocss": [
|
||||
'#false_background {',
|
||||
'\tpolygon-fill: @landmass_fill;',
|
||||
'}'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var options = {
|
||||
statusCode: 400
|
||||
};
|
||||
|
||||
testClient.createLayergroup(mapConfig, options, function(err, res, parsedBody) {
|
||||
assert.ok(parsedBody.errors);
|
||||
// more assertions when errors is populated with better format
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
192
test/acceptance/ported/server_png8_format.js
Normal file
@@ -0,0 +1,192 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var _ = require('underscore');
|
||||
var fs = require('fs');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85;
|
||||
|
||||
describe('server_png8_format', function() {
|
||||
|
||||
var serverOptionsPng32 = ServerOptions;
|
||||
serverOptionsPng32.grainstore = _.clone(ServerOptions.grainstore);
|
||||
serverOptionsPng32.grainstore.mapnik_tile_format = 'png32';
|
||||
var serverPng32 = new cartodbServer(serverOptionsPng32);
|
||||
serverPng32.setMaxListeners(0);
|
||||
|
||||
var serverOptionsPng8 = ServerOptions;
|
||||
serverOptionsPng8.grainstore = _.clone(ServerOptions.grainstore);
|
||||
serverOptionsPng8.grainstore.mapnik_tile_format = 'png8:m=h';
|
||||
var serverPng8 = new cartodbServer(serverOptionsPng8);
|
||||
serverPng8.setMaxListeners(0);
|
||||
|
||||
|
||||
var layergroupId;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
var testPngFilesDir = __dirname + '/../../results/png';
|
||||
fs.readdirSync(testPngFilesDir)
|
||||
.filter(function(fileName) {
|
||||
return /.*\.png$/.test(fileName);
|
||||
})
|
||||
.map(function(fileName) {
|
||||
return testPngFilesDir + '/' + fileName;
|
||||
})
|
||||
.forEach(fs.unlinkSync);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
function testOutputForPng32AndPng8(desc, tile, callback) {
|
||||
|
||||
var bufferPng32,
|
||||
bufferPng8;
|
||||
|
||||
it(desc + '; tile: ' + JSON.stringify(tile), function(done){
|
||||
assert.response(
|
||||
serverPng32,
|
||||
{
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
layergroupId = JSON.parse(res.body).layergroupid;
|
||||
|
||||
var tilePartialUrl = _.template('/<%= z %>/<%= x %>/<%= y %>.png', tile);
|
||||
|
||||
var requestPayload = {
|
||||
url: '/database/windshaft_test/layergroup/' + layergroupId + tilePartialUrl,
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
var requestHeaders = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(serverPng32, requestPayload, requestHeaders, function(responsePng32) {
|
||||
assert.equal(responsePng32.headers['content-type'], "image/png");
|
||||
bufferPng32 = responsePng32.body;
|
||||
assert.response(serverPng8, requestPayload, requestHeaders, function(responsePng8) {
|
||||
assert.equal(responsePng8.headers['content-type'], "image/png");
|
||||
bufferPng8 = responsePng8.body;
|
||||
assert.ok(bufferPng8.length < bufferPng32.length);
|
||||
assert.imageBuffersAreEqual(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, imagePaths, similarity) {
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
|
||||
callback(err, imagePaths, similarity, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var currentLevel = 3,
|
||||
allLevelTiles = [],
|
||||
maxLevelTile = Math.pow(2, currentLevel);
|
||||
|
||||
for (var i = 0; i < maxLevelTile; i++) {
|
||||
for (var j = 0; j < maxLevelTile; j++) {
|
||||
allLevelTiles.push({
|
||||
z: currentLevel,
|
||||
x: i,
|
||||
y: j
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'SELECT * FROM populated_places_simple_reduced',
|
||||
cartocss: [
|
||||
'#populated_places_simple_reduced {',
|
||||
'marker-fill: #FFCC00;',
|
||||
'marker-width: 10;',
|
||||
'marker-line-color: #FFF;',
|
||||
'marker-line-width: 1.5;',
|
||||
'marker-line-opacity: 1;',
|
||||
'marker-fill-opacity: 0.9;',
|
||||
'marker-comp-op: multiply;',
|
||||
'marker-type: ellipse;',
|
||||
'marker-placement: point;',
|
||||
'marker-allow-overlap: true;',
|
||||
'marker-clip: false;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var allImagePaths = [],
|
||||
similarities = [];
|
||||
allLevelTiles.forEach(function(tile) {
|
||||
testOutputForPng32AndPng8('intensity visualization', tile, function(err, imagePaths, similarity, done) {
|
||||
allImagePaths.push(imagePaths);
|
||||
similarities.push(similarity);
|
||||
var transformPaths = [];
|
||||
for (var i = 0, len = allImagePaths.length; i < len; i++) {
|
||||
if (similarities[i] > 0.075) {
|
||||
transformPaths.push({
|
||||
passive: allImagePaths[i][0],
|
||||
active: allImagePaths[i][1],
|
||||
similarity: similarities[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
var output = 'handleResults(' + JSON.stringify(transformPaths) + ');';
|
||||
fs.writeFileSync('test/results/png/results.js', output);
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
154
test/acceptance/ported/static_maps.js
Normal file
@@ -0,0 +1,154 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var http = require('http');
|
||||
var fs = require('fs');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('static_maps', function() {
|
||||
|
||||
var validUrlTemplate = 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png';
|
||||
var invalidUrlTemplate = 'http://127.0.0.1:8033/INVALID/{z}/{x}/{y}.png';
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
var filename = __dirname + '/../../fixtures/http/basemap.png';
|
||||
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
|
||||
response.writeHead(200);
|
||||
response.write(file, "binary");
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
httpRendererResourcesServer.listen(8033, done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
function staticMapConfig(urlTemplate, cartocss) {
|
||||
return {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: urlTemplate,
|
||||
subdomains: ['abcd']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'SELECT * FROM populated_places_simple_reduced',
|
||||
cartocss: cartocss || '#layer { marker-fill:red; } #layer { marker-width: 2; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
var zoom = 3,
|
||||
lat = 0,
|
||||
lon = 0,
|
||||
width = 400,
|
||||
height = 300;
|
||||
|
||||
it('center image', function (done) {
|
||||
var mapConfig = staticMapConfig(validUrlTemplate);
|
||||
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, function(err, res, image) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(image.width(), width);
|
||||
assert.equal(image.height(), height);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('center image with invalid basemap', function (done) {
|
||||
var mapConfig = staticMapConfig(invalidUrlTemplate);
|
||||
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, function(err, res, image) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(image.width(), width);
|
||||
assert.equal(image.height(), height);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var west = -90,
|
||||
south = -45,
|
||||
east = 90,
|
||||
north = 45,
|
||||
bbWidth = 640,
|
||||
bbHeight = 480;
|
||||
|
||||
it('bbox', function (done) {
|
||||
var mapConfig = staticMapConfig(validUrlTemplate);
|
||||
testClient.getStaticBbox(mapConfig, west, south, east, north, bbWidth, bbHeight, function(err, res, image) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(image.width(), bbWidth);
|
||||
assert.equal(image.height(), bbHeight);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not fail for coordinates out of range', function (done) {
|
||||
var outOfRangeHeight = 3000;
|
||||
var mapConfig = staticMapConfig(validUrlTemplate);
|
||||
testClient.getStaticCenter(mapConfig, 1, lat, lon, width, outOfRangeHeight, function(err, res, image) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(image.width(), width);
|
||||
assert.equal(image.height(), outOfRangeHeight);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should keep failing for other errors', function (done) {
|
||||
var invalidStyleForZoom = '#layer { marker-fill:red; } #layer[zoom='+zoom+'] { marker-width: [wadus] * 2; }';
|
||||
var mapConfig = staticMapConfig(validUrlTemplate, invalidStyleForZoom);
|
||||
var expectedResponse = {
|
||||
statusCode: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, expectedResponse, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.ok(parsedBody.errors);
|
||||
assert.ok(parsedBody.errors.length);
|
||||
assert.ok(parsedBody.errors[0].match(/column \"wadus\" does not exist/));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
84
test/acceptance/ported/support/ported_server_options.js
Normal file
@@ -0,0 +1,84 @@
|
||||
var _ = require('underscore');
|
||||
var serverOptions = require('../../../../lib/cartodb/server_options');
|
||||
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
|
||||
module.exports = _.extend({}, serverOptions, {
|
||||
base_url: '/database/:dbname/table/:table',
|
||||
base_url_mapconfig: '/database/:dbname/layergroup',
|
||||
grainstore: {
|
||||
datasource: {
|
||||
geometry_field: 'the_geom',
|
||||
srid: 4326
|
||||
},
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
|
||||
gc_prob: 0 // run the garbage collector at each invocation
|
||||
},
|
||||
renderer: {
|
||||
mapnik: {
|
||||
poolSize: 4,//require('os').cpus().length,
|
||||
metatile: 1,
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
scale_factors: [1, 2],
|
||||
limits: {
|
||||
render: 0,
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 5000,
|
||||
whitelist: ['http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png'],
|
||||
fallbackImage: {
|
||||
type: 'fs',
|
||||
src: __dirname + '/../../test/fixtures/http/basemap.png'
|
||||
}
|
||||
}
|
||||
},
|
||||
redis: global.environment.redis,
|
||||
enable_cors: global.environment.enable_cors,
|
||||
unbuffered_logging: true, // for smoother teardown from tests
|
||||
log_format: null, // do not log anything
|
||||
afterLayergroupCreateCalls: 0,
|
||||
useProfiler: true,
|
||||
req2params: function(req, callback){
|
||||
|
||||
if ( req.query.testUnexpectedError ) {
|
||||
return callback('test unexpected error');
|
||||
}
|
||||
|
||||
// this is in case you want to test sql parameters eg ...png?sql=select * from my_table limit 10
|
||||
req.params = _.extend({}, req.params);
|
||||
if (req.params.token) {
|
||||
req.params.token = LayergroupToken.parse(req.params.token).token;
|
||||
}
|
||||
|
||||
_.extend(req.params, req.query);
|
||||
req.params.user = 'localhost';
|
||||
req.context = {user: 'localhost'};
|
||||
|
||||
req.params.dbhost = global.environment.postgres.host;
|
||||
req.params.dbport = req.params.dbport || global.environment.postgres.port;
|
||||
|
||||
req.params.dbuser = 'test_windshaft_publicuser';
|
||||
if (req.params.dbname !== 'windshaft_test2') {
|
||||
req.params.dbuser = 'test_windshaft_cartodb_user_1';
|
||||
}
|
||||
req.params.dbname = 'test_windshaft_cartodb_user_1_db';
|
||||
|
||||
|
||||
// increment number of calls counter
|
||||
global.req2params_calls = global.req2params_calls ? global.req2params_calls + 1 : 1;
|
||||
|
||||
// send the finished req object on
|
||||
callback(null,req);
|
||||
},
|
||||
afterLayergroupCreate: function(req, cfg, res, callback) {
|
||||
res.layercount = cfg.layers.length;
|
||||
// config.afterLayergroupCreateCalls++;
|
||||
callback(null);
|
||||
}
|
||||
|
||||
});
|
||||
488
test/acceptance/ported/support/test_client.js
Normal file
@@ -0,0 +1,488 @@
|
||||
var testHelper = require('../../../support/test_helper');
|
||||
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var step = require('step');
|
||||
var assert = require('../../../support/assert');
|
||||
var _ = require('underscore');
|
||||
var querystring = require('querystring');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var CartodbServer = require('../../../../lib/cartodb/server');
|
||||
var PortedServerOptions = require('./ported_server_options');
|
||||
|
||||
var DEFAULT_POINT_STYLE = [
|
||||
'#layer {',
|
||||
' marker-fill: #FF6600;',
|
||||
' marker-opacity: 1;',
|
||||
' marker-width: 16;',
|
||||
' marker-line-color: white;',
|
||||
' marker-line-width: 3;',
|
||||
' marker-line-opacity: 0.9;',
|
||||
' marker-placement: point;',
|
||||
' marker-type: ellipse;',
|
||||
' marker-allow-overlap: true;',
|
||||
'}'
|
||||
].join('');
|
||||
|
||||
module.exports = {
|
||||
createLayergroup: createLayergroup,
|
||||
withLayergroup: withLayergroup,
|
||||
|
||||
singleLayerMapConfig: singleLayerMapConfig,
|
||||
defaultTableMapConfig: defaultTableMapConfig,
|
||||
|
||||
getStaticBbox: getStaticBbox,
|
||||
getStaticCenter: getStaticCenter,
|
||||
getGrid: getGrid,
|
||||
getGridJsonp: getGridJsonp,
|
||||
getTorque: getTorque,
|
||||
getTile: getTile,
|
||||
getTileLayer: getTileLayer
|
||||
};
|
||||
|
||||
|
||||
var server = new CartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var jsonContentType = 'application/json; charset=utf-8';
|
||||
var jsContentType = 'text/javascript; charset=utf-8';
|
||||
var pngContentType = 'image/png';
|
||||
|
||||
function createLayergroup(layergroupConfig, options, callback) {
|
||||
if (!callback) {
|
||||
callback = options;
|
||||
options = {
|
||||
method: 'POST',
|
||||
statusCode: 200
|
||||
};
|
||||
}
|
||||
|
||||
var expectedResponse = {
|
||||
status: options.statusCode || 200,
|
||||
headers: options.headers || {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
step(
|
||||
function requestLayergroup() {
|
||||
var next = this;
|
||||
var request = layergroupRequest(layergroupConfig, options.method, options.callbackName, options.params);
|
||||
assert.response(serverInstance(options), request, expectedResponse, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function validateLayergroup(err, res) {
|
||||
assert.ok(!err, 'Failed to request layergroup');
|
||||
|
||||
var parsedBody;
|
||||
var layergroupid;
|
||||
if (options.callbackName) {
|
||||
global[options.callbackName] = function(layergroup) {
|
||||
layergroupid = layergroup.layergroupid;
|
||||
};
|
||||
// jshint ignore:start
|
||||
eval(res.body);
|
||||
// jshint ignore:end
|
||||
delete global[options.callbackName];
|
||||
} else {
|
||||
parsedBody = JSON.parse(res.body);
|
||||
layergroupid = parsedBody.layergroupid;
|
||||
if (layergroupid) {
|
||||
layergroupid = LayergroupToken.parse(layergroupid).token;
|
||||
}
|
||||
}
|
||||
|
||||
if (layergroupid) {
|
||||
var keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
var redisKey = 'map_cfg|' + layergroupid;
|
||||
keysToDelete[redisKey] = 0;
|
||||
testHelper.deleteRedisKeys(keysToDelete, function() {
|
||||
return callback(err, res, parsedBody);
|
||||
});
|
||||
} else {
|
||||
return callback(err, res, parsedBody);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function serverInstance(options) {
|
||||
if (options.server) {
|
||||
return options.server;
|
||||
}
|
||||
|
||||
if (options.serverOptions) {
|
||||
var otherServer = new CartodbServer(options.serverOptions);
|
||||
otherServer.req2params = options.serverOptions.req2params;
|
||||
otherServer.setMaxListeners(0);
|
||||
return otherServer;
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function layergroupRequest(layergroupConfig, method, callbackName, extraParams) {
|
||||
method = method || 'POST';
|
||||
|
||||
var request = {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
var urlParams = _.extend({}, extraParams);
|
||||
if (callbackName) {
|
||||
urlParams.callback = callbackName;
|
||||
}
|
||||
|
||||
if (method.toUpperCase() === 'GET') {
|
||||
request.method = 'GET';
|
||||
urlParams.config = JSON.stringify(layergroupConfig);
|
||||
} else {
|
||||
request.method = 'POST';
|
||||
request.data = JSON.stringify(layergroupConfig);
|
||||
}
|
||||
|
||||
if (Object.keys(urlParams).length) {
|
||||
request.url += '?' + querystring.stringify(urlParams);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function singleLayerMapConfig(sql, cartocss, cartocssVersion, interactivity) {
|
||||
return {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss || DEFAULT_POINT_STYLE,
|
||||
cartocss_version: cartocssVersion || '2.3.0',
|
||||
interactivity: interactivity,
|
||||
geom_column: 'the_geom'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function defaultTableMapConfig(tableName, cartocss, cartocssVersion, interactivity) {
|
||||
return singleLayerMapConfig(defaultTableQuery(tableName), cartocss, cartocssVersion, interactivity);
|
||||
}
|
||||
|
||||
function defaultTableQuery(tableName) {
|
||||
return _.template('SELECT * FROM <%= tableName %>', {tableName: tableName});
|
||||
}
|
||||
|
||||
function getStaticBbox(layergroupConfig, west, south, east, north, width, height, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = pngContentType;
|
||||
}
|
||||
|
||||
var url = [
|
||||
'static',
|
||||
'bbox',
|
||||
'<%= layergroupid %>',
|
||||
[west, south, east, north].join(','),
|
||||
width,
|
||||
height
|
||||
].join('/') + '.png';
|
||||
return getGeneric(layergroupConfig, url, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getStaticCenter(layergroupConfig, zoom, lat, lon, width, height, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = pngContentType;
|
||||
}
|
||||
|
||||
var url = [
|
||||
'static',
|
||||
'center',
|
||||
'<%= layergroupid %>',
|
||||
zoom,
|
||||
lat,
|
||||
lon,
|
||||
width,
|
||||
height
|
||||
].join('/') + '.png';
|
||||
return getGeneric(layergroupConfig, url, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getGrid(layergroupConfig, layer, z, x, y, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = jsonContentType;
|
||||
}
|
||||
|
||||
var options = {
|
||||
layer: layer,
|
||||
z: z,
|
||||
x: x,
|
||||
y: y,
|
||||
format: 'grid.json'
|
||||
};
|
||||
return getLayer(layergroupConfig, options, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getGridJsonp(layergroupConfig, layer, z, x, y, jsonpCallbackName, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = jsContentType;
|
||||
}
|
||||
|
||||
var options = {
|
||||
layer: layer,
|
||||
z: z,
|
||||
x: x,
|
||||
y: y,
|
||||
format: 'grid.json',
|
||||
jsonpCallbackName: jsonpCallbackName
|
||||
};
|
||||
return getLayer(layergroupConfig, options, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getTorque(layergroupConfig, layer, z, x, y, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = jsonContentType;
|
||||
}
|
||||
|
||||
var options = {
|
||||
layer: layer,
|
||||
z: z,
|
||||
x: x,
|
||||
y: y,
|
||||
format: 'torque.json'
|
||||
};
|
||||
return getLayer(layergroupConfig, options, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getTile(layergroupConfig, z, x, y, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = pngContentType;
|
||||
}
|
||||
|
||||
var options = {
|
||||
z: z,
|
||||
x: x,
|
||||
y: y,
|
||||
format: 'png'
|
||||
};
|
||||
return getLayer(layergroupConfig, options, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getTileLayer(layergroupConfig, options, expectedResponse, callback) {
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = pngContentType;
|
||||
}
|
||||
|
||||
return getLayer(layergroupConfig, options, expectedResponse, callback);
|
||||
}
|
||||
|
||||
function getLayer(layergroupConfig, options, expectedResponse, callback) {
|
||||
return getGeneric(layergroupConfig, tileUrlStrategy(options), expectedResponse, callback);
|
||||
}
|
||||
|
||||
function tileUrlStrategy(options) {
|
||||
var urlLayerPattern = [
|
||||
'<%= layer %>',
|
||||
'<%= z %>',
|
||||
'<%= x %>',
|
||||
'<%= y %>'
|
||||
].join('/') + '.<%= format %>';
|
||||
|
||||
if (options.jsonpCallbackName) {
|
||||
urlLayerPattern += '?callback=<%= jsonpCallbackName %>';
|
||||
}
|
||||
|
||||
var urlNoLayerPattern = [
|
||||
'<%= z %>',
|
||||
'<%= x %>',
|
||||
'<%= y %>'
|
||||
].join('/') + '.<%= format %>';
|
||||
|
||||
var urlTemplate = _.template((options.layer === undefined) ? urlNoLayerPattern : urlLayerPattern);
|
||||
|
||||
options.format = options.format || 'png';
|
||||
|
||||
return '<%= layergroupid %>/' + urlTemplate(_.defaults(options, { z: 0, x: 0, y: 0, layer: 0 }));
|
||||
}
|
||||
|
||||
function getGeneric(layergroupConfig, url, expectedResponse, callback) {
|
||||
if (_.isString(expectedResponse)) {
|
||||
expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': expectedResponse
|
||||
}
|
||||
};
|
||||
}
|
||||
var contentType = expectedResponse.headers['Content-Type'];
|
||||
|
||||
var layergroupid = null;
|
||||
|
||||
step(
|
||||
function requestLayergroup() {
|
||||
var next = this;
|
||||
var request = {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroupConfig)
|
||||
};
|
||||
var expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
assert.response(server, request, expectedResponse, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function validateLayergroup(err, res) {
|
||||
assert.ok(!err, 'Failed to create layergroup');
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
layergroupid = parsedBody.layergroupid;
|
||||
|
||||
assert.ok(layergroupid);
|
||||
|
||||
return res;
|
||||
},
|
||||
function requestTile(err, res) {
|
||||
assert.ok(!err, 'Invalid layergroup response: ' + res.body);
|
||||
|
||||
var next = this;
|
||||
|
||||
var finalUrl = '/database/windshaft_test/layergroup/' + _.template(url, {
|
||||
layergroupid: layergroupid
|
||||
});
|
||||
|
||||
var request = {
|
||||
url: finalUrl,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
if (contentType === pngContentType) {
|
||||
request.encoding = 'binary';
|
||||
}
|
||||
|
||||
assert.response(server, request, expectedResponse, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function validateTile(err, res) {
|
||||
assert.ok(!err, 'Failed to get tile');
|
||||
|
||||
var img;
|
||||
if (contentType === pngContentType) {
|
||||
img = new mapnik.Image.fromBytesSync(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
|
||||
var keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
var redisKey = 'map_cfg|' + LayergroupToken.parse(layergroupid).token;
|
||||
keysToDelete[redisKey] = 0;
|
||||
testHelper.deleteRedisKeys(keysToDelete, function() {
|
||||
return callback(err, res, img);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function withLayergroup(layergroupConfig, options, callback) {
|
||||
var validationLayergroupFn = function() {};
|
||||
if (!callback) {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (_.isFunction(options)) {
|
||||
validationLayergroupFn = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var layergroupExpectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
step(
|
||||
function requestLayergroup() {
|
||||
var next = this;
|
||||
var request = layergroupRequest(layergroupConfig, 'POST');
|
||||
assert.response(serverInstance(options), request, layergroupExpectedResponse, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function validateLayergroup(err, res) {
|
||||
assert.ok(!err, 'Failed to request layergroup');
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var layergroupid = parsedBody.layergroupid;
|
||||
|
||||
assert.ok(layergroupid, 'No layergroup was created');
|
||||
|
||||
validationLayergroupFn(res);
|
||||
|
||||
function requestTile(layergroupUrl, options, callback) {
|
||||
if (!callback) {
|
||||
callback = options;
|
||||
options = {
|
||||
statusCode: 200,
|
||||
contentType: pngContentType
|
||||
};
|
||||
}
|
||||
|
||||
var baseUrlTpl = '/database/windshaft_test/layergroup/<%= layergroupid %>';
|
||||
var finalUrl = _.template(baseUrlTpl, { layergroupid: layergroupid }) + layergroupUrl;
|
||||
|
||||
var request = {
|
||||
url: finalUrl,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
if (options.contentType === pngContentType) {
|
||||
request.encoding = 'binary';
|
||||
}
|
||||
|
||||
var tileExpectedResponse = {
|
||||
status: options.statusCode || 200,
|
||||
headers: {
|
||||
'Content-Type': options.contentType || pngContentType
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(serverInstance(options), request, tileExpectedResponse, function (res, err) {
|
||||
callback(err, res);
|
||||
});
|
||||
}
|
||||
|
||||
function finish(done) {
|
||||
var keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
var redisKey = 'map_cfg|' + LayergroupToken.parse(layergroupid).token;
|
||||
keysToDelete[redisKey] = 0;
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
}
|
||||
|
||||
return callback(err, requestTile, finish);
|
||||
}
|
||||
);
|
||||
}
|
||||
436
test/acceptance/ported/torque.js
Normal file
@@ -0,0 +1,436 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('torque', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
function checkCORSHeaders(res) {
|
||||
assert.equal(res.headers['access-control-allow-headers'], 'X-Requested-With, X-Prototype-Version, X-CSRF-Token');
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
}
|
||||
|
||||
it("missing required property from torque layer", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: 'select cartodb_id, the_geom from test_table',
|
||||
geom_column: 'the_geom',
|
||||
srid: 4326,
|
||||
cartocss: 'Map { marker-fill:blue; }'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function do_post1()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, parsed);
|
||||
var error = parsed.errors[0];
|
||||
assert.equal(error,
|
||||
"Missing required property '-torque-frame-count' in torque layer CartoCSS");
|
||||
return null;
|
||||
},
|
||||
function do_post2(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
var css = 'Map { -torque-frame-count: 2; }';
|
||||
layergroup.layers[0].options.cartocss = css;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse2(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, parsed);
|
||||
var error = parsed.errors[0];
|
||||
assert.equal(error,
|
||||
"Missing required property '-torque-resolution' in torque layer CartoCSS");
|
||||
return null;
|
||||
},
|
||||
function do_post3(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
var css = 'Map { -torque-frame-count: 2; -torque-resolution: 3; }';
|
||||
layergroup.layers[0].options.cartocss = css;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse3(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, parsed);
|
||||
var error = parsed.errors[0];
|
||||
assert.equal(error,
|
||||
"Missing required property '-torque-aggregation-function' in torque layer CartoCSS");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft/issues/150
|
||||
it.skip("unquoted property in torque layer", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: 'select updated_at as d, cartodb_id as id, the_geom from test_table',
|
||||
geom_column: 'the_geom',
|
||||
srid: 4326,
|
||||
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:"d"; ' +
|
||||
'-torque-aggregation-function:count(id); }'
|
||||
} }
|
||||
]
|
||||
};
|
||||
step(
|
||||
function do_post1()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, parsed);
|
||||
var error = parsed.errors[0];
|
||||
assert.equal(error, "Something meaningful here");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("can render tile for valid mapconfig", function(done) {
|
||||
|
||||
var mapconfig = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: "select 1 as id, '1970-01-02'::date as d, 'POINT(0 0)'::geometry as the_geom UNION select 2, " +
|
||||
"'1970-01-01'::date, 'POINT(1 1)'::geometry",
|
||||
geom_column: 'the_geom',
|
||||
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
|
||||
'-torque-aggregation-function:\'count(id)\'; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(mapconfig)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// CORS headers should be sent with response
|
||||
// from layergroup creation via POST
|
||||
checkCORSHeaders(res);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if ( expected_token ) {
|
||||
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
|
||||
} else {
|
||||
expected_token = parsedBody.layergroupid;
|
||||
}
|
||||
var meta = parsedBody.metadata;
|
||||
assert.ok(!_.isUndefined(meta),
|
||||
'No metadata in torque MapConfig creation response: ' + res.body);
|
||||
var tm = meta.torque;
|
||||
assert.ok(tm,
|
||||
'No "torque" in metadata:' + JSON.stringify(meta));
|
||||
var tm0 = tm[0];
|
||||
assert.ok(tm0,
|
||||
'No layer 0 in "torque" in metadata:' + JSON.stringify(tm));
|
||||
var expectedTorqueMetadata = {"start":0,"end":86400000,"data_steps":2,"column_type":"date"};
|
||||
assert.deepEqual(tm0, expectedTorqueMetadata);
|
||||
assert.deepEqual(meta.layers[0].meta, expectedTorqueMetadata);
|
||||
return null;
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_mapnik_error_1(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' ));
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
assert.equal(parsed.errors[0], "No 'mapnik' layers in MapConfig");
|
||||
return null;
|
||||
},
|
||||
function do_get_grid0(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.grid.json',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_mapnik_error_2(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' ));
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
assert.equal(parsed.errors[0], "Unsupported format grid.json");
|
||||
return null;
|
||||
},
|
||||
function do_get_torque0(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.json.torque',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_torque0_response(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var tile_content = [{"x__uint8":43,"y__uint8":43,"vals__uint8":[1,1],"dates__uint16":[0,1]}];
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(tile_content, parsed);
|
||||
return null;
|
||||
},
|
||||
function do_get_torque0_1(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.torque.json',
|
||||
method: 'GET'
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function check_torque0_response_1(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var tile_content = [{"x__uint8":43,"y__uint8":43,"vals__uint8":[1,1],"dates__uint16":[0,1]}];
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(tile_content, parsed);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Test that you cannot write to the database from a torque tile request
|
||||
//
|
||||
// Test for http://github.com/CartoDB/Windshaft/issues/130
|
||||
//
|
||||
it("database access is read-only", function(done) {
|
||||
var mapconfig = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: "select 'SRID=3857;POINT(0 0)'::geometry as g, now() as d,* from " +
|
||||
"test_table_inserter(st_setsrid(st_point(0,0),4326),'write')",
|
||||
geom_column: 'g',
|
||||
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
|
||||
'-torque-aggregation-function:\'count(*)\'; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(mapconfig)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
// TODO: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var msg = parsed.errors[0];
|
||||
assert.equal(msg, "TorqueRenderer: cannot execute INSERT in a read-only transaction");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft/issues/164
|
||||
it("gives a 500 on database connection refused", function(done) {
|
||||
|
||||
var mapconfig = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: "select 1 as id, '1970-01-03'::date as d, 'POINT(0 0)'::geometry as the_geom UNION select 2, " +
|
||||
"'1970-01-01'::date, 'POINT(1 1)'::geometry",
|
||||
geom_column: 'the_geom',
|
||||
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
|
||||
'-torque-aggregation-function:\'count(id)\'; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup?dbport=1234567',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(mapconfig)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function checkPost(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, parsed);
|
||||
var error = parsed.errors[0];
|
||||
assert.equal(error, "TorqueRenderer: cannot connect to the database");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("checks types for torque-specific styles", function(done) {
|
||||
var wrongStyle = ["Map {",
|
||||
"-torque-frame-count:512;",
|
||||
"-torque-animation-duration:30;",
|
||||
"-torque-time-attribute:'cartodb_id';",
|
||||
"-torque-aggregation-function:count(cartodb_id);", //unquoted aggregation function
|
||||
"-torque-resolution:4;",
|
||||
"-torque-data-aggregation:linear;",
|
||||
"}"].join(" ");
|
||||
var layergroup = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: 'select cartodb_id, the_geom from test_table',
|
||||
geom_column: 'the_geom',
|
||||
srid: 4326,
|
||||
cartocss: wrongStyle
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function request(){
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors, parsed);
|
||||
var error = parsed.errors[0];
|
||||
assert.equal(error,
|
||||
"Unexpected type for property '-torque-aggregation-function', expected string");
|
||||
done();
|
||||
return null;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
459
test/acceptance/ported/torque_boundaries.js
Normal file
@@ -0,0 +1,459 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('torque boundary points', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var layergroupIdToDelete = null;
|
||||
|
||||
beforeEach(function() {
|
||||
layergroupIdToDelete = null;
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
var keysToDelete = {
|
||||
'user:localhost:mapviews:global': 5
|
||||
};
|
||||
var mapKey = 'map_cfg|' + LayergroupToken.parse(layergroupIdToDelete).token;
|
||||
keysToDelete[mapKey] = 0;
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var boundaryPointsMapConfig = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: "WITH p AS ( " +
|
||||
" SELECT '1970-01-02'::date as d, " +
|
||||
// r1 is pixel resolution at zoom level 1
|
||||
// s1 is tile size at zoom level 1
|
||||
" 1e-40 as o, 78271.517578125 as r1, 20037508.5 as s1 )" +
|
||||
" SELECT 1 as i, d, ST_MakePoint(0,0) as ug FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 2, d, ST_MakePoint(-o,-o) FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 3, d, ST_MakePoint(-o,o) FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 4, d, ST_MakePoint(o,-o) FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 5, d, ST_MakePoint(o,o) FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 6, d, ST_MakePoint(-r1,r1) FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 7, d, ST_MakePoint(-r1/2,r1) FROM p" +
|
||||
" UNION ALL " +
|
||||
"SELECT 8, d, ST_MakePoint(-r1,-r1) FROM p" +
|
||||
" UNION ALL " +
|
||||
// this is discarded, being on the right boundary
|
||||
"SELECT 9, d, ST_MakePoint(s1,0) FROM p" +
|
||||
" UNION ALL " +
|
||||
// this is discarded, being on the top boundary
|
||||
"SELECT 10, d, ST_MakePoint(0,s1) FROM p" +
|
||||
" UNION ALL " +
|
||||
// this is discarded, rounding to the right boundary
|
||||
"SELECT 11, d, ST_MakePoint(s1-r1/2,0) FROM p" +
|
||||
" UNION ALL " +
|
||||
// this is discarded, rounding to the top boundary
|
||||
"SELECT 12, d, ST_MakePoint(0,s1-r1/2) FROM p",
|
||||
geom_column: 'ug',
|
||||
cartocss: 'Map { ' +
|
||||
'-torque-frame-count:2; ' +
|
||||
'-torque-resolution:1; ' +
|
||||
'-torque-time-attribute:"d"; ' +
|
||||
'-torque-data-aggregation:linear; ' +
|
||||
'-torque-aggregation-function:"count(i)"; }',
|
||||
cartocss_version: '1.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
var tileRequests = [
|
||||
{
|
||||
desc: '0/0/0',
|
||||
repr: [
|
||||
' ------',
|
||||
'| |',
|
||||
'| |',
|
||||
' ------'
|
||||
],
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
expects: [
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 128,
|
||||
vals__uint8: [{
|
||||
v: 2,
|
||||
d: 'i=9 and i=11'
|
||||
}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 128,
|
||||
y__uint8: 128,
|
||||
vals__uint8: [{
|
||||
v: 8,
|
||||
d: 'i=[1..8]'
|
||||
}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 128,
|
||||
y__uint8: 255,
|
||||
vals__uint8: [{
|
||||
v: 2,
|
||||
d: 'i=10 and i=12'
|
||||
}],
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: '1/0/0',
|
||||
repr: [
|
||||
'*00 | 10',
|
||||
'----+----',
|
||||
'01 | 11'
|
||||
],
|
||||
z: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
expects: [
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 0,
|
||||
vals__uint8: [{v: 2, d: 'i=6 (-r1,r1) and i=7(-r1/2,r1)'}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 1,
|
||||
vals__uint8: [{v: 2, d: 'i=1 and i=3'}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 255,
|
||||
vals__uint8: [{v: 2, d: 'i=10 and i=12'}],
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: '1/1/0',
|
||||
repr: [
|
||||
'00 | 10*',
|
||||
'----+----',
|
||||
'01 | 11'
|
||||
],
|
||||
z: 1,
|
||||
x: 1,
|
||||
y: 0,
|
||||
expects: [
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 0,
|
||||
vals__uint8: [{v: 2, d: 'i=9 and i=11'}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 0,
|
||||
y__uint8: 0,
|
||||
vals__uint8: [{v: 2, d: 'i=1 and i=5'}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 0,
|
||||
y__uint8: 255,
|
||||
vals__uint8: [{v: 2, d: 'i=10 and i=12'}],
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: '1/0/1',
|
||||
repr: [
|
||||
'00 | 10',
|
||||
'----+----',
|
||||
'*01 | 11'
|
||||
],
|
||||
z: 1,
|
||||
x: 0,
|
||||
y: 1,
|
||||
expects: [
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 255,
|
||||
vals__uint8: [{v: 3, d: 'i=1, i=2 and i=8'}],
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: '1/1/1',
|
||||
repr: [
|
||||
'00 | 10',
|
||||
'----+----',
|
||||
'01 | 11*'
|
||||
],
|
||||
z: 1,
|
||||
x: 1,
|
||||
y: 1,
|
||||
expects: [
|
||||
{
|
||||
x__uint8: 255,
|
||||
y__uint8: 255,
|
||||
vals__uint8: [{
|
||||
v: 2,
|
||||
d: 'i=9 and i=11'
|
||||
}],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 0,
|
||||
y__uint8: 255,
|
||||
vals__uint8: [{
|
||||
v: 2,
|
||||
d: 'i=1 and i=4'
|
||||
}],
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
tileRequests.forEach(function(tileRequest) {
|
||||
// See https://github.com/CartoDB/Windshaft/issues/186
|
||||
var desc = 'handles ' + tileRequest.desc + '.json.torque\n\n\t' + tileRequest.repr.join('\n\t') + '\n\n';
|
||||
it(desc, function (done) {
|
||||
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(boundaryPointsMapConfig)
|
||||
}, {}, function (res, err) {
|
||||
|
||||
assert.ok(!err, 'Failed to create layergroup');
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var expected_token = parsedBody.layergroupid;
|
||||
layergroupIdToDelete = expected_token;
|
||||
|
||||
var partialUrl = tileRequest.z + '/' + tileRequest.x + '/' + tileRequest.y;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/' + partialUrl + '.json.torque',
|
||||
method: 'GET'
|
||||
}, {}, function (res, err) {
|
||||
assert.ok(!err, 'Failed to get json');
|
||||
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
var i = 0;
|
||||
tileRequest.expects.forEach(function(expected) {
|
||||
assert.equal(
|
||||
parsed[i].x__uint8,
|
||||
expected.x__uint8,
|
||||
parsed[i].x__uint8 + ' == ' + expected.x__uint8 +
|
||||
'\nRESULT\n------' +
|
||||
'\n' + JSON.stringify(parsed, null, 4) +
|
||||
'\nEXPECTED\n--------' +
|
||||
'\n' + JSON.stringify(tileRequest.expects, null, 4)
|
||||
);
|
||||
assert.equal(
|
||||
parsed[i].y__uint8,
|
||||
expected.y__uint8,
|
||||
parsed[i].y__uint8 + ' == ' + expected.y__uint8 +
|
||||
'\nRESULT\n------' +
|
||||
'\n' + JSON.stringify(parsed, null, 4) +
|
||||
'\nEXPECTED\n--------' +
|
||||
'\n' + JSON.stringify(tileRequest.expects, null, 4)
|
||||
);
|
||||
|
||||
var j = 0;
|
||||
expected.vals__uint8.forEach(function(val) {
|
||||
assert.equal(
|
||||
parsed[i].vals__uint8[j],
|
||||
val.v,
|
||||
'desc: ' + val.d +
|
||||
'Number of points got=' + parsed.length + '; ' +
|
||||
'expected=' + tileRequest.expects.length +
|
||||
'\n\tindex=' + i +
|
||||
'\n\tvals__uint8 index=' + j +
|
||||
'\n\tgot=' + parsed[i].vals__uint8[j] +
|
||||
'\n\texpected=' + val.v +
|
||||
'\nRESULT\n------' +
|
||||
'\n' + JSON.stringify(parsed, null, 4) +
|
||||
'\nEXPECTED\n--------' +
|
||||
'\n' + JSON.stringify(tileRequest.expects, null, 4));
|
||||
|
||||
j++;
|
||||
});
|
||||
|
||||
i++;
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
parsed.length,
|
||||
tileRequest.expects.length,
|
||||
'Number of points did not match ' +
|
||||
'got=' + parsed.length + '; ' +
|
||||
'expected=' + tileRequest.expects.length +
|
||||
'\nRESULT\n------' +
|
||||
'\n' + JSON.stringify(parsed, null, 4) +
|
||||
'\nEXPECTED\n--------' +
|
||||
'\n' + JSON.stringify(tileRequest.expects, null, 4));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('regression london point', function(done) {
|
||||
var londonPointMapConfig = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: "SELECT " +
|
||||
"1 as i, " +
|
||||
"'1970-01-02'::date as d, " +
|
||||
"ST_MakePoint(-11309.9155492599,6715342.44989312) g",
|
||||
geom_column: 'g',
|
||||
cartocss: 'Map { ' +
|
||||
'-torque-frame-count:2; ' +
|
||||
'-torque-resolution:1; ' +
|
||||
'-torque-time-attribute:"d"; ' +
|
||||
'-torque-data-aggregation:linear; ' +
|
||||
'-torque-aggregation-function:"count(i)"; }',
|
||||
cartocss_version: '1.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(londonPointMapConfig)
|
||||
}, {}, function (res, err) {
|
||||
assert.ok(!err, 'Failed to create layergroup');
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var layergroupId = parsedBody.layergroupid;
|
||||
layergroupIdToDelete = layergroupId;
|
||||
|
||||
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/2/1/1.json.torque',
|
||||
method: 'GET'
|
||||
}, {}, function (res, err) {
|
||||
assert.ok(!err, 'Failed to request torque.json');
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.deepEqual(parsed, [{
|
||||
x__uint8: 255,
|
||||
y__uint8: 172,
|
||||
vals__uint8: [1],
|
||||
dates__uint16: [0]
|
||||
}]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should consider resolution for least value in query', function(done) {
|
||||
var londonPointMapConfig = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ type: 'torque', options: {
|
||||
sql: "" +
|
||||
"SELECT " +
|
||||
"0 as i, " +
|
||||
"st_transform('0101000020E6100000FABD3AB4B5031C400581A80ECC2F4940'::geometry, 3857) as g " +
|
||||
"UNION ALL " +
|
||||
"SELECT " +
|
||||
"2 as i, " +
|
||||
"st_transform('0101000020E61000006739E30EAE031C406625C0C3C72F4940'::geometry, 3857) as g " +
|
||||
"UNION ALL " +
|
||||
"SELECT " +
|
||||
"3 as i, " +
|
||||
"st_transform('0101000020E6100000E26DB8A2A7031C40C8BAA5C2C52F4940'::geometry, 3857) as g",
|
||||
geom_column: 'g',
|
||||
cartocss: 'Map { ' +
|
||||
'-torque-frame-count:1; ' +
|
||||
'-torque-animation-duration:30;' +
|
||||
'-torque-time-attribute:"i"; ' +
|
||||
'-torque-aggregation-function:"count(i)"; ' +
|
||||
'-torque-resolution:2; ' +
|
||||
'-torque-data-aggregation: cumulative; }',
|
||||
cartocss_version: '1.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(londonPointMapConfig)
|
||||
}, {}, function (res, err) {
|
||||
assert.ok(!err, 'Failed to create layergroup');
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var layergroupId = parsedBody.layergroupid;
|
||||
layergroupIdToDelete = layergroupId;
|
||||
|
||||
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/13/4255/2765.json.torque',
|
||||
method: 'GET'
|
||||
}, {}, function (res, err) {
|
||||
assert.ok(!err, 'Failed to request torque.json');
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.deepEqual(parsed, [
|
||||
{
|
||||
x__uint8: 47,
|
||||
y__uint8: 127,
|
||||
vals__uint8: [2],
|
||||
dates__uint16: [0]
|
||||
},
|
||||
{
|
||||
x__uint8: 48,
|
||||
y__uint8: 127,
|
||||
vals__uint8: [1],
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
155
test/acceptance/ported/torque_png.js
Normal file
@@ -0,0 +1,155 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('torque png renderer', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var torquePngPointsMapConfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced where the_geom" +
|
||||
" && ST_MakeEnvelope(-90, 0, 90, 65)",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' buffer-size:0;',
|
||||
' -torque-frame-count:1;',
|
||||
' -torque-animation-duration:30;',
|
||||
' -torque-time-attribute:"cartodb_id";',
|
||||
' -torque-aggregation-function:"count(cartodb_id)";',
|
||||
' -torque-resolution:1;',
|
||||
' -torque-data-aggregation:linear;',
|
||||
'}',
|
||||
'#populated_places_simple_reduced{',
|
||||
' comp-op: multiply;',
|
||||
' marker-fill-opacity: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-width: 0;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: rectangle;',
|
||||
' marker-width: 3;',
|
||||
' marker-fill: #FFCC00;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var tileRequests = [
|
||||
{
|
||||
z: 2,
|
||||
x: 2,
|
||||
y: 1,
|
||||
layer: 0,
|
||||
format: 'torque.png'
|
||||
},
|
||||
{
|
||||
z: 2,
|
||||
x: 1,
|
||||
y: 1,
|
||||
layer: 0,
|
||||
format: 'torque.png'
|
||||
}
|
||||
];
|
||||
|
||||
function torquePngFixture(zxy) {
|
||||
return './test/fixtures/torque/populated_places_simple_reduced-' + zxy.join('.') + '.png';
|
||||
}
|
||||
|
||||
tileRequests.forEach(function(tileRequest) {
|
||||
var z = tileRequest.z,
|
||||
x = tileRequest.x,
|
||||
y = tileRequest.y;
|
||||
var zxy = [z, x, y];
|
||||
it('tile ' + zxy.join('/') + '.torque.png', function (done) {
|
||||
testClient.getTileLayer(torquePngPointsMapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
var mapConfigTorqueOffset = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
'buffer-size:0;',
|
||||
'-torque-frame-count:1;',
|
||||
'-torque-animation-duration:30;',
|
||||
'-torque-time-attribute:"cartodb_id";',
|
||||
'-torque-aggregation-function:"count(cartodb_id)";',
|
||||
'-torque-resolution:2;',
|
||||
'-torque-data-aggregation:linear;',
|
||||
'}',
|
||||
'',
|
||||
'#populated_places_simple_reduced{',
|
||||
' comp-op: lighter;',
|
||||
' marker-fill-opacity: 0.9;',
|
||||
' marker-line-color: #2167AB;',
|
||||
' marker-line-width: 5;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: ellipse;',
|
||||
' marker-width: 6;',
|
||||
' marker-fill: #FF9900;',
|
||||
'}',
|
||||
'#populated_places_simple_reduced[frame-offset=1] {',
|
||||
' marker-width:8;',
|
||||
' marker-fill-opacity:0.45; ',
|
||||
'}',
|
||||
'#populated_places_simple_reduced[frame-offset=2] {',
|
||||
' marker-width:10;',
|
||||
' marker-fill-opacity:0.225; ',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it('torque static map with offset', function(done) {
|
||||
var w = 600,
|
||||
h = 400;
|
||||
|
||||
testClient.getStaticBbox(mapConfigTorqueOffset, -170, -87, 170, 87, w, h, function(err, res, img) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(img.width(), w);
|
||||
assert.equal(img.height(), h);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
97
test/acceptance/ported/torque_zero_zero.js
Normal file
@@ -0,0 +1,97 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('torque tiles at 0,0 point', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
/*
|
||||
Tiles are represented as in:
|
||||
|
||||
---------
|
||||
| TL | TR |
|
||||
|--(0,0)--|
|
||||
| BL | BR |
|
||||
---------
|
||||
*/
|
||||
|
||||
var tiles = [
|
||||
{
|
||||
what: 'tl',
|
||||
x: 3,
|
||||
y: 3,
|
||||
expects: []
|
||||
},
|
||||
{
|
||||
what: 'tr',
|
||||
x: 4,
|
||||
y: 3,
|
||||
expects: []
|
||||
},
|
||||
{
|
||||
what: 'bl',
|
||||
x: 3,
|
||||
y: 4,
|
||||
expects: [{"x__uint8":1,"y__uint8":1,"vals__uint8":[1],"dates__uint16":[0]}]
|
||||
},
|
||||
{
|
||||
what: 'br',
|
||||
x: 4,
|
||||
y: 4,
|
||||
expects: [{"x__uint8":0,"y__uint8":1,"vals__uint8":[1],"dates__uint16":[0]}]
|
||||
}
|
||||
];
|
||||
|
||||
tiles.forEach(function(tile) {
|
||||
it(tile.what, function(done) {
|
||||
|
||||
var query = 'select 1 cartodb_id,' +
|
||||
' ST_Transform(ST_SetSRID(ST_MakePoint(0, 0), 4326), 3857) the_geom_webmercator';
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: query,
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' -torque-time-attribute: "cartodb_id";',
|
||||
' -torque-aggregation-function: "count(cartodb_id)";',
|
||||
' -torque-frame-count: 1;',
|
||||
' -torque-animation-duration: 15;',
|
||||
' -torque-resolution: 128',
|
||||
'}',
|
||||
'#layer {',
|
||||
' marker-fill: #fff;',
|
||||
' marker-fill-opacity: 0.4;',
|
||||
' marker-width: 1;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testClient.getTorque(mapConfig, 0, 3, tile.x, tile.y, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), tile.expects);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
131
test/acceptance/ported/wrap.js
Normal file
@@ -0,0 +1,131 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('wrap x coordinate', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
describe('renders correct tile', function() {
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
function plainTorqueMapConfig(plainColor) {
|
||||
return {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: plainColor
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced " +
|
||||
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' buffer-size:0;',
|
||||
' -torque-frame-count:1;',
|
||||
' -torque-animation-duration:30;',
|
||||
' -torque-time-attribute:"cartodb_id";',
|
||||
' -torque-aggregation-function:"count(cartodb_id)";',
|
||||
' -torque-resolution:1;',
|
||||
' -torque-data-aggregation:linear;',
|
||||
'}',
|
||||
'#populated_places_simple_reduced{',
|
||||
' comp-op: multiply;',
|
||||
' marker-fill-opacity: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-width: 0;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: rectangle;',
|
||||
' marker-width: 3;',
|
||||
' marker-fill: #FFCC00;',
|
||||
'}'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
tile: {
|
||||
z: 2,
|
||||
x: -2,
|
||||
y: 1,
|
||||
layer: 'all',
|
||||
format: 'png'
|
||||
},
|
||||
fixture: {
|
||||
z: 2,
|
||||
x: 2,
|
||||
y: 1
|
||||
},
|
||||
plainColor: 'white'
|
||||
},
|
||||
{
|
||||
tile: {
|
||||
z: 2,
|
||||
x: -3,
|
||||
y: 1,
|
||||
layer: 'all',
|
||||
format: 'png'
|
||||
},
|
||||
fixture: {
|
||||
z: 2,
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
plainColor: '#339900'
|
||||
}
|
||||
];
|
||||
|
||||
function blendPngFixture(zxy) {
|
||||
return './test/fixtures/blend/blend-plain-torque-' + zxy.join('.') + '.png';
|
||||
}
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
var tileRequest = testScenario.tile;
|
||||
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
|
||||
var fixtureZxy = [testScenario.fixture.z, testScenario.fixture.x, testScenario.fixture.y];
|
||||
it('tile all/' + zxy.join('/') + '.png', function (done) {
|
||||
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapnik', function() {
|
||||
it("can get a tile with negative x coordinate", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 2, -2, 1, function(err, res, img) {
|
||||
assert.ok(!err);
|
||||
assert.ok(img);
|
||||
assert.equal(img.width(), 256);
|
||||
assert.equal(img.height(), 256);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -4,16 +4,16 @@ var assert = require('../support/assert');
|
||||
var querystring = require('querystring');
|
||||
var step = require('step');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
|
||||
suite('server', function() {
|
||||
describe('server', function() {
|
||||
|
||||
// TODO: I guess this should be a 404 instead...
|
||||
test("get call to server returns 200", function(done){
|
||||
it("get call to server returns 200", function(done){
|
||||
step(
|
||||
function doGet() {
|
||||
var next = this;
|
||||
@@ -23,7 +23,7 @@ suite('server', function() {
|
||||
},{}, function(res, err) { next(err,res); });
|
||||
},
|
||||
function doCheck(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
assert.ok(res.statusCode, 200);
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(!cc);
|
||||
@@ -35,7 +35,7 @@ suite('server', function() {
|
||||
);
|
||||
});
|
||||
|
||||
test("get call to server returns 200", function(done){
|
||||
it("get call to server returns 200", function(done){
|
||||
assert.response(server, {
|
||||
url: '/version',
|
||||
method: 'GET'
|
||||
@@ -44,25 +44,19 @@ suite('server', function() {
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('windshaft_cartodb'), "No 'windshaft_cartodb' version in " + parsed);
|
||||
console.log("Windshaft-cartodb: " + parsed.windshaft_cartodb);
|
||||
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
|
||||
console.log("Windshaft: " + parsed.windshaft);
|
||||
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
|
||||
console.log("Grainstore: " + parsed.grainstore);
|
||||
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
|
||||
console.log("Node-mapnik: " + parsed.node_mapnik);
|
||||
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
|
||||
console.log("Mapnik: " + parsed.mapnik);
|
||||
// TODO: check actual versions ?
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('server old_api', function() {
|
||||
describe('server old_api', function() {
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/115
|
||||
test.skip("get'ing tile with not-strictly-valid style", function(done) {
|
||||
it.skip("get'ing tile with not-strictly-valid style", function(done) {
|
||||
var style = querystring.stringify({style: '#test_table{line-color:black}}', style_version: '2.0.0'});
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
require('../support/test_helper');
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var qs = require('querystring');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('get requests x-cache-channel', function() {
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var statusOkResponse = {
|
||||
status: 200
|
||||
};
|
||||
@@ -99,7 +110,10 @@ describe('get requests x-cache-channel', function() {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, JSON.parse(res.body).layergroupid);
|
||||
var layergroupId = JSON.parse(res.body).layergroupid;
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
callback(null, layergroupId, res);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -107,12 +121,10 @@ describe('get requests x-cache-channel', function() {
|
||||
describe('header should be present', function() {
|
||||
|
||||
it('/api/v1/map Map instantiation', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
layergroupRequest,
|
||||
statusOkResponse,
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
var testFn = validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table');
|
||||
withLayergroupId(function(err, layergroupId, res) {
|
||||
testFn(res);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) {
|
||||
@@ -219,7 +231,7 @@ describe('get requests x-cache-channel', function() {
|
||||
|
||||
var templateName = 'x_cache';
|
||||
|
||||
before(function(done) {
|
||||
beforeEach(function(done) {
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
@@ -249,7 +261,7 @@ describe('get requests x-cache-channel', function() {
|
||||
);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
afterEach(function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// small benchmark to execute with nodejs
|
||||
|
||||
var http = require('http')
|
||||
|
||||
if ( process.argv.length < 3 ) {
|
||||
console.error("Usage: " + process.argv[1] + " <baseurl> [<map_key>]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var baseurl = process.argv[2];
|
||||
console.log("Baseurl is " + baseurl);
|
||||
var baseurl_comps = baseurl.match(/(https?:\/\/)?([^:\/]*)(:([^\/]*))?(\/.*).*/);
|
||||
|
||||
var options = {
|
||||
host: baseurl_comps[2],
|
||||
port: baseurl_comps[4] ? baseurl_comps[4] : 8181,
|
||||
path: baseurl_comps[5] + '/{z}/{x}/{y}.png?cache_buster=0'
|
||||
};
|
||||
|
||||
if ( process.argv.length > 3 ) {
|
||||
options.path += '&map_key=' + process.argv[3];
|
||||
}
|
||||
|
||||
console.dir(options);
|
||||
|
||||
function randInt(min, max) {
|
||||
return min + Math.floor(Math.random()*(max- min +1));
|
||||
}
|
||||
|
||||
var start_time = new Date().getTime();
|
||||
function end() {
|
||||
var end_time = new Date().getTime();
|
||||
var t = (end_time - start_time)/1000;
|
||||
console.log("ok: ", ok)
|
||||
console.log("error: ", error)
|
||||
console.log("time: ", t);
|
||||
console.log("req/s: ", ok/t);
|
||||
}
|
||||
|
||||
var N = 1000
|
||||
var ok = 0;
|
||||
var error = 0;
|
||||
|
||||
function fail(msg) {
|
||||
console.log(msg);
|
||||
++ error;
|
||||
if ( error + ok === N ) end();
|
||||
}
|
||||
|
||||
function pass() {
|
||||
++ ok;
|
||||
if ( error + ok === N ) end();
|
||||
}
|
||||
|
||||
for(var i = 0; i < N; ++i) {
|
||||
var opt = {
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
path: new String(options.path)
|
||||
};
|
||||
opt.path = opt.path.replace('{z}', 2).replace('{x}', randInt(0, 3)).replace('{y}', randInt(0, 3));
|
||||
//console.log(opt.path)
|
||||
http.get(opt, function(res) {
|
||||
res.body = '';
|
||||
res.on('data', function(chunk) {
|
||||
// Save only first chunk, to reduce cost of the operation
|
||||
if ( res.body.length == 0 ) res.body += chunk;
|
||||
});
|
||||
res.on('end', function() {
|
||||
if ( res.statusCode == 200 ) pass();
|
||||
else {
|
||||
fail(res.statusCode + ' http://' + opt.host + ':' + opt.port + opt.path + ' ' + res.body);
|
||||
}
|
||||
});
|
||||
}).on('error', function(e) {
|
||||
fail('unknown (http) error');
|
||||
});
|
||||
}
|
||||
|
||||
BIN
test/fixtures/blend/blend-filtering-layers-0.1-zxy-1.0.0.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
test/fixtures/blend/blend-filtering-layers-0.2-zxy-1.0.0.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
test/fixtures/blend/blend-filtering-layers-0.3-zxy-1.0.0.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
test/fixtures/blend/blend-filtering-layers-1.2-zxy-1.0.0.png
vendored
Normal file
|
After Width: | Height: | Size: 33 KiB |