Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c314819ade |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,7 +5,3 @@ config/environments/*.js
|
||||
tools/munin/windshaft.conf
|
||||
logs/
|
||||
pids/
|
||||
redis.pid
|
||||
test.log
|
||||
npm-debug.log
|
||||
coverage/
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
test/results/
|
||||
test/monkey/
|
||||
test/benchmark.js
|
||||
test/support/
|
||||
98
.jshintrc
98
.jshintrc
@@ -1,98 +0,0 @@
|
||||
{
|
||||
// // JSHint Default Configuration File (as on JSHint website)
|
||||
// // See http://jshint.com/docs/ for more details
|
||||
//
|
||||
// "maxerr" : 50, // {int} Maximum error before stopping
|
||||
//
|
||||
// // 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
|
||||
"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 () { } ());`
|
||||
// "indent" : 4, // {int} Number of spaces to use for indentation
|
||||
// "latedef" : false, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
// "noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
// "plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
// "quotmark" : false, // Quotation mark consistency:
|
||||
// // false : do nothing (default)
|
||||
// // true : ensure whatever is used is consistent
|
||||
// // "single" : require single quotes
|
||||
// // "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
// "strict" : true, // true: Requires all functions run in ES5 Strict Mode
|
||||
// "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
|
||||
"maxlen" : 120, // {int} Max number of characters per line
|
||||
//
|
||||
// // Relaxing
|
||||
// "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
// "boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
// "eqnull" : false, // true: Tolerate use of `== null`
|
||||
// "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
// "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
// "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// // (ex: `for each`, multiple try/catch, function expression…)
|
||||
// "evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
// "expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
// "funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
// "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
// "iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
// "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
// "laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
// "laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
// "loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
// "multistr" : false, // true: Tolerate multi-line strings
|
||||
// "noyield" : false, // true: Tolerate generator functions with no yield statement in them.
|
||||
// "notypeof" : false, // true: Tolerate invalid typeof operator values
|
||||
// "proto" : false, // true: Tolerate using the `__proto__` property
|
||||
// "scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
// "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
// "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
// "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
// "validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
//
|
||||
// // Environments
|
||||
// "browser" : true, // Web Browser (window, document, etc)
|
||||
// "browserify" : false, // Browserify (node.js code in the browser)
|
||||
// "couch" : false, // CouchDB
|
||||
// "devel" : true, // Development/debugging (alert, confirm, etc)
|
||||
// "dojo" : false, // Dojo Toolkit
|
||||
// "jasmine" : false, // Jasmine
|
||||
// "jquery" : false, // jQuery
|
||||
// "mocha" : true, // Mocha
|
||||
// "mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
// "nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
// "prototypejs" : false, // Prototype and Scriptaculous
|
||||
// "qunit" : false, // QUnit
|
||||
// "rhino" : false, // Rhino
|
||||
// "shelljs" : false, // ShellJS
|
||||
// "worker" : false, // Web Workers
|
||||
// "wsh" : false, // Windows Scripting Host
|
||||
// "yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : { // additional predefined global variables
|
||||
"describe": true,
|
||||
"before": true,
|
||||
"after": true,
|
||||
"beforeEach": true,
|
||||
"afterEach": true,
|
||||
"it": true,
|
||||
"suite": true,
|
||||
"suiteSetup": true,
|
||||
"test": true,
|
||||
"suiteTeardown": true
|
||||
}
|
||||
}
|
||||
15
.travis.yml
15
.travis.yml
@@ -1,18 +1,19 @@
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
|
||||
- sudo apt-get install postgresql-plpython-9.3
|
||||
- sudo apt-add-repository --yes ppa:mapnik/v2.1.0
|
||||
- sudo apt-get update -q
|
||||
- sudo apt-get install -q libmapnik-dev
|
||||
- createdb template_postgis
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
# Tell npm to use known registrars:
|
||||
# see http://blog.npmjs.org/post/78085451721/npms-self-signed-certificate-is-no-more
|
||||
- npm config set ca ""
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres
|
||||
- NPROCS=1 JOBS=1
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.8"
|
||||
- "0.10"
|
||||
|
||||
notifications:
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. Drop npm-shrinkwrap.json
|
||||
5. Run npm shrinkwrap to recreate npm-shrinkwrap.json
|
||||
6. Commit package.json, npm-shrinwrap.json, NEWS
|
||||
7. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
8. Announce on cartodb@googlegroups.com
|
||||
9. Stub NEWS/package for next version
|
||||
1. Ensure proper version in package.json
|
||||
2. Ensure NEWS section exists for the new version, review it, add release date
|
||||
3. Drop npm-shrinkwrap.json
|
||||
4. Run npm install
|
||||
5. Test (make check or npm test), fix if broken before proceeding
|
||||
6. Run npm shrinkwrap
|
||||
7. Set "from" in npm-shrinkwrap.json for known packages
|
||||
(windshaft, node-varnish, grainstore...)
|
||||
8. Commit package.json, npm-shrinwrap.json, NEWS
|
||||
9. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
10. Announce
|
||||
11. Stub NEWS/package for next version
|
||||
|
||||
Versions:
|
||||
|
||||
Bugfix releases increment Patch component of version.
|
||||
Feature releases increment Minor and set Patch to zero.
|
||||
If backward compatibility is broken, increment Major and
|
||||
set to zero Minor and Patch.
|
||||
|
||||
Branches named 'b<Major>.<Minor>' are kept for any critical
|
||||
fix that might need to be shipped before next feature release
|
||||
is ready.
|
||||
|
||||
27
LICENCE
Normal file
27
LICENCE
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2011, Vizzuality
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
This product includes software developed by Vizzuality.
|
||||
4. Neither the name of Vizzuality nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
27
LICENSE
27
LICENSE
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2014, Vizzuality
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
38
Makefile
38
Makefile
@@ -1,10 +1,7 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
pre-install:
|
||||
@$(SHELL) ./scripts/check-node-canvas.sh
|
||||
srcdir=$(shell pwd)
|
||||
|
||||
all:
|
||||
@$(SHELL) ./scripts/install.sh
|
||||
npm install
|
||||
|
||||
clean:
|
||||
rm -rf node_modules/*
|
||||
@@ -18,24 +15,21 @@ config.status--test:
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
|
||||
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
|
||||
check-local: config/environments/test.js
|
||||
./run_tests.sh ${RUNTESTFLAGS} \
|
||||
test/unit/cartodb/*.js \
|
||||
test/acceptance/*.js
|
||||
|
||||
jshint:
|
||||
@echo "***jshint***"
|
||||
@./node_modules/.bin/jshint lib/ test/ app.js
|
||||
check-submodules:
|
||||
PATH="$$PATH:$(srcdir)/node_modules/.bin/"; \
|
||||
for sub in windshaft grainstore node-varnish mapnik; do \
|
||||
if test -e node_modules/$${sub}; then \
|
||||
echo "Testing submodule $${sub}"; \
|
||||
make -C node_modules/$${sub} check || exit 1; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
test-all: jshint test
|
||||
check-full: check-local check-submodules
|
||||
|
||||
coverage:
|
||||
@RUNTESTFLAGS=--with-coverage make test
|
||||
check: check-local
|
||||
|
||||
check: test
|
||||
|
||||
.PHONY: pre-install test jshint coverage
|
||||
|
||||
737
NEWS.md
737
NEWS.md
@@ -1,548 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 2.2.0
|
||||
|
||||
Released 2015-04-29
|
||||
|
||||
Enhancements:
|
||||
- jshint is run against tests
|
||||
- tests moved to mocha's `describe`
|
||||
|
||||
New features:
|
||||
- Fastly surrogate keys invalidation for named maps
|
||||
* **New configuration entry**: `fastly`. Check example configurations for more information.
|
||||
- `PgQueryRunner` extracted from `QueryTablesApi` so it can be reused in new `TablesExtentApi`
|
||||
- New top level element, `view`, in templates that holds attributes to identify the map scene.
|
||||
- Named maps static preview in /api/v1/map/static/named/:name/:width/:height.:format endpoint
|
||||
* It will be invalidated if the named map changes
|
||||
* But have a Cache-Control header with a 2 hours max-age, won't be invalidated on data changes
|
||||
|
||||
|
||||
## 2.1.3
|
||||
|
||||
Released 2015-04-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.42.2](https://github.com/CartoDB/Windshaft/releases/tag/0.42.2)
|
||||
|
||||
|
||||
## 2.1.2
|
||||
|
||||
Released 2015-04-15
|
||||
|
||||
Bug fixes:
|
||||
- Do not check statsd_client in profiler
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.42.1](https://github.com/CartoDB/Windshaft/releases/tag/0.42.1)
|
||||
|
||||
|
||||
## 2.1.1
|
||||
|
||||
Released 2015-04-10
|
||||
|
||||
Bug fixes:
|
||||
- Do not add x-cache-channel header for GET template routes
|
||||
|
||||
|
||||
## 2.1.0
|
||||
|
||||
Released 2015-04-09
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.42.0](https://github.com/CartoDB/Windshaft/releases/tag/0.42.0)
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
Released 2015-04-08
|
||||
|
||||
Announcements:
|
||||
- Major release with **BREAKING CHANGES**:
|
||||
* Removes `/:table/infowindow`, `/:table/map_metadata` and `/:table/flush_cache` endpoints
|
||||
* Sample configuration removes `/tiles/template` and `/tiles/layergroup`
|
||||
* URLs to use from now on are: `/api/v1/map/named` and `/api/v1/map`
|
||||
* No more state changes for styles
|
||||
* No more dump stats for renderers: SIGUSR1 and SIGUSR2 signals
|
||||
* Removes query params:
|
||||
- sql
|
||||
- geom_type
|
||||
- cache_buster
|
||||
- cache_policy
|
||||
- interactivity
|
||||
- style
|
||||
- style_version
|
||||
- style_convert
|
||||
- scale_factor
|
||||
* Affected tables for x-cache-channel will use direct connection to postgresql
|
||||
* Removes some metrics: authorized times ones
|
||||
* Mapnik renderer configuration not part of the `renderer` root configuration
|
||||
- All configuration must be moved into `renderer.mapnik`, see `config/environments/*.js.example` for reference
|
||||
- Removes rollbar as optional logger
|
||||
|
||||
|
||||
## 1.30.0
|
||||
|
||||
Released 2015-03-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.40.0](https://github.com/CartoDB/Windshaft/releases/tag/0.40.0)
|
||||
|
||||
|
||||
## 1.29.0
|
||||
|
||||
Released 2015-03-09
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.39.0](https://github.com/CartoDB/Windshaft/releases/tag/0.39.0)
|
||||
|
||||
|
||||
## 1.28.5
|
||||
|
||||
Released 2015-02-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.37.5](https://github.com/CartoDB/Windshaft/releases/tag/0.37.5)
|
||||
|
||||
|
||||
## 1.28.4
|
||||
|
||||
Released 2015-02-18
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.37.4](https://github.com/CartoDB/Windshaft/releases/tag/0.37.4)
|
||||
|
||||
|
||||
## 1.28.3
|
||||
|
||||
Released 2015-02-17
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.37.3](https://github.com/CartoDB/Windshaft/releases/tag/0.37.3)
|
||||
|
||||
|
||||
## 1.28.2
|
||||
|
||||
Released 2015-02-17
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.37.2](https://github.com/CartoDB/Windshaft/releases/tag/0.37.2)
|
||||
|
||||
|
||||
## 1.28.1
|
||||
|
||||
Released 2015-02-17
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.37.1](https://github.com/CartoDB/Windshaft/releases/tag/0.37.1)
|
||||
|
||||
|
||||
## 1.28.0
|
||||
|
||||
Released 2015-02-17
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [0.37.0](https://github.com/CartoDB/Windshaft/releases/tag/0.37.0)
|
||||
|
||||
New features:
|
||||
- QueryTablesApi will always use an authenticated query to retrieve last update, this allows to query affected private
|
||||
tables last update (#253)
|
||||
|
||||
|
||||
## 1.27.0
|
||||
|
||||
Released 2015-02-16
|
||||
|
||||
Announcements:
|
||||
- Adds default image placeholder for http renderer to use as fallback
|
||||
|
||||
New features:
|
||||
- `named` layers type, see [MapConfig-NamedMaps-extension](docs/MapConfig-NamedMaps-extension.md)
|
||||
- Starts using datasource per layer feature from Windshaft ([2c7bc6a](https://github.com/CartoDB/Windshaft-cartodb/commit/2c7bc6adde561b20ed955b905e3c7bcd6795d128))
|
||||
|
||||
Bugfixes:
|
||||
- Fixes tests with beforeEach and afterEach triggers
|
||||
|
||||
|
||||
## 1.26.2
|
||||
|
||||
Released 2015-01-28
|
||||
|
||||
Bugfixes:
|
||||
- Accept 'open' string in templates' `auth` as authorized.
|
||||
|
||||
|
||||
## 1.26.1
|
||||
|
||||
Released 2015-01-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.35.1, see https://github.com/CartoDB/Windshaft/pull/254
|
||||
|
||||
|
||||
## 1.26.0
|
||||
|
||||
Released 2015-01-27
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.35.0, supports mapconfig version `1.3.0`
|
||||
|
||||
|
||||
## 1.25.0
|
||||
|
||||
Released 2015-01-26
|
||||
|
||||
Announcements:
|
||||
- No more signed maps (#227 and #238)
|
||||
- Splits template maps endpoint into its own controller
|
||||
- Removes TemplateMaps dependency on SignedMaps
|
||||
- Token validation is done against the template
|
||||
- Template is always extended with default values for auth and placeholders
|
||||
- MapConfig is extended, in order to validate auth_tokens, with template info:
|
||||
- template name
|
||||
- template auth
|
||||
- No more locks to create, update or delete templates
|
||||
- Trusting in redis' hash semantics
|
||||
- Some tradeoffs:
|
||||
* A client having more templates than allowed by a race condition between limit (HLEN) check and creation (HSET)
|
||||
* Updating a template could happen while deleting it, resulting in a new template
|
||||
* Templates already instantiated will be accessible through their layergroup so it is possible to continue requesting tiles/grids/etc.
|
||||
- Authorisation is now handled by template maps
|
||||
- Template instantiation returns new instances with default values if they are missing
|
||||
|
||||
|
||||
New features:
|
||||
- Basic layergroup validation on named map creation/update (#196)
|
||||
- Add named maps surrogate keys and call invalidation on template modification/deletion (#247)
|
||||
- Extends TemplateMaps backend with EventEmitter
|
||||
- Emits for create, update and delete templates
|
||||
- VarnishHttpCacheBackend will invalidate a varnish instance via HTTP PURGE method
|
||||
- In the future there could be more backends, for instance to invalidate a CDN.
|
||||
- NamedMapsEntry has the responsibility to generate a cache key for a named map
|
||||
- It probably should receive a template/named map instead of owner and template name
|
||||
- SurrogateKeysCache is responsible to tag responses with a header
|
||||
- It also is responsible for invalidations given an Invalidation Backend
|
||||
- In the future it could have several backends so it can invalidates different caches
|
||||
- SurrogateKeysCache is subscribed to TemplateMaps events to do the invalidations
|
||||
|
||||
|
||||
## 1.24.0
|
||||
|
||||
Released 2015-01-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.34.0 for retina support
|
||||
|
||||
|
||||
## 1.23.1
|
||||
|
||||
Released 2015-01-14
|
||||
|
||||
Announcements:
|
||||
- Regenerate npm-shrinkwrap.json
|
||||
|
||||
|
||||
## 1.23.0
|
||||
|
||||
Released 2015-01-14
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.33.0
|
||||
|
||||
New features:
|
||||
- Sets HTTP renderer configuration in server_options
|
||||
|
||||
|
||||
## 1.22.0
|
||||
|
||||
Released 2015-01-13
|
||||
|
||||
New features:
|
||||
- Health check endpoint
|
||||
|
||||
|
||||
## 1.21.2
|
||||
|
||||
Released 2014-12-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.32.4
|
||||
|
||||
|
||||
## 1.21.1
|
||||
|
||||
Released 2014-12-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.32.2
|
||||
|
||||
Bugfixes:
|
||||
- Closes fd for log files on `kill -HUP` (#230)
|
||||
|
||||
|
||||
|
||||
## 1.21.0
|
||||
|
||||
Released 2014-10-24
|
||||
|
||||
New features:
|
||||
- Allow a different cache-control max-age for layergroup responses
|
||||
|
||||
|
||||
## 1.20.2
|
||||
|
||||
Released 2014-10-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.31.0
|
||||
|
||||
|
||||
## 1.20.1
|
||||
|
||||
Released 2014-10-17
|
||||
|
||||
Announcements:
|
||||
- Upgrades redis-mpool to 0.3.0
|
||||
|
||||
|
||||
## 1.20.0
|
||||
|
||||
Released 2014-10-15
|
||||
|
||||
New features:
|
||||
- Report to statsd the status of redis pools
|
||||
- Upgrades Windshaft to start reporting redis/renderers/mapnik pool metrics
|
||||
|
||||
Enhancements:
|
||||
- Share one redis-mpool across the application
|
||||
|
||||
|
||||
## 1.19.0
|
||||
|
||||
Released 2014-10-14
|
||||
|
||||
Announcements:
|
||||
- Dropping support for npm <1.2.1
|
||||
npm-shrinkwrap.json is incompatible when generated with npm >=1.2.1 and consumed by npm <1.2.1
|
||||
- Upgrades windshaft to 0.28.2
|
||||
- Generates npm-shrinkwrap.json with npm >1.2.0
|
||||
|
||||
|
||||
## 1.18.2
|
||||
|
||||
Released 2014-10-13
|
||||
|
||||
Bug fixes:
|
||||
- Defaults resultSet to object if undefined in QueryTablesApi
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.28.1
|
||||
|
||||
|
||||
## 1.18.1
|
||||
|
||||
Released 2014-10-13
|
||||
|
||||
New features:
|
||||
- Allow to add more node.js' threadpool workers via process.env.UV_THREADPOOL_SIZE
|
||||
|
||||
|
||||
## 1.18.0
|
||||
|
||||
Released 2014-10-03
|
||||
|
||||
Announcements:
|
||||
- Comes back to use mapnik 2.3.x based on cartodb/node-mapnik@1.4.15-cdb from windshaft@0.28.0
|
||||
|
||||
|
||||
## 1.17.2
|
||||
|
||||
Released 2014-10-01
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.27.2 which downgrades node-mapnik to 0.7.26-cdb1
|
||||
|
||||
|
||||
## 1.17.1
|
||||
|
||||
Released 2014-09-30
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.27.1 which downgrades node-mapnik to 1.4.10
|
||||
|
||||
Enhancements:
|
||||
- TTL for template locks so they are not kept forever
|
||||
- Upgrades mocha
|
||||
|
||||
|
||||
## 1.17.0
|
||||
|
||||
Released 2014-09-25
|
||||
|
||||
New features:
|
||||
- Starts using mapnik 2.3.x
|
||||
|
||||
Enhancements:
|
||||
- Upgrades windshaft and cartodb-redis
|
||||
- Supports `!scale_denominator!` dynamic param in SQL queries
|
||||
- Metrics revamp: removes and adds some metrics
|
||||
- Adds poolSize configuration for mapnik
|
||||
|
||||
## 1.16.1
|
||||
|
||||
Released 2014-08-19
|
||||
|
||||
Enhancements:
|
||||
- Upgrades cartodb-redis
|
||||
|
||||
## 1.16.0
|
||||
|
||||
Released 2014-08-18
|
||||
|
||||
New features:
|
||||
- Configurable QueryTablesAPI to call directly postgresql using cartodb-psql
|
||||
or to keep using a request to the SQL API
|
||||
|
||||
Enhancements:
|
||||
- Removes mapnik dependency as it now relies on Windshaft to check mapnik version
|
||||
- Upgrades dependencies:
|
||||
- underscore
|
||||
- lzma
|
||||
- log4js
|
||||
- rollbar
|
||||
- windshaft
|
||||
- request
|
||||
|
||||
## 1.15.0
|
||||
|
||||
Released 2014-08-13
|
||||
Enhancements:
|
||||
- Upgrades dependencies:
|
||||
- redis-mpool
|
||||
- cartodb-redis
|
||||
- windshaft
|
||||
- Specifies name in the redis pool
|
||||
- Slow pool configuration in example configurations
|
||||
|
||||
|
||||
## 1.14.0
|
||||
|
||||
Released 2014-08-07
|
||||
|
||||
Enhancements:
|
||||
- SQL API requests moved to its own entity
|
||||
|
||||
New features:
|
||||
- Affected tables and last updated time for a query are performed in a single
|
||||
request to the SQL API
|
||||
- Allow specifying the tile format, upgrades windshaft and grainstore
|
||||
dependencies for this matter
|
||||
|
||||
|
||||
## 1.13.1
|
||||
|
||||
Released 2014-08-04
|
||||
|
||||
Enhancements:
|
||||
- Profiler header sent as JSON string
|
||||
|
||||
|
||||
## 1.13.0
|
||||
|
||||
Released 2014-07-30
|
||||
|
||||
New features:
|
||||
- Support for postgresql schemas
|
||||
- Use public user from redis
|
||||
- Support for several auth tokens
|
||||
|
||||
## 1.12.1
|
||||
|
||||
Released 2014-06-24
|
||||
|
||||
Enhancements:
|
||||
- Caches layergroup and sets X-Cache-Channel in GET requests also in named maps
|
||||
|
||||
## 1.12.0
|
||||
|
||||
Released 2014-06-24
|
||||
|
||||
New features:
|
||||
- Caches layergroup and sets X-Cache-Channel in GET requests
|
||||
|
||||
## 1.11.1
|
||||
|
||||
Released 2014-05-07
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Upgrade Windshaft to 0.21.0, see
|
||||
http://github.com/CartoDB/Windshaft/blob/0.21.0/NEWS
|
||||
|
||||
## 1.11.0
|
||||
|
||||
Released 2014-04-28
|
||||
|
||||
New features:
|
||||
|
||||
- Add support for log_filename directive
|
||||
- Reopen log file on SIGHUP, for better logrotate integration
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Set default PostgreSQL application name to "cartodb_tiler"
|
||||
|
||||
## 1.10.2
|
||||
|
||||
Released 2014-04-08
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Fix show_style tool broken since 1.8.1
|
||||
- Fix X-Cache-Channel of tiles accessed via signed token (#188)
|
||||
|
||||
## 1.10.1
|
||||
|
||||
Released 2014-03-21
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Do not cache non-success jsonp responses (#186)
|
||||
|
||||
## 1.10.0
|
||||
|
||||
Released 2014-03-20
|
||||
|
||||
New features:
|
||||
|
||||
- Add optional support for rollbar (#150)
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Do not send connection details to client (#183)
|
||||
- Upgrade node-varnish to 0.3.0
|
||||
- Upgrade Windshaft to 0.20.0, see
|
||||
http://github.com/CartoDB/Windshaft/blob/0.20.0/NEWS
|
||||
- Include tiler version in startup log
|
||||
- Install an uncaught exception handler
|
||||
- Require own fork of node-mapnik, with temptative fix
|
||||
for libxml usage (glibc detected corruptions)
|
||||
|
||||
Other changes:
|
||||
|
||||
- Switch to 3-clause BSD license (#184)
|
||||
|
||||
## 1.9.0
|
||||
|
||||
Released 2014-03-10
|
||||
|
||||
New features:
|
||||
|
||||
- Allow to set server related configuration in serverMetadata (#182)
|
||||
|
||||
## 1.8.5
|
||||
|
||||
Released 2014-03-10
|
||||
1.8.5 -- 2014-03-10
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
@@ -560,9 +17,8 @@ Bug fixes:
|
||||
|
||||
- Do not cache map creation responses (#176)
|
||||
|
||||
## 1.8.4
|
||||
|
||||
Released 2014-03-03
|
||||
1.8.4 -- 2014-03-03
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
@@ -579,9 +35,8 @@ Bug fixes:
|
||||
|
||||
- Fix database connection settings on template instanciation (#174)
|
||||
|
||||
## 1.8.3
|
||||
|
||||
Released 2014-02-27
|
||||
1.8.3 -- 2014-02-27
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
@@ -594,9 +49,8 @@ Enhancements:
|
||||
[ new sqlapi.timeout directive, defaults to 100 ms ]
|
||||
- Do not query CDB_TableMetadata for queries affected by no tables (#168)
|
||||
|
||||
## 1.8.2
|
||||
|
||||
Released 2014-02-25
|
||||
1.8.2 -- 2014-02-25
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
@@ -610,9 +64,8 @@ Bug fixes:
|
||||
|
||||
* Fix munin plugin after log format changes (#154)
|
||||
|
||||
## 1.8.1
|
||||
|
||||
Released 2014-02-19
|
||||
1.8.1 -- 2014-02-19
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
@@ -622,9 +75,8 @@ Bug fixes:
|
||||
|
||||
* Always generate X-Cache-Channel for token-based tile responses (#152)
|
||||
|
||||
## 1.8.0
|
||||
|
||||
Released 2014-02-18
|
||||
1.8.0 -- 2014-02-18
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
@@ -639,18 +91,16 @@ Enhancements:
|
||||
* Allow limiting number of templates for each user (#136)
|
||||
* Allow configuring TTL of mapConfigs via "mapConfigTTL"
|
||||
|
||||
## 1.7.1
|
||||
|
||||
Released 2014-02-11
|
||||
1.7.1 -- 2014-02-11
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Disable debug logging unless "debug" config param evaluates to true (#137)
|
||||
* Require windshaft 0.17.2 for further reducing log noise (#137)
|
||||
|
||||
## 1.7.0
|
||||
|
||||
Released 2014-02-11
|
||||
1.7.0 -- 2014-02-11
|
||||
-------------------
|
||||
|
||||
New features:
|
||||
|
||||
@@ -671,9 +121,8 @@ Bug fixes:
|
||||
* Allow passing numbers as values for numeric template variables (#130)
|
||||
|
||||
|
||||
## 1.6.3
|
||||
|
||||
Released 2014-01-30
|
||||
1.6.3 -- 2014-01-30
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
@@ -688,9 +137,8 @@ Enhancements:
|
||||
* Stop processing XML on renderer creation, not needed anymore since 1.6.1
|
||||
introduced on-demand XML generation.
|
||||
|
||||
## 1.6.2
|
||||
|
||||
Released 2014-01-23
|
||||
1.6.2 -- 2014-01-23
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
@@ -702,18 +150,16 @@ Enhancements:
|
||||
print XML style now it is not in redis anymore (#110)
|
||||
* Support CORS in template instanciation endpoint (#113)
|
||||
|
||||
## 1.6.1
|
||||
|
||||
Released 2014-01-15
|
||||
1.6.1 -- 2014-01-15
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Drop cache headers from error responses (#107)
|
||||
* Localize external CartoCSS resources at renderer creation time (#108)
|
||||
|
||||
## 1.6.0
|
||||
|
||||
Released 2014-01-10
|
||||
1.6.0 -- 2014-01-10
|
||||
-------------------
|
||||
|
||||
New features:
|
||||
|
||||
@@ -726,27 +172,24 @@ Other changes:
|
||||
* Update cartodb-redis dependency to "~0.3.0"
|
||||
* Update redis-server dependency to "2.4.0+"
|
||||
|
||||
## 1.5.2
|
||||
|
||||
Released 2013-12-05
|
||||
1.5.2 -- 2013-12-05
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix configuration-level compatibility with versions prior to 1.5 (#96)
|
||||
* Fix use of old layergroups on mapnik upgrade (#97)
|
||||
|
||||
## 1.5.1
|
||||
|
||||
Released 2013-11-28
|
||||
1.5.1 -- 2013-11-28
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Survive presence of malformed CartoCSS in redis (#94)
|
||||
* Accept useless point-transform:scale directives (#93)
|
||||
|
||||
## 1.5.0
|
||||
|
||||
Released 2013-11-19
|
||||
1.5.0 -- 2013-11-19
|
||||
-------------------
|
||||
|
||||
NOTE: new configuration directives `postgres_auth_pass` and
|
||||
`postgres.password` added; see config/environments/*.example
|
||||
@@ -770,28 +213,24 @@ Other changes:
|
||||
* CartoDB redis interaction delegated to "cartodb-redis" module
|
||||
|
||||
|
||||
## 1.4.1
|
||||
|
||||
Released 2013-11-08
|
||||
1.4.1 -- 2013-11-08
|
||||
-------------------
|
||||
|
||||
* Fix support for exponential notation in CartoCSS filter values (#87)
|
||||
|
||||
## 1.4.0
|
||||
|
||||
Released 2013-10-31
|
||||
1.4.0 -- 2013-10-31
|
||||
-------------------
|
||||
|
||||
* Add Support for Mapnik-2.2.0 (#78)
|
||||
|
||||
## 1.3.6
|
||||
|
||||
Released 2013-10-11
|
||||
1.3.6 -- 2013-10-11
|
||||
-------------------
|
||||
|
||||
* Restore support for node-0.8.9 accidentally dropped by 1.3.5
|
||||
NOTE: needs removing node_modules/windshaft and re-running npm install
|
||||
|
||||
## 1.3.5
|
||||
|
||||
Released 2013-10-03
|
||||
1.3.5 -- 2013-10-03
|
||||
-------------------
|
||||
|
||||
* Fixing apostrophes in CartoCSS
|
||||
* Fix "sql/table must contain zoom variable" error when using
|
||||
@@ -800,8 +239,8 @@ Released 2013-10-03
|
||||
* Fix error for invalid text-name in CartoCSS (#81)
|
||||
* Do not let anonymous requests use authorized renderer caches
|
||||
|
||||
## 1.3.4
|
||||
|
||||
1.3.4
|
||||
------
|
||||
|
||||
NOTE: configuration sqlapi.host renamed to sqlapi.domain
|
||||
(support for "sqlapi.host" is retained for backward compatibility)
|
||||
@@ -810,38 +249,38 @@ NOTE: configuration sqlapi.host renamed to sqlapi.domain
|
||||
* Improve invalid mapnik-geometry-type CSS error message
|
||||
* Fix race condition in localization of network resources
|
||||
|
||||
## 1.3.3
|
||||
|
||||
1.3.3
|
||||
------
|
||||
* Set Last-Modified header to allow for 304 responses
|
||||
* Add profiling support (needs useProfiler in env config file)
|
||||
* Fix double-checking for layergroups with no interactivity
|
||||
* Log full layergroup config at creation time (#76)
|
||||
|
||||
## 1.3.2
|
||||
|
||||
1.3.2
|
||||
------
|
||||
* Set default layergroup TTL to 2 hours
|
||||
* Serve multilayer tiles and grid with persistent cache control
|
||||
|
||||
## 1.3.1
|
||||
|
||||
1.3.1
|
||||
------
|
||||
* Fix deadlock on new style creation
|
||||
* Fix database authentication with multi-table layergroups
|
||||
* Add tile and grid fetching checks at layergroup creation time
|
||||
* Fix SQL error reporting to NOT split on newline
|
||||
* Fix support for CartoCSS attachments
|
||||
|
||||
## 1.3.0
|
||||
|
||||
1.3.0
|
||||
------
|
||||
* Change stats format for multilayer map token request, see
|
||||
http://github.com/Vizzuality/Windshaft-cartodb/wiki/Redis-stats-format
|
||||
|
||||
## 1.2.1
|
||||
|
||||
1.2.1
|
||||
------
|
||||
* Fix multilayer post from firefox
|
||||
* Fix multilayer cartocss layer name handling
|
||||
|
||||
## 1.2.0
|
||||
|
||||
1.2.0
|
||||
------
|
||||
* Multilayer API changes
|
||||
* Layers passed by index in grid fetching url
|
||||
* Interactivity only specified in layergroup config
|
||||
@@ -849,14 +288,14 @@ NOTE: configuration sqlapi.host renamed to sqlapi.domain
|
||||
* Use ISO format for last_modified timestamp
|
||||
* Expected LZMA encoding changed to base64
|
||||
|
||||
## 1.1.10
|
||||
|
||||
1.1.10
|
||||
------
|
||||
* Fix regression with default interactivity parameter (#74)
|
||||
* More verbose logging for SQL api connection errors
|
||||
* Write stats for multilayer map token request
|
||||
|
||||
## 1.1.9
|
||||
|
||||
1.1.9
|
||||
-----
|
||||
* Handle SQL API errors by requesting no Varnish cache
|
||||
* Fix X-Cache-Channel for multilayer (by token) responses
|
||||
* Add last_modified field to layergroup creation response (#72)
|
||||
@@ -865,22 +304,18 @@ NOTE: configuration sqlapi.host renamed to sqlapi.domain
|
||||
* Add support for LZMA compressed GET parameters
|
||||
* Add support for creating layergroups via GET
|
||||
|
||||
## 1.1.8
|
||||
|
||||
1.1.8
|
||||
-----
|
||||
* Require Windshaft-0.9.1, to reduce harmfulness of cache_buster param
|
||||
|
||||
## 1.1.7
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
1.1.7 (DD//MM//YY)
|
||||
-----
|
||||
* Do not let /etc/services confuse FD checker (munin plugin)
|
||||
* Multilayer support (#72)
|
||||
* Expose renderer settings in the environment config files
|
||||
|
||||
## 1.1.6
|
||||
|
||||
Released 19//02//13
|
||||
|
||||
1.1.6 (19//02//13)
|
||||
-----
|
||||
* Require windshaft 0.8.5, fixing some stability issues
|
||||
and providing cache info on request
|
||||
* Require grainstore 0.10.9, fixing an issue with multi-geom markers
|
||||
@@ -889,26 +324,20 @@ Released 19//02//13
|
||||
* Survive connection refusals from redis
|
||||
* Add maxConnection environment configuration, default to 128
|
||||
|
||||
## 1.1.5
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
1.1.5 (DD//MM//YY)
|
||||
-----
|
||||
* Fix bogus cached return of utf grid for fully contained tiles (#67)
|
||||
|
||||
## 1.1.4
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
1.1.4 (DD//MM//YY)
|
||||
-----
|
||||
* Reduce default extent to allow for consistent proj4 round-tripping
|
||||
* Enhance reset_styles script to use full configuration (#62)
|
||||
* Have reset_styles script also drop extended keys (#58)
|
||||
* Fix example postgis parameter for simplifying input geoms (#63)
|
||||
* Add row_limit to example config (#64)
|
||||
|
||||
## 1.1.3
|
||||
|
||||
Released 30//11//12
|
||||
|
||||
1.1.3 (30//11//12)
|
||||
-----
|
||||
* Fix reset_styles script to really skip extended keys
|
||||
* CartoCSS versioning
|
||||
* Mapnik-version dependent default styles
|
||||
@@ -916,20 +345,16 @@ Released 30//11//12
|
||||
* styles with conditional markers
|
||||
* scale arrow markers by 50%
|
||||
|
||||
## 1.1.2
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
1.1.2 (DD//MM//YY)
|
||||
-----
|
||||
* CartoCSS versioning
|
||||
* Fix use of "style_version" with GET (inline styles)
|
||||
* Enhance 2.0 -> 2.1 transforms:
|
||||
* styles with no semicolon
|
||||
* markers shift due to geometry clipping
|
||||
|
||||
## 1.1.1
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
1.1.1 (DD//MM//YY)
|
||||
-----
|
||||
* Add support for persistent client cache headers
|
||||
* Fix crash on unknown user (#55)
|
||||
* Add /version entry point
|
||||
@@ -938,10 +363,8 @@ Released DD//MM//YY
|
||||
* Support style_version and style_convert parameters in POST /style request
|
||||
* Support style_version in GET /:z/:x/:y request
|
||||
|
||||
## 1.1.0
|
||||
|
||||
Released (30/10/12)
|
||||
|
||||
1.1.0 (30/10/12)
|
||||
=======
|
||||
* Add /version entry point
|
||||
* CartoCSS versioning
|
||||
* Include version in GET /style response
|
||||
@@ -958,16 +381,12 @@ Released (30/10/12)
|
||||
* Replaced environment configs by .example ones
|
||||
* Fixed some issues with cluster2
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Released 03/10/12
|
||||
|
||||
1.0.0 (03/10/12)
|
||||
-----
|
||||
* Migrated to node 0.8.x.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
Released 25/09/12
|
||||
|
||||
0.9.0 (25/09/12)
|
||||
-----
|
||||
* External resources in CartoCSS
|
||||
* Added X-Cache-Channel header in all the tiler GET requests
|
||||
* Small fixes
|
||||
|
||||
100
README.md
100
README.md
@@ -1,38 +1,39 @@
|
||||
Windshaft-CartoDB
|
||||
==================
|
||||
|
||||
[](https://travis-ci.org/CartoDB/Windshaft-cartodb)
|
||||
[]
|
||||
(http://travis-ci.org/CartoDB/Windshaft-cartodb)
|
||||
|
||||
This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api.html) tiler. It extends
|
||||
[Windshaft](https://github.com/CartoDB/Windshaft) with some extra functionality and custom filters for authentication.
|
||||
This is the CartoDB map tiler. It extends Windshaft with some extra
|
||||
functionality and custom filters for authentication
|
||||
|
||||
* reads dbname from subdomain and cartodb redis for pretty tile urls
|
||||
* configures windshaft to publish `cartodb_id` as the interactivity layer
|
||||
* configures windshaft to publish ``cartodb_id`` as the interactivity layer
|
||||
* 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)
|
||||
* provides signed template maps API
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See Installing Mapnik.
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- 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)
|
||||
[core]
|
||||
- node-0.8.x+
|
||||
- PostgreSQL-8.3+
|
||||
- PostGIS-1.5.0+
|
||||
- Redis 2.4.0+ (http://www.redis.io)
|
||||
- Mapnik 2.0 or 2.1
|
||||
|
||||
- For running the testsuite
|
||||
- ImageMagick (http://www.imagemagick.org)
|
||||
[for cache control]
|
||||
- CartoDB-SQL-API 1.0.0+
|
||||
- CartoDB 0.9.5+ (for ``CDB_QueryTables``)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
[for running the testsuite]
|
||||
- Imagemagick (http://www.imagemagick.org)
|
||||
|
||||
Configure
|
||||
---------
|
||||
@@ -74,16 +75,59 @@ there may be out-of-sync records in there.
|
||||
Take a look: http://redis.io/commands
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
URLs
|
||||
----
|
||||
|
||||
The [docs directory](https://github.com/CartoDB/Windshaft-cartodb/tree/master/docs) contains different documentation
|
||||
resources, from higher level to more detailed ones:
|
||||
The [Maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md) defined the endpoints and their
|
||||
expected parameters and outputs.
|
||||
**TILES**
|
||||
|
||||
[GET] subdomain.cartodb.com/tiles/:table_name/:z/:x/:y.[png|png8|grid.json]
|
||||
|
||||
Args:
|
||||
|
||||
* sql - plain SQL arguments
|
||||
* interactivity - specify the column to use in UTFGrid
|
||||
* cache_buster - Specify an identifier for the internal tile cache.
|
||||
Requesting tiles with the same cache_buster value may
|
||||
result in being served a cached version of the tile
|
||||
(even when requesting a tile for the first time, as tiles
|
||||
can be prepared in advance)
|
||||
* cache_policy - Set to "persist" to have the server send an Cache-Control
|
||||
header requesting caching devices to keep the response
|
||||
cached as much as possible. This is best used with a
|
||||
timestamp value in cache_buster for manual control of
|
||||
updates.
|
||||
* geom_type - override the cartodb default
|
||||
* style - override the default map style with Carto
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
**STYLE**
|
||||
|
||||
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
|
||||
[GET/POST] subdomain.cartodb.com/tiles/:table_name/style
|
||||
|
||||
Args:
|
||||
|
||||
* style - the style in CartoCSS you want to set
|
||||
* style_version - the version of the style for POST
|
||||
* style_convert - request conversion to target version (both POST and GET)
|
||||
|
||||
|
||||
**INFOWINDOW**
|
||||
|
||||
[GET] subdomain.cartodb.com/tiles/:table_name/infowindow
|
||||
|
||||
Args:
|
||||
|
||||
* infowindow - returns contents of infowindow from CartoDB.
|
||||
|
||||
|
||||
**MAP METADATA**
|
||||
|
||||
[GET] subdomain.cartodb.com/tiles/:table_name/map_metadata
|
||||
|
||||
Args:
|
||||
|
||||
* infowindow - returns contents of infowindow from CartoDB.
|
||||
|
||||
|
||||
All GET requests are wrappable with JSONP using callback argument,
|
||||
including the UTFGrid map tile call.
|
||||
|
||||
99
app.js
99
app.js
@@ -7,21 +7,12 @@
|
||||
* environments: [development, production]
|
||||
*/
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var _ = require('underscore');
|
||||
|
||||
var ENV;
|
||||
if ( process.argv[2] ) {
|
||||
ENV = process.argv[2];
|
||||
} else if ( process.env.NODE_ENV ) {
|
||||
ENV = process.env.NODE_ENV;
|
||||
} else {
|
||||
ENV = 'development';
|
||||
}
|
||||
if ( process.argv[2] ) ENV = process.argv[2];
|
||||
else if ( process.env['NODE_ENV'] ) ENV = process.env['NODE_ENV'];
|
||||
else ENV = 'development';
|
||||
|
||||
process.env.NODE_ENV = ENV;
|
||||
process.env['NODE_ENV'] = ENV;
|
||||
|
||||
// sanity check
|
||||
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
|
||||
@@ -30,59 +21,29 @@ if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
;
|
||||
// set environment specific variables
|
||||
global.settings = require(__dirname + '/config/settings');
|
||||
global.environment = require(__dirname + '/config/environments/' + ENV);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
_.extend(global.settings, global.environment);
|
||||
|
||||
global.log4js = require('log4js');
|
||||
var log4js_config = {
|
||||
appenders: [],
|
||||
replaceConsole:true
|
||||
};
|
||||
|
||||
if (global.environment.uv_threadpool_size) {
|
||||
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
|
||||
}
|
||||
|
||||
if ( global.environment.log_filename ) {
|
||||
var logdir = path.dirname(global.environment.log_filename);
|
||||
// See cwd inlog4js.configure call below
|
||||
logdir = path.resolve(__dirname, logdir);
|
||||
if ( ! fs.existsSync(logdir) ) {
|
||||
console.error("Log filename directory does not exist: " + logdir);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Logs will be written to " + global.environment.log_filename);
|
||||
log4js_config.appenders.push(
|
||||
{ type: "file", filename: global.environment.log_filename }
|
||||
);
|
||||
} else {
|
||||
log4js_config.appenders.push(
|
||||
global.log4js = require('log4js')
|
||||
log4js.configure({
|
||||
appenders: [
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
}
|
||||
],
|
||||
replaceConsole:true
|
||||
});
|
||||
|
||||
global.log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft' }),
|
||||
redisPool = new RedisPool(redisOpts);
|
||||
|
||||
// Include cartodb_windshaft only _after_ the "global" variable is set
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
|
||||
var cartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
|
||||
serverOptions = require('./lib/cartodb/server_options')(redisPool);
|
||||
var CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft');
|
||||
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);
|
||||
});
|
||||
}
|
||||
ws = CartodbWindshaft(serverOptions);
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good number if you have up to 1024 filedescriptors
|
||||
@@ -92,23 +53,19 @@ ws.maxConnections = global.environment.maxConnections || 128;
|
||||
|
||||
ws.listen(global.environment.port, global.environment.host);
|
||||
|
||||
var version = require("./package").version;
|
||||
|
||||
ws.on('listening', function() {
|
||||
console.log(
|
||||
"Windshaft tileserver %s started on %s:%s (%s)",
|
||||
version, global.environment.host, global.environment.port, ENV
|
||||
);
|
||||
console.log("Windshaft tileserver started on "
|
||||
+ global.environment.host + ':' + global.environment.port
|
||||
+ " (" + ENV + ")");
|
||||
});
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.logger = global.log4js.getLogger();
|
||||
console.log('Log files reloaded');
|
||||
});
|
||||
// DEPRECATED, use SIGUSR2
|
||||
process.on('SIGUSR1', function() {
|
||||
console.log('WARNING: handling of SIGUSR1 by Windshaft-CartoDB is deprecated, please send SIGUSR2 instead');
|
||||
ws.dumpCacheStats();
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
global.logger.error('Uncaught exception: ' + err.stack);
|
||||
process.on('SIGUSR2', function() {
|
||||
ws.dumpCacheStats();
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -2,7 +2,6 @@ var config = {
|
||||
environment: 'development'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.localhost'
|
||||
@@ -14,11 +13,13 @@ var config = {
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
@@ -30,15 +31,11 @@ var config = {
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
// idle socket timeout, in miliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: undefined
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
|
||||
@@ -61,19 +58,9 @@ var config = {
|
||||
*/
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
@@ -84,63 +71,8 @@ var config = {
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
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
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -152,65 +84,44 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 2 pools involved in serving
|
||||
// There are currently 3 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 2 to know how many possible connections will be
|
||||
// by 3 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 1, // idle time before dropping connection
|
||||
reapIntervalMillis: 1, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
reapIntervalMillis: 1 // time between cleanups
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'http',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
host: '127.0.0.1',
|
||||
port: 8080,
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'localhost.lan',
|
||||
version: 'v1',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
port: 6082,
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
ttl: 86400
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,7 +2,6 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.cartodb\\.com$'
|
||||
@@ -14,11 +13,13 @@ var config = {
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
@@ -30,15 +31,11 @@ var config = {
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
// idle socket timeout, in miliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'cartodb_user_<%= user_id %>'
|
||||
@@ -54,20 +51,10 @@ var config = {
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
@@ -78,63 +65,8 @@ var config = {
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
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
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -146,71 +78,44 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 2 pools involved in serving
|
||||
// There are currently 3 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 2 to know how many possible connections will be
|
||||
// by 3 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
reapIntervalMillis: 1000 // time between cleanups
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'https',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
//host: '127.0.0.1',
|
||||
port: 8080,
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'cartodb.com',
|
||||
version: 'v2',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
port: 6082,
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
ttl: 86400
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:false
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'api.cartocdn.com',
|
||||
https: 'cartocdn.global.ssl.fastly.net'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,7 +2,6 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.cartodb\\.com$'
|
||||
@@ -14,11 +13,13 @@ var config = {
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/maps/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
,base_url_templated: '(?:/api/v1/maps/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/maps" is the the new API,
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
,base_url_detached: '(?:/api/v1/maps|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
@@ -30,15 +31,11 @@ var config = {
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
// idle socket timeout, in miliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>'
|
||||
@@ -55,19 +52,9 @@ var config = {
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
@@ -78,63 +65,8 @@ var config = {
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
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
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -146,71 +78,44 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 2 pools involved in serving
|
||||
// There are currently 3 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 2 to know how many possible connections will be
|
||||
// by 3 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
reapIntervalMillis: 1000 // time between cleanups
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'https',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
//host: '127.0.0.1',
|
||||
port: 8080,
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'cartodb.com',
|
||||
version: 'v2',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
port: 6082,
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
ttl: 86400
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'api.cartocdn.com',
|
||||
https: 'cartocdn.global.ssl.fastly.net'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,7 +2,6 @@ var config = {
|
||||
environment: 'test'
|
||||
,port: 8888
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '(.*)'
|
||||
@@ -14,11 +13,13 @@ var config = {
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
@@ -30,15 +31,11 @@ var config = {
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
// idle socket timeout, in miliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
//,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
|
||||
@@ -48,26 +45,16 @@ var config = {
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "test_windshaft_publicuser",
|
||||
user: "testpublicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: ''
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
@@ -78,65 +65,8 @@ var config = {
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
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
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png',
|
||||
// for testing purposes
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -148,65 +78,46 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 2 pools involved in serving
|
||||
// There are currently 3 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 2 to know how many possible connections will be
|
||||
// by 3 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 1, // idle time before dropping connection
|
||||
reapIntervalMillis: 1, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
reapIntervalMillis: 1 // time between cleanups
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'http',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
host: '127.0.0.1',
|
||||
port: 1080,
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'donot_look_this_up',
|
||||
// This port will be used by "make check" for testing purposes
|
||||
// It must be available
|
||||
version: 'v1',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: '',
|
||||
port: null, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
port: null,
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
ttl: 86400
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
1
config/settings.js
Normal file
1
config/settings.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports.oneDay = 86400000;
|
||||
5
configure
vendored
5
configure
vendored
@@ -56,8 +56,9 @@ while test -n "$1"; do
|
||||
ENVIRONMENT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
*)
|
||||
echo "Unused option '$1'" >&2
|
||||
;;
|
||||
echo "Unknown option '$1'" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
@@ -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 }
|
||||
|
||||
897
docs/Map-API.md
897
docs/Map-API.md
@@ -1,862 +1,111 @@
|
||||
## Maps API
|
||||
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and you can apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
|
||||
You can create two types of maps with the Maps API:
|
||||
|
||||
- **Anonymous maps**
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example]({{ '/cartodb-platform/cartodb-js.html' | prepend: site.baseurl }}).
|
||||
|
||||
- **Named maps**
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
|
||||
## Quickstart
|
||||
# Kind of maps
|
||||
|
||||
### Anonymous maps
|
||||
Windshaft-CartoDB supports these kind of maps:
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
- [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)
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
## Temporary maps
|
||||
|
||||
$.ajax({
|
||||
crossOrigin: true,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'https://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
```
|
||||
Temporary maps have no owners and are anonymous in nature.
|
||||
There are two kind of temporary maps:
|
||||
|
||||
### Named maps
|
||||
- Detached maps (aka MultiLayer-API)
|
||||
- Inline maps
|
||||
|
||||
Let's create a named map using some private tables in a CartoDB account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
### Detached maps
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"layergroup": {
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
Detached maps are maps which are configured with a request
|
||||
obtaining a temporary token and then used by referencing
|
||||
the obtained token. The token expires automatically when unused.
|
||||
|
||||
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type ``man curl`` in bash. Using `curl`, and storing the config from above in a file `mapconfig.json`, the call would look like:
|
||||
Anyone can create detached maps, but users will need read access
|
||||
to the data source of the map layers.
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
The configuration format is a [MapConfig]
|
||||
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) document.
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
The HTTP endpoints for creating the map and using it are described [here]
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/MultiLayer-API)
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl -X POST 'https://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
*TODO* cleanup the referenced document
|
||||
|
||||
The response will return JSON with properties for the `layergroupid` and the timestamp (`last_updated`) of the last data modification.
|
||||
### Inline maps
|
||||
|
||||
Here is an example response:
|
||||
Inline maps are maps that only exist for a single request,
|
||||
being the request for a specific map resource (tile).
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
Inline maps only support PNG and UTF8GRID tiles.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
The configuration consist in a set of parameters, to be
|
||||
specified in the query string of the tile request:
|
||||
|
||||
## General Concepts
|
||||
* 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,
|
||||
|
||||
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
|
||||
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][].
|
||||
|
||||
### Auth
|
||||
*TODO* specify (or link) api endpoints
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a named map).
|
||||
## Persistent maps
|
||||
|
||||
To execute an authorized request, `api_key=YOURAPIKEY` should be added to the request URL. The param can be also passed as POST param. Using HTTPS is mandatory when you are performing requests that include your `api_key`.
|
||||
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:
|
||||
|
||||
### Errors
|
||||
- Template maps
|
||||
- Table maps (legacy, deprecated)
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
### Templated maps
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
```
|
||||
Templated maps are templated [MapConfig]
|
||||
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) documents
|
||||
associated with an authorization certificate.
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
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.
|
||||
|
||||
### CORS support
|
||||
The HTTP endpoints for creating and using templated maps are described [here]
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps).
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
*TODO* cleanup the referenced document
|
||||
|
||||
## Anonymous Maps
|
||||
### Table maps
|
||||
|
||||
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
Table maps are maps associated with a table.
|
||||
Configuration of such maps is limited to the CartoCSS style.
|
||||
|
||||
### Instantiate
|
||||
* style - the CartoCSS style for the datasource, can be an array
|
||||
* style_version - version of the CartoCSS style, can be an array
|
||||
|
||||
#### Definition
|
||||
You can only fetch PNG or UTF8GRID tiles from these maps.
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
Access method is the same as the one for [Inline maps](#inline-maps)
|
||||
|
||||
#### Params
|
||||
# Endpoints description
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e",
|
||||
"interactivity": ["cartodb_id", "iso3"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
- **/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)
|
||||
|
||||
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.19.1/doc/MapConfig-1.1.0.md).
|
||||
|
||||
#### Response
|
||||
NOTE: in case Multilayer-API does not contain this info yet, the
|
||||
endpoint for fetching attributes is this:
|
||||
|
||||
The response includes:
|
||||
- **/api/maps/:map_id/:layer_index/attributes/:feature_id**
|
||||
- would return { c: 1, d: 2 }
|
||||
|
||||
- **layergroupid**
|
||||
The ID for that map, used to compose the URL for the tiles. The final URL is:
|
||||
|
||||
```html
|
||||
https://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- **updated_at**
|
||||
The ISO date of the last time the data involved in the query was updated.
|
||||
|
||||
- **metadata** *(optional)*
|
||||
Includes information about the layers. Some layers may not have metadata.
|
||||
|
||||
- **cdn_url**
|
||||
URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The tiles can be accessed using:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
For attributes defined in `attributes` section:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
```javascript
|
||||
{ c: 1, d: 2 }
|
||||
```
|
||||
|
||||
Notice UTF Grid and attributes endpoints need an integer parameter, ``layer``. That number is the 0-based index of the layer inside the mapconfig. In this case, 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example mapconfig. If a second layer was available it could be returned with 1, a third layer with 2, etc.
|
||||
|
||||
### Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **config**
|
||||
Encoded JSON with the params for creating named maps (the variables defined in the template).
|
||||
|
||||
- **lmza**
|
||||
This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
|
||||
|
||||
- **callback**
|
||||
JSON callback name.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl "https://documentation.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
|
||||
## Named Maps
|
||||
|
||||
Named maps are essentially the same as anonymous maps except the mapconfig is stored on the server and the map is given a unique name. Two other big differences are: you can create named maps from private data and that users without an API Key can see them even though they are from that private data.
|
||||
|
||||
The main two differences compared to anonymous maps are:
|
||||
|
||||
- **auth layer**
|
||||
This allows you to control who is able to see the map based on a token auth
|
||||
|
||||
- **templates**
|
||||
Since the mapconfig is static it can contain some variables so the client can modify the map's appearance using those variables.
|
||||
|
||||
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
|
||||
|
||||
### Create
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
<div class="code-title">template.json</div>
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
"auth": {
|
||||
"method": "token",
|
||||
"valid_tokens": [
|
||||
"auth_token1",
|
||||
"auth_token2"
|
||||
]
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
},
|
||||
"cartodb_id": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"layergroup": {
|
||||
"version": "1.0.1",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter, and only contain letters, numbers, or underscores (_).
|
||||
- **auth**:
|
||||
- **method** `"token"` or `"open"` (the default if no `"method"` is given).
|
||||
- **valid_tokens** when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
|
||||
- **placeholders**: Variables not listed here are not substituted. Variables not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
|
||||
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.1.0.md) for more info.
|
||||
|
||||
#### Template Format
|
||||
|
||||
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced.
|
||||
|
||||
##### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
|
||||
|
||||
#### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
- **sql_literal** internal single-quotes will be sql-escaped
|
||||
- **sql_ident** internal double-quotes will be sql-escaped
|
||||
- **number** can only contain numerical representation
|
||||
- **css_color** can only contain color names or hex-values
|
||||
|
||||
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with new options provided.
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```html
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id":"name",
|
||||
}
|
||||
```
|
||||
|
||||
### Instantiate
|
||||
|
||||
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
|
||||
```javascript
|
||||
// params.json
|
||||
{
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400)
|
||||
|
||||
- **auth_token** *optional* if the named map needs auth
|
||||
|
||||
#### Example
|
||||
|
||||
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
|
||||
|
||||
Valid credentials will be needed if required by the template.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
<div class="code-title">Response</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">Error</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However you'll need to show the `auth_token`, if required by the template.
|
||||
|
||||
### Using JSONP
|
||||
|
||||
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
- **config** Encoded JSON with the params for creating named maps (the variables defined in the template)
|
||||
- **lmza** This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
|
||||
- **callback:** JSON callback name
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
The response is in this format:
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
PUT /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
#### Other Info
|
||||
|
||||
Updating a named map removes all the named map instances so they need to be initialized again.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"error": "error string here"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
Delete the specified template map from the server and it disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
DELETE /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request">REQUEST</div>
|
||||
```bash
|
||||
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response with an error will be returned.
|
||||
|
||||
### Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
### Getting a Specific Template
|
||||
|
||||
This gets the definition of a template.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
### Use with CartoDB.js
|
||||
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
|
||||
|
||||
```js
|
||||
var layerSource = {
|
||||
user_name: '{your_user_name}',
|
||||
type: 'namedmap',
|
||||
named_map: {
|
||||
name: '{template_name}',
|
||||
layers: [{
|
||||
layer_name: "layer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
cartodb.createLayer('map_dom_id',layerSource)
|
||||
.addTo(map_object);
|
||||
|
||||
```
|
||||
|
||||
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) has methods for accessing your named maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
##Static Maps API
|
||||
|
||||
The Static Maps API can be initiated using both named and anonymous maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
|
||||
|
||||
### Maps API endpoints
|
||||
|
||||
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupsid token` calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:z**: the zoom level of the map
|
||||
* **:lat**: the latitude for the center of the map
|
||||
* **:lng**: the longitude for the center of the map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
#### Bounding Box
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:bbox/:width/:height.:format`
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:bbox**: the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
- LowerCorner longitude, in decimal degrees (aka most western)
|
||||
- LowerCorner latitude, in decimal degrees (aka most southern)
|
||||
- UpperCorner longitude, in decimal degrees (aka most eastern)
|
||||
- UpperCorner latitude, in decimal degrees (aka most northern)
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
Note: you can see this endpoint as:
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`
|
||||
```
|
||||
|
||||
####Layers
|
||||
|
||||
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
|
||||
|
||||
**Basemaps**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
By manipulating the `"urlTemplate"` custom basemaps can be used in generating static images. Supported map types for the Static Maps API are:
|
||||
|
||||
'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||
|
||||
**Mapnik**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
Additoinally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
####Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
####Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/4b615ff367e498e770e7d05e99181873:1420231989550.8699/14/40.71502926732618/-73.96039009094238/600/400.png
|
||||
```
|
||||
|
||||
####Response
|
||||
<div clas="wrap"><p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>,</div>
|
||||
|
||||
####MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from residential_zoning_2009",
|
||||
"cartocss": "/** simple visualization */\n\n#residential_zoning_2009{\n polygon-fill: #c7eae5;\n polygon-opacity: 1;\n line-color: #FFF;\n line-width: 0.2;\n line-opacity: 0.5;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from nycha_developments_july2011",
|
||||
"cartocss": "/** simple visualization */\n\n#nycha_developments_july2011{\n polygon-fill: #ef3b2c;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.3.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.3.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension introduces a new layer type so it's possible to use a named map by its name as a layer.
|
||||
|
||||
## 2.1 Named layers definition
|
||||
|
||||
```javascript
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `named` is the only supported value
|
||||
type: "named",
|
||||
|
||||
// REQUIRED
|
||||
// object, set `named` map layers configuration
|
||||
options: {
|
||||
|
||||
// REQUIRED
|
||||
// string, the name for the named map to use
|
||||
name: "world_borders",
|
||||
|
||||
// OPTIONAL
|
||||
// object, the replacement values for the named map's template placeholders
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#instantiate-1 for more details
|
||||
config: {
|
||||
"color": "#000"
|
||||
},
|
||||
|
||||
// OPTIONAL
|
||||
// string array, the authorized tokens in case the named map has auth method set to `token`
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#named-maps-1 for more details
|
||||
auth_tokens: [
|
||||
"token1",
|
||||
"token2"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.2 Limitations
|
||||
|
||||
1. A Named Map will not allow to have `named` type layers inside their templates layergroup's layers definition.
|
||||
2. A `named` layer does not allow Named Maps form other accounts, it's only possible to use Named Maps from the very
|
||||
same user account.
|
||||
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version
|
||||
@@ -1,6 +1,6 @@
|
||||
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/Vizzuality/Windshaft/wiki/Multilayer-API) in a few ways.
|
||||
|
||||
## Last modification timestamp embedded in the token
|
||||
## Last modification timestamps
|
||||
|
||||
It encodes a timestamp of 'last modification time' into the map token (token:EPOCH) returned to the client.
|
||||
It accepts tokens with encoded timestamp from the client considering the token suffix as a cache_buster value.
|
||||
@@ -8,21 +8,8 @@ It accepts tokens with encoded timestamp from the client considering the token s
|
||||
Clients don't need to be aware of the extension but rather use the API as they would use the base one.
|
||||
The only difference will be that the _same_ layergroup configuration may result in different tokens if source data was modified between the mapview requests.
|
||||
|
||||
## Additional attributes in the response object
|
||||
|
||||
Windshaft-CartoDB adds the following attributes in the response object
|
||||
|
||||
- ``last_update`` field with ISO format (2013-11-30T12:23:10).
|
||||
- ``cdn_url`` object containing CDN url client should use (not mandatory) to access the tiles. It's in the form:
|
||||
|
||||
```json
|
||||
{
|
||||
http: 'http://cdn_url.com/'
|
||||
https: 'https://secure.cdn_url.com/'
|
||||
}
|
||||
```
|
||||
|
||||
Also Windshaft-CartoDB adds a ``last_update`` field with ISO format (2013-11-30T12:23:10).
|
||||
|
||||
## Stats tag
|
||||
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](https://github.com/CartoDB/Windshaft-cartodb/wiki/Redis-stats-format) gathering.
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](Redis-stats-format) gathering.
|
||||
101
docs/Routes.md
101
docs/Routes.md
@@ -1,101 +0,0 @@
|
||||
This document list all routes available in Windshaft-cartodb Maps API server.
|
||||
|
||||
## Routes list
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y@:scale_factor?x.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:scale_factor(t),:format(f)} (1)`
|
||||
<br/>Notes: Mapnik retina tiles [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:format(f)} (1)`
|
||||
<br/>Notes: Mapnik tiles [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/:z/:x/:y.(:format) {:user(f),:token(f),:layer(f),:z(f),:x(f),:y(f),:format(f)} (1)`
|
||||
<br/>Notes: Per :layer rendering based on :format [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/attributes/:fid {:user(f),:token(f),:layer(f),:fid(f)} (1)`
|
||||
<br/>Notes: Endpoint for info windows data, alternative for sql api when tables are private [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/center/:token/:z/:lat/:lng/:width/:height.:format {:user(f),:token(f),:z(f),:lat(f),:lng(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static Maps API [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format {:user(f),:token(f),:west(f),:south(f),:east(f),:north(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static Maps API [0]
|
||||
|
||||
1. `GET / {} (1)`
|
||||
<br/>Notes: Welcome message
|
||||
|
||||
1. `GET /version {} (1)`
|
||||
<br/>Notes: Return relevant module versions: mapnik, grainstore, etc
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id/jsonp {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named maps JSONP instantiation [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named map retrieval (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: List named maps (w/ API KEY) [1]
|
||||
|
||||
1. `GET /health {} (1)`
|
||||
<br/>Notes: Healt check
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: CORS [0]
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: CORS [1]
|
||||
|
||||
1. `POST (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: Create named map (w/ API KEY) [1]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Instantiate named map [1]
|
||||
|
||||
1. `PUT (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Update a named map (w/ API KEY) [1]
|
||||
|
||||
1. `DELETE (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Delete named map (w/ API KEY) [1]
|
||||
|
||||
## Optional deprecated routes
|
||||
|
||||
- [0] `/tiles/layergroup` is deprecated and `/api/v1/map` should be used but we keep it for now.
|
||||
- [1] `/tiles/template` is deprecated and `/api/v1/map/named` should be used but we keep it for now.
|
||||
|
||||
## How to generate the list of routes
|
||||
|
||||
Something like the following patch should do the trick
|
||||
|
||||
```javascript
|
||||
diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js
|
||||
index 477a4c2..f69eebb 100644
|
||||
--- a/lib/cartodb/cartodb_windshaft.js
|
||||
+++ b/lib/cartodb/cartodb_windshaft.js
|
||||
@@ -242,6 +242,20 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
}
|
||||
});
|
||||
|
||||
+ var format = require('util').format;
|
||||
+ var routesNotes = Object.keys(ws.routes.routes)
|
||||
+ .map(function(method) { return ws.routes.routes[method]; })
|
||||
+ .reduce(function(previous, current) { current.map(function(r) { previous.push(r) }); return previous;}, [])
|
||||
+ .map(function(route) {
|
||||
+ return format("\n1. `%s %s {%s} (%d)`\n<br/>Notes: [DEPRECATED]? ",
|
||||
+ route.method.toUpperCase(),
|
||||
+ route.path,
|
||||
+ route.keys.map(function(k) { return format(':%s(%s)', k.name, k.optional ? 't' : 'f'); } ).join(','),
|
||||
+ route.callbacks.length
|
||||
+ );
|
||||
+ });
|
||||
+ console.log(routesNotes.join('\n'));
|
||||
+
|
||||
return ws;
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
@@ -5,6 +5,13 @@ layergroup configurations (instantiation).
|
||||
Template maps are persistent, can only be created and deleted by the
|
||||
CartoDB user showing a valid API_KEY.
|
||||
|
||||
Instantiating a signed template map would result in a [signed
|
||||
map](https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
|
||||
instance that would be signed with the same signature as the template.
|
||||
|
||||
Deleting a signed template results in deletion of all signatures created
|
||||
as a result of instantiation.
|
||||
|
||||
|
||||
# Template format
|
||||
|
||||
@@ -36,19 +43,22 @@ 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.
|
||||
|
||||
Additionally you'll be able to embed an authorization
|
||||
certificate that would be used to sign any instance of the template.
|
||||
|
||||
```js
|
||||
// template.json
|
||||
{
|
||||
version: "0.0.1",
|
||||
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",
|
||||
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"]
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
||||
method: 'token', // or "open" (the default if no "method" is given)
|
||||
valid_tokens: ['auth_token1','auth_token2'] // only (required and non empty) for 'token' method
|
||||
},
|
||||
// Variables not listed here are not substituted
|
||||
// Variable not provided at instantiation time trigger an error
|
||||
@@ -56,11 +66,11 @@ template by creating a default instance.
|
||||
// Type specification is used for quoting, to avoid injections
|
||||
placeholders: {
|
||||
color: {
|
||||
type:"css_color",
|
||||
default:"red"
|
||||
type:'css_color',
|
||||
default:'red'
|
||||
},
|
||||
cartodb_id: {
|
||||
type:"number",
|
||||
type:'number',
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
@@ -81,8 +91,7 @@ template by creating a default instance.
|
||||
|
||||
# Creating a templated map
|
||||
|
||||
You can create a template map with a single call (for simplicity).
|
||||
|
||||
You can create a signed template map with a single call (for simplicity).
|
||||
You'd use a POST sending JSON data:
|
||||
|
||||
```sh
|
||||
@@ -111,7 +120,10 @@ Errors are in this form:
|
||||
|
||||
# Updating an existing template
|
||||
|
||||
You can update a template map with a PUT:
|
||||
Update of a template map implies removal all signatures from previous
|
||||
map instances.
|
||||
|
||||
You can update a signed template map with a PUT:
|
||||
|
||||
```sh
|
||||
curl -X PUT \
|
||||
@@ -230,7 +242,13 @@ or, on error:
|
||||
|
||||
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.
|
||||
But you'll still have to show the ``auth_token``, if required by the template
|
||||
(see https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
|
||||
|
||||
Instances of a signed template map will be signed with the same signature
|
||||
certificate associated with the template. Such certificate would contain
|
||||
a reference to the template identifier, so that it can be revoked every
|
||||
time the template is updated or deleted.
|
||||
|
||||
### using JSONP
|
||||
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
|
||||
@@ -256,6 +274,8 @@ last_updated: "2014-01-27T17:41:03.021Z"
|
||||
```
|
||||
# Deleting a template map
|
||||
|
||||
Deletion of a template map will imply removal all instance signatures
|
||||
|
||||
You can delete a templated map with a DELETE to ``/template/:template_name``:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
Windshaft-cartodb metrics
|
||||
=========================
|
||||
See [Windshaft metrics documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/metrics.md) to understand the full picture.
|
||||
|
||||
The next list includes the API endpoints, each endpoint may have several inner timers, some of them are displayed within this list as subitems. Find the description for them in the Inner timers section.
|
||||
## Timers
|
||||
- **windshaft-cartodb.flush_cache**: time to flush the tile and sql cache
|
||||
- **windshaft-cartodb.get_template**: time to retrieve an specific template
|
||||
- **windshaft-cartodb.delete_template**: time to delete an specific template
|
||||
- **windshaft-cartodb.get_template_list**: time to retrieve the list of owned templates
|
||||
- **windshaft-cartodb.instance_template_post**: time to create a template via HTTP POST
|
||||
- **windshaft-cartodb.instance_template_get**: time to create a template via HTTP GET
|
||||
+ TemplateMaps_instance
|
||||
+ createLayergroup
|
||||
|
||||
There are some endpoints that are not being tracked:
|
||||
- Adding a template
|
||||
- Updating a template
|
||||
|
||||
### Inner timers
|
||||
Again, each inner timer may have several inner timers.
|
||||
|
||||
- **addCacheChannel**: time to add X-Cache-Channel header based on table last modifications
|
||||
- **LZMA decompress**: time to decompress request params with LZMA
|
||||
- **TemplateMaps_instance**: time to retrieve a map template instance, see *getTemplate* and *authorizedByCert*
|
||||
- **affectedTables**: time to check what are the affected tables for adding the cache channel, see *addCacheChannel*
|
||||
- **authorize**: time to authorize a request, see *authorizedByAPIKey*, *authorizedByCert*, *authorizedBySigner*
|
||||
- **authorizedByCert**: time to authorize a template instantiation
|
||||
- **findLastUpdated**: time to retrieve the last update time for a list of tables, see *affectedTables*
|
||||
- **generateCacheChannel**: time to generate the headers for the cache channel based on the request, see *addCacheChannel*
|
||||
- **getSignerMapKey**: time to retrieve from redis the authorized user for a template map
|
||||
- **getTablePrivacy**: time to retrieve from redis the privacy of a table
|
||||
- **getTemplate**: time to retrieve from redis the template for a map
|
||||
- **getUserMapKey**: time to retrieve from redis the user key for a map
|
||||
- **incMapviewCount**: time to incremenent in redis the map views
|
||||
- **mapStore_load**: time to retrieve from redis a map configuration
|
||||
- **req2params.setup**: time to prepare the params from a request, see *req2params* in Windshaft documentation
|
||||
- **setDBAuth**: time to retrieve from redis and set db user and db password from a user
|
||||
- **setDBConn**: time to retrieve from redis and set db host and db name from a user
|
||||
- **setDBParams**: time to prepare all db params to be able to connect/query a database, see *setDBAuth* and *setDBConn*
|
||||
- **tablePrivacy_getUserDBName**: time to retrieve from redis the database for a user
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
function QueryTablesApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
module.exports = QueryTablesApi;
|
||||
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, callback) {
|
||||
|
||||
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesInQueryRows, callback);
|
||||
};
|
||||
|
||||
function handleAffectedTablesInQueryRows(err, rows, callback) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var qtables = rows[0].cdb_querytables;
|
||||
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
callback(null, tableNames);
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, sql, callback) {
|
||||
|
||||
var query = [
|
||||
'WITH querytables AS (',
|
||||
'SELECT * FROM CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
|
||||
')',
|
||||
'SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max',
|
||||
'FROM CDB_TableMetadata m',
|
||||
'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
|
||||
};
|
||||
|
||||
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err || rows.length === 0) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch affected tables and last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
var result = rows[0];
|
||||
|
||||
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
callback(null, {
|
||||
affectedTables: tableNames,
|
||||
lastUpdatedTime: lastUpdatedTime * 1000
|
||||
});
|
||||
}
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1')
|
||||
;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
function TablesExtentApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = TablesExtentApi;
|
||||
|
||||
/**
|
||||
* Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on
|
||||
* the_geom_webmercator (SRID 3857) column.
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {Array} tableNames The named can be schema qualified, so this accepts both `schema_name.table_name` and
|
||||
* `table_name` format as valid input
|
||||
* @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north`
|
||||
*/
|
||||
TablesExtentApi.prototype.getBounds = function (username, tableNames, callback) {
|
||||
var estimatedExtentSQLs = tableNames.map(function(tableName) {
|
||||
var schemaTable = tableName.split('.');
|
||||
if (schemaTable.length > 1) {
|
||||
return "ST_EstimatedExtent('" + schemaTable[0] + "', '" + schemaTable[1] + "', 'the_geom_webmercator')";
|
||||
}
|
||||
return "ST_EstimatedExtent('" + schemaTable[0] + "', 'the_geom_webmercator')";
|
||||
});
|
||||
|
||||
var query = [
|
||||
"WITH ext as (" +
|
||||
"SELECT ST_Transform(ST_SetSRID(ST_Extent(ST_Union(ARRAY[",
|
||||
estimatedExtentSQLs.join(','),
|
||||
"])), 3857), 4326) geom)",
|
||||
"SELECT",
|
||||
"ST_XMin(geom) west,",
|
||||
"ST_YMin(geom) south,",
|
||||
"ST_XMax(geom) east,",
|
||||
"ST_YMax(geom) north",
|
||||
"FROM ext"
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleBoundsResult, callback);
|
||||
};
|
||||
|
||||
function handleBoundsResult(err, rows, callback) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
function PgConnection(metadataBackend) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
}
|
||||
|
||||
module.exports = PgConnection;
|
||||
|
||||
|
||||
// Set db authentication parameters to those of the given username
|
||||
//
|
||||
// @param username the cartodb username, mapped to a database username
|
||||
// via CartodbRedis metadata records
|
||||
//
|
||||
// @param params the parameters to set auth options into
|
||||
// added params are: "dbuser" and "dbpassword"
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var user_params = {};
|
||||
var auth_user = global.environment.postgres_auth_user;
|
||||
var auth_pass = global.environment.postgres_auth_pass;
|
||||
step(
|
||||
function getId() {
|
||||
self.metadataBackend.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
if (err) throw 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;
|
||||
|
||||
self.metadataBackend.getUserDBPass(username, this);
|
||||
},
|
||||
function(err, user_password) {
|
||||
if (err) throw err;
|
||||
user_params.user_password = user_password;
|
||||
if ( auth_pass ) {
|
||||
var dbpass = _.template(auth_pass, user_params);
|
||||
_.extend(params, {dbpassword:dbpass});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Set db connection parameters to those for the given username
|
||||
//
|
||||
// @param dbowner cartodb username of database owner,
|
||||
// mapped to a database username
|
||||
// via CartodbRedis metadata records
|
||||
//
|
||||
// @param params the parameters to set connection options into
|
||||
// added params are: "dbname", "dbhost"
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
var self = this;
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
step(
|
||||
function getConnectionParams() {
|
||||
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
|
||||
},
|
||||
function extendParams(err, dbParams){
|
||||
if (err) throw err;
|
||||
// we don't want null values or overwrite a non public user
|
||||
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
if ( dbParams ) _.extend(params, dbParams);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
function PgQueryRunner(pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = PgQueryRunner;
|
||||
|
||||
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
self.pgConnection.setDBConn(username, params, this);
|
||||
},
|
||||
function executeQuery(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var psql = new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
});
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
queryHandler(err, rows, callback);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
18
lib/cartodb/cache/backend/fastly.js
vendored
18
lib/cartodb/cache/backend/fastly.js
vendored
@@ -1,18 +0,0 @@
|
||||
var FastlyPurge = require('fastly-purge');
|
||||
|
||||
function FastlyCacheBackend(apiKey, serviceId, softPurge) {
|
||||
this.serviceId = serviceId;
|
||||
this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: softPurge || true });
|
||||
}
|
||||
|
||||
module.exports = FastlyCacheBackend;
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
FastlyCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
this.fastlyPurge.key(this.serviceId, cacheObject.key(), callback);
|
||||
};
|
||||
|
||||
module.exports = FastlyCacheBackend;
|
||||
32
lib/cartodb/cache/backend/varnish_http.js
vendored
32
lib/cartodb/cache/backend/varnish_http.js
vendored
@@ -1,32 +0,0 @@
|
||||
var request = require('request');
|
||||
|
||||
function VarnishHttpCacheBackend(host, port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
request(
|
||||
{
|
||||
method: 'PURGE',
|
||||
url: 'http://' + this.host + ':' + this.port + '/key',
|
||||
headers: {
|
||||
'Invalidation-Match': '\\b' + cacheObject.key() + '\\b'
|
||||
}
|
||||
},
|
||||
function(err, response) {
|
||||
if (err || response.statusCode !== 204) {
|
||||
return callback(new Error('Unable to invalidate Varnish object'));
|
||||
}
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
18
lib/cartodb/cache/model/named_maps_entry.js
vendored
18
lib/cartodb/cache/model/named_maps_entry.js
vendored
@@ -1,18 +0,0 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
function NamedMaps(owner, name) {
|
||||
this.namespace = 'n';
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
module.exports = NamedMaps;
|
||||
|
||||
|
||||
NamedMaps.prototype.key = function() {
|
||||
return this.namespace + ':' + shortHashKey(this.owner + ':' + this.name);
|
||||
};
|
||||
|
||||
function shortHashKey(target) {
|
||||
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
|
||||
}
|
||||
41
lib/cartodb/cache/surrogate_keys_cache.js
vendored
41
lib/cartodb/cache/surrogate_keys_cache.js
vendored
@@ -1,41 +0,0 @@
|
||||
var queue = require('queue-async');
|
||||
|
||||
/**
|
||||
* @param {Array|Object} cacheBackends each backend backend should respond to `invalidate(cacheObject, callback)` method
|
||||
* @constructor
|
||||
*/
|
||||
function SurrogateKeysCache(cacheBackends) {
|
||||
this.cacheBackends = Array.isArray(cacheBackends) ? cacheBackends : [cacheBackends];
|
||||
}
|
||||
|
||||
module.exports = SurrogateKeysCache;
|
||||
|
||||
|
||||
/**
|
||||
* @param response should respond to `header(key, value)` method
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
*/
|
||||
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
|
||||
response.header('Surrogate-Key', cacheObject.key());
|
||||
};
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) {
|
||||
var invalidationQueue = queue(this.cacheBackends.length);
|
||||
|
||||
this.cacheBackends.forEach(function(cacheBackend) {
|
||||
invalidationQueue.defer(function(cacheBackend, done) {
|
||||
cacheBackend.invalidate(cacheObject, done);
|
||||
}, cacheBackend);
|
||||
});
|
||||
|
||||
invalidationQueue.awaitAll(function(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
};
|
||||
26
lib/cartodb/cache_validator.js
Normal file
26
lib/cartodb/cache_validator.js
Normal file
@@ -0,0 +1,26 @@
|
||||
var _ = require('underscore'),
|
||||
Varnish = require('node-varnish'),
|
||||
varnish_queue = null;
|
||||
|
||||
function init(host, port, secret) {
|
||||
varnish_queue = new Varnish.VarnishQueue(host, port, secret);
|
||||
varnish_queue.on('error', function(e) {
|
||||
console.log("[CACHE VALIDATOR ERROR] " + e);
|
||||
});
|
||||
}
|
||||
|
||||
function invalidate_db(dbname, table) {
|
||||
var cmd = 'purge obj.http.X-Cache-Channel ~ "^' + dbname +
|
||||
':(.*'+ table +'.*)|(table)$"';
|
||||
try{
|
||||
varnish_queue.run_cmd(cmd, false);
|
||||
} catch (e) {
|
||||
console.log("[CACHE VALIDATOR ERROR] could not queue command " +
|
||||
cmd + " -- " + e);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
invalidate_db: invalidate_db
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
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 _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, Windshaft = require('windshaft')
|
||||
, redisPool = new require('redis-mpool')(global.environment.redis)
|
||||
// TODO: instanciate cartoData with redisPool
|
||||
, cartoData = require('cartodb-redis')(global.environment.redis)
|
||||
, SignedMaps = require('./signed_maps.js')
|
||||
, TemplateMaps = require('./template_maps.js')
|
||||
, Cache = require('./cache_validator')
|
||||
, os = require('os')
|
||||
;
|
||||
|
||||
var CartodbWindshaft = function(serverOptions) {
|
||||
var debug = global.environment.debug;
|
||||
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
@@ -22,12 +23,22 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
var redisPool = serverOptions.redis.pool ||
|
||||
require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
|
||||
if(serverOptions.cache_enabled) {
|
||||
console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port);
|
||||
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret);
|
||||
serverOptions.afterStateChange = function(req, data, callback) {
|
||||
Cache.invalidate_db(req.params.dbname, req.params.table);
|
||||
callback(null, data);
|
||||
}
|
||||
}
|
||||
|
||||
var cartoData = require('cartodb-redis')({pool: redisPool});
|
||||
|
||||
var templateMaps = serverOptions.templateMaps;
|
||||
serverOptions.beforeStateChange = function(req, callback) {
|
||||
var err = null;
|
||||
if ( ! req.params.hasOwnProperty('_authorizedByApiKey') ) {
|
||||
err = new Error("map state cannot be changed by unauthenticated request!");
|
||||
}
|
||||
callback(err, req);
|
||||
}
|
||||
|
||||
// This is for Templated maps
|
||||
//
|
||||
@@ -35,41 +46,11 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
//
|
||||
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)
|
||||
);
|
||||
serverOptions.signedMaps = new SignedMaps(redisPool);
|
||||
var templateMapsOpts = {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
var templateMaps = new TemplateMaps(redisPool, serverOptions.signedMaps, templateMapsOpts);
|
||||
|
||||
// boot
|
||||
var ws = new Windshaft.Server(serverOptions);
|
||||
@@ -80,36 +61,27 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
var version = wsversion();
|
||||
version.windshaft_cartodb = require('../../package.json').version;
|
||||
return version;
|
||||
};
|
||||
}
|
||||
|
||||
var ws_sendResponse = ws.sendResponse;
|
||||
// GET routes for which we don't want to request any caching.
|
||||
// POST/PUT/DELETE requests are never cached anyway.
|
||||
var noCacheGETRoutes = [
|
||||
'/',
|
||||
'/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;
|
||||
}
|
||||
if ( args.length > 2 ) statusCode = args[2];
|
||||
else {
|
||||
statusCode = args[1] || 200;
|
||||
}
|
||||
var req = res.req;
|
||||
step (
|
||||
Step (
|
||||
function addCacheChannel() {
|
||||
if ( ! req ) {
|
||||
// having no associated request can happen when
|
||||
@@ -131,11 +103,10 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
//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"));
|
||||
//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*/) {
|
||||
function sendResponse(err, added) {
|
||||
if ( err ) console.log(err + err.stack);
|
||||
ws_sendResponse.apply(that, thatArgs);
|
||||
return null;
|
||||
@@ -146,73 +117,546 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
/**
|
||||
* Helper to allow access to the layer to be used in the maps infowindow popup.
|
||||
*/
|
||||
ws.get(serverOptions.base_url + '/infowindow', function(req, res){
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_infowindow');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function(){
|
||||
serverOptions.getInfowindow(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW', err);
|
||||
//ws.sendResponse(res, [{error: err.message}, 500]);
|
||||
} else {
|
||||
ws.sendResponse(res, [{infowindow: data}, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Helper to allow access to metadata to be used in embedded maps.
|
||||
*/
|
||||
ws.get(serverOptions.base_url + '/map_metadata', function(req, res){
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_map_metadata');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function(){
|
||||
serverOptions.getMapMetadata(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA', err);
|
||||
//ws.sendResponse(res, [err.message, 500]);
|
||||
} else {
|
||||
ws.sendResponse(res, [{map_metadata: data}, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper API to allow per table tile cache (and sql cache) to be invalidated remotely.
|
||||
* TODO: Move?
|
||||
*/
|
||||
ws.del(serverOptions.base_url + '/flush_cache', function(req, res){
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.flush_cache');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function flushCache(){
|
||||
serverOptions.flushCache(req, serverOptions.cache_enabled ? Cache : null, this);
|
||||
},
|
||||
function sendResponse(err, data){
|
||||
if (err){
|
||||
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE', err);
|
||||
//ws.sendResponse(res, [500]);
|
||||
} else {
|
||||
ws.sendResponse(res, [{status: 'ok'}, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ---- Template maps interface starts @{
|
||||
|
||||
ws.userByReq = function(req) {
|
||||
return serverOptions.userByReq(req);
|
||||
}
|
||||
|
||||
// Add a template
|
||||
ws.post(template_baseurl, function(req, res) {
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can create templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
var next = this;
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cdbuser" if == dbowner ...
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
var report = req.profiler.toString();
|
||||
res.header('X-Tiler-Profiler', report);
|
||||
}
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
var statusCode = 400;
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'POST TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Update a template
|
||||
ws.put(template_baseurl + '/:template_id', function(req, res) {
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template PUT data must be of type application/json');
|
||||
template = req.body;
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
err = new Error("Invalid template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
if ( err ) throw err;
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
var report = req.profiler.toString();
|
||||
res.header('X-Tiler-Profiler', report);
|
||||
}
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'PUT TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Get a specific template
|
||||
ws.get(template_baseurl + '/:template_id', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can get template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot get template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'GET TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Delete a specific template
|
||||
ws.del(template_baseurl + '/:template_id', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can delete template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot find template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'DELETE TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, ['', 204]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Get a list of owned templates
|
||||
ws.get(template_baseurl, function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cbduser" if == dbowner ...
|
||||
var ids = _.map(tpl_ids, function(id) { return cdbuser + '@' + id; })
|
||||
return { template_ids: ids };
|
||||
},
|
||||
function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, statusCode]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ws.setDBParams = function(cdbuser, params, callback) {
|
||||
Step(
|
||||
function setAuth() {
|
||||
serverOptions.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
serverOptions.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
ws.options(template_baseurl + '/:template_id', function(req, res) {
|
||||
ws.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
});
|
||||
|
||||
// Instantiate a template
|
||||
function instanciateTemplate(req, res, template_params, callback) {
|
||||
ws.doCORS(res);
|
||||
if ( req.profiler ) req.profiler.done('cors');
|
||||
var that = this;
|
||||
var response = {};
|
||||
var template;
|
||||
var signedMaps = serverOptions.signedMaps;
|
||||
var layergroup;
|
||||
var layergroupid;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = ws.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] && tpl_id[0] != cdbuser ) {
|
||||
var err = new Error('Cannot instanciate map of user "'
|
||||
+ tpl_id[0] + '" on database of user "'
|
||||
+ cdbuser + '"')
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
var auth_token = req.query.auth_token;
|
||||
Step(
|
||||
function getTemplate(){
|
||||
templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function checkAuthorized(err, data) {
|
||||
if ( req.profiler ) req.profiler.done('getTemplate');
|
||||
if ( err ) throw err;
|
||||
if ( ! data ) {
|
||||
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
template = data;
|
||||
var cert = templateMaps.getTemplateCertificate(template);
|
||||
var authorized = false;
|
||||
try {
|
||||
// authorizedByCert will throw if unauthorized
|
||||
authorized = signedMaps.authorizedByCert(cert, auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
err = new Error('Unauthorized template instanciation');
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
/*if ( (! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') && req.query.callback === undefined) {
|
||||
throw new Error('template POST data must be of type application/json, it is instead ');
|
||||
}*/
|
||||
//var template_params = req.body;
|
||||
if ( req.profiler ) req.profiler.done('authorizedByCert');
|
||||
return templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = { query: {}, params: {}, headers: _.clone(req.headers),
|
||||
profiler: req.profiler
|
||||
};
|
||||
ws.setDBParams(cdbuser, fakereq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
if ( req.profiler ) req.profiler.done('setDBParams');
|
||||
if ( err ) throw err;
|
||||
cartoData.getUserMapKey(cdbuser, this);
|
||||
},
|
||||
function createLayergroup(err, val) {
|
||||
if ( req.profiler ) req.profiler.done('getUserMapKey');
|
||||
if ( err ) throw err;
|
||||
fakereq.params.api_key = val;
|
||||
ws.createLayergroup(layergroup, fakereq, this);
|
||||
},
|
||||
function signLayergroup(err, resp) {
|
||||
// NOTE: createLayergroup uses profiler.start()/end() internally
|
||||
//if ( req.profiler ) req.profiler.done('createLayergroup');
|
||||
if ( err ) throw err;
|
||||
response = resp;
|
||||
var signer = cdbuser;
|
||||
var map_id = response.layergroupid.split(':')[0]; // dropping last_updated
|
||||
var crt_id = template.auth_id; // check ?
|
||||
if ( ! crt_id ) {
|
||||
var errmsg = "Template '" + tpl_id + "' of user '" + cdbuser + "' has no signature";
|
||||
// Is this really illegal ?
|
||||
// Maybe we could just return an unsigned layergroupid
|
||||
// in this case...
|
||||
err = new Error(errmsg);
|
||||
err.http_status = 403; // Forbidden, we refuse to respond to this
|
||||
throw err;
|
||||
}
|
||||
signedMaps.signMap(signer, map_id, crt_id, this);
|
||||
},
|
||||
function prepareResponse(err) {
|
||||
if ( req.profiler ) req.profiler.done('signMap');
|
||||
if ( err ) throw err;
|
||||
//console.log("Response from createLayergroup: "); console.dir(response);
|
||||
// Add the signature part to the token!
|
||||
var tplhash = templateMaps.fingerPrint(template).substring(0,8);
|
||||
if ( req.profiler ) req.profiler.done('fingerPrint');
|
||||
response.layergroupid = cdbuser + '@' + tplhash + '@' + response.layergroupid;
|
||||
return response;
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function finish_instanciation(err, response, res, req) {
|
||||
if ( req.profiler ) {
|
||||
var report = req.profiler.toString();
|
||||
res.header('X-Tiler-Profiler', report);
|
||||
}
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
if(debug) {
|
||||
response.stack = err.stack;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
|
||||
ws.post(template_baseurl + '/:template_id', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') {
|
||||
throw new Error('template POST data must be of type application/json, it is instead ');
|
||||
}
|
||||
instanciateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
finish_instanciation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instanciate a template with a json call.
|
||||
* callback query argument is mandartoy
|
||||
*/
|
||||
ws.get(template_baseurl + '/:template_id/jsonp', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
throw new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
var config = {};
|
||||
if(req.query.config) {
|
||||
try {
|
||||
config = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
throw new Error('badformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
instanciateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
finish_instanciation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// ---- Template maps interface ends @}
|
||||
|
||||
return ws;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = CartodbWindshaft;
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
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) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.templateBaseUrl = templateBaseUrl;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
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')
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
NamedMapsController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
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;
|
||||
}
|
||||
// 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')
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a specific template
|
||||
NamedMapsController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
self.instantiateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Instantiate a template
|
||||
NamedMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var template;
|
||||
var layergroup;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = templateName(req.params.template_id);
|
||||
var auth_token = req.query.auth_token;
|
||||
step(
|
||||
function getTemplate(){
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function checkAuthorized(err, templateValue) {
|
||||
if ( req.profiler ) req.profiler.done('getTemplate');
|
||||
if ( err ) throw err;
|
||||
if ( ! templateValue ) {
|
||||
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
template = templateValue;
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(template, auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
err = new Error('Unauthorized template instanciation');
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByCert');
|
||||
}
|
||||
|
||||
return self.templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = {
|
||||
query: {},
|
||||
params: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.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 ) {
|
||||
throw err;
|
||||
}
|
||||
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
|
||||
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
|
||||
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name));
|
||||
|
||||
return layergroup;
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.finish_instantiation = function(err, response, res) {
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
this.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
};
|
||||
|
||||
function finishFn(app, res, description, okResponse) {
|
||||
return function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err) {
|
||||
statusCode = 400;
|
||||
response = { error: '' + err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
app.sendError(res, response, statusCode, description, err);
|
||||
} else {
|
||||
app.sendResponse(res, okResponse || [response, statusCode]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
function CdbRequest() {
|
||||
this.RE_USER_FROM_HOST = new RegExp(global.environment.user_from_host ||
|
||||
'^([^\\.]+)\\.' // would extract "strk" from "strk.cartodb.com"
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = CdbRequest;
|
||||
|
||||
|
||||
CdbRequest.prototype.userByReq = function(req) {
|
||||
var host = req.headers.host;
|
||||
if (req.params.user) {
|
||||
return req.params.user;
|
||||
}
|
||||
var mat = host.match(this.RE_USER_FROM_HOST);
|
||||
if ( ! mat ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' does not match hostname '" + host + "'");
|
||||
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);
|
||||
return;
|
||||
}
|
||||
return mat[1];
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
var Datasource = require('windshaft').Datasource;
|
||||
|
||||
function MapConfigNamedLayersAdapter(templateMaps) {
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = MapConfigNamedLayersAdapter;
|
||||
|
||||
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
|
||||
var self = this;
|
||||
|
||||
var adaptLayersQueue = queue(layers.length);
|
||||
|
||||
function adaptLayer(layer, done) {
|
||||
if (isNamedTypeLayer(layer)) {
|
||||
|
||||
if (!layer.options.name) {
|
||||
return done(new Error('Missing Named Map `name` in layer options'));
|
||||
}
|
||||
|
||||
var templateName = layer.options.name;
|
||||
var templateConfigParams = layer.options.config || {};
|
||||
var templateAuthTokens = layer.options.auth_tokens;
|
||||
|
||||
self.templateMaps.getTemplate(username, templateName, function(err, template) {
|
||||
if (err || !template) {
|
||||
return done(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
|
||||
}
|
||||
|
||||
if (self.templateMaps.isAuthorized(template, templateAuthTokens)) {
|
||||
var nestedNamedLayers = template.layergroup.layers.filter(function(layer) {
|
||||
return layer.type === 'named';
|
||||
});
|
||||
|
||||
if (nestedNamedLayers.length > 0) {
|
||||
var nestedNamedMapsError = new Error('Nested named layers are not allowed');
|
||||
// nestedNamedMapsError.http_status = 400;
|
||||
return done(nestedNamedMapsError);
|
||||
}
|
||||
|
||||
try {
|
||||
var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams);
|
||||
return done(null, {
|
||||
datasource: true,
|
||||
layers: templateLayergroupConfig.layers
|
||||
});
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
} else {
|
||||
var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation");
|
||||
unauthorizedError.http_status = 403;
|
||||
return done(unauthorizedError);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
return done(null, {
|
||||
datasource: false,
|
||||
layers: [layer]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var datasourceBuilder = new Datasource.Builder();
|
||||
|
||||
function layersAdaptQueueFinish(err, layersResults) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!layersResults || layersResults.length === 0) {
|
||||
return callback(new Error('Missing layers array from layergroup config'));
|
||||
}
|
||||
|
||||
var layers = [],
|
||||
currentLayerIndex = 0;
|
||||
|
||||
layersResults.forEach(function(layersResult) {
|
||||
|
||||
layersResult.layers.forEach(function(layer) {
|
||||
layers.push(layer);
|
||||
if (layersResult.datasource) {
|
||||
datasourceBuilder.withLayerDatasource(currentLayerIndex, {
|
||||
user: dbAuth.dbuser
|
||||
});
|
||||
}
|
||||
currentLayerIndex++;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
}
|
||||
|
||||
|
||||
var dbAuth = {};
|
||||
|
||||
if (_.some(layers, isNamedTypeLayer)) {
|
||||
// Lazy load dbAuth
|
||||
dbMetadata.setDBAuth(username, dbAuth, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
layers.forEach(function(layer) {
|
||||
adaptLayersQueue.defer(adaptLayer, layer);
|
||||
});
|
||||
adaptLayersQueue.awaitAll(layersAdaptQueueFinish);
|
||||
});
|
||||
} else {
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function isNamedTypeLayer(layer) {
|
||||
return layer.type === 'named';
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var step = require('step');
|
||||
|
||||
function HealthCheck(metadataBackend, tilelive) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.tilelive = tilelive;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
<Map
|
||||
background-color="#c33"
|
||||
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
|
||||
</Map>
|
||||
File diff suppressed because it is too large
Load Diff
401
lib/cartodb/signed_maps.js
Normal file
401
lib/cartodb/signed_maps.js
Normal file
@@ -0,0 +1,401 @@
|
||||
var crypto = require('crypto');
|
||||
var Step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
var debug = global.environment ? global.environment.debug : undefined;
|
||||
|
||||
// Class handling map signatures and user certificates
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
||||
//
|
||||
// @param redis_pool an instance of a "redis-mpool"
|
||||
// See https://github.com/CartoDB/node-redis-mpool
|
||||
// Needs version 0.x.x of the API.
|
||||
//
|
||||
function SignedMaps(redis_pool) {
|
||||
this.redis_pool = redis_pool;
|
||||
|
||||
// Database containing signatures
|
||||
// TODO: allow configuring ?
|
||||
// NOTE: currently it is the same as
|
||||
// the one containing layergroups
|
||||
this.db_signatures = 0;
|
||||
|
||||
//
|
||||
// Map signatures in redis are reference to signature certificates
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User certificates: set of per-user authorization certificates
|
||||
// 2. Map signatures: set of per-map certificate references
|
||||
// 3. Certificate applications: set of per-certificate signed maps
|
||||
|
||||
// User certificates (HASH:crt_id->crt_val)
|
||||
this.key_map_crt = "map_crt|<%= signer %>";
|
||||
|
||||
// Map signatures (SET:crt_id)
|
||||
this.key_map_sig = "map_sig|<%= signer %>|<%= map_id %>";
|
||||
|
||||
// Certificates applications (SET:map_id)
|
||||
//
|
||||
// Everytime a map is signed, the map identifier (layergroup_id)
|
||||
// is added to this set. The purpose of this set is to drop
|
||||
// all map signatures when a certificate is removed
|
||||
//
|
||||
this.key_crt_sig = "crt_sig|<%= signer %>|<%= crt_id %>";
|
||||
|
||||
};
|
||||
|
||||
var o = SignedMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
|
||||
o._acquireRedis = function(callback) {
|
||||
this.redis_pool.acquire(this.db_signatures, callback);
|
||||
};
|
||||
|
||||
o._releaseRedis = function(client) {
|
||||
this.redis_pool.release(this.db_signatures, client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function to communicate with redis
|
||||
*
|
||||
* @param redisFunc - the redis function to execute
|
||||
* @param redisArgs - the arguments for the redis function in an array
|
||||
* @param callback - function to pass results too.
|
||||
*/
|
||||
o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var redisClient;
|
||||
var that = this;
|
||||
var db = that.db_signatures;
|
||||
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function executeQuery(err, data) {
|
||||
if ( err ) throw err;
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
o._getAuthMethod = function(auth) {
|
||||
return auth.method || 'open';
|
||||
};
|
||||
|
||||
//--------------- PUBLIC API -------------------------------------
|
||||
|
||||
/// Check formal validity of a certificate
|
||||
//
|
||||
/// Return an Error instance if invalid, null otherwise
|
||||
///
|
||||
o.checkInvalidCertificate = function(cert) {
|
||||
//console.log("Checking cert: "); console.dir(cert);
|
||||
if ( cert.version !== "0.0.1" ) {
|
||||
return new Error("Unsupported certificate version " + cert.version);
|
||||
}
|
||||
|
||||
if ( ! cert.auth ) {
|
||||
console.log("Cert is : "); console.dir(cert);
|
||||
return new Error("No certificate authorization");
|
||||
}
|
||||
|
||||
var method = this._getAuthMethod(cert.auth);
|
||||
|
||||
switch ( method ) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(cert.auth.valid_tokens) )
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! cert.auth.valid_tokens.length )
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + cert.auth.method);
|
||||
break;
|
||||
}
|
||||
|
||||
return null; // all valid
|
||||
}
|
||||
|
||||
// Check if the given certificate authorizes waiver of "auth"
|
||||
o.authorizedByCert = function(cert, auth) {
|
||||
|
||||
var err = this.checkInvalidCertificate(cert);
|
||||
if ( err ) throw err;
|
||||
|
||||
var method = this._getAuthMethod(cert.auth);
|
||||
|
||||
// Open authentication certificates are always authorized
|
||||
if ( method === 'open' ) return true;
|
||||
|
||||
// Token based authentication requires valid token
|
||||
if ( method === 'token' ) {
|
||||
var found = cert.auth.valid_tokens.indexOf(auth);
|
||||
//if ( found !== -1 ) {
|
||||
//console.log("Token " + auth + " is found at position " + found + " in valid tokens " + cert.auth.valid_tokens);
|
||||
// return true;
|
||||
//} else return false;
|
||||
return cert.auth.valid_tokens.indexOf(auth) !== -1;
|
||||
}
|
||||
|
||||
throw new Error("Unsupported authentication method: " + cert.auth.method);
|
||||
};
|
||||
|
||||
// Check if shown credential are authorized to access a map
|
||||
// by the given signer.
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param map_id a layergroup_id
|
||||
// @param auth an authentication token, or undefined if none
|
||||
// (can still be authorized by signature)
|
||||
//
|
||||
// @param callback function(Error, Boolean)
|
||||
//
|
||||
o.isAuthorized = function(signer, map_id, auth, callback) {
|
||||
var that = this;
|
||||
var redisClient;
|
||||
var db = that.db_signatures;
|
||||
var authorized = false;
|
||||
var certificate_id_list;
|
||||
var missing_certificates = [];
|
||||
if ( debug ) {
|
||||
console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' with auth '" + auth + "'");
|
||||
}
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function getMapSignatures(err, client) {
|
||||
if ( err ) throw err;
|
||||
redisClient = client;
|
||||
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
|
||||
redisClient.SMEMBERS(map_sig_key, this);
|
||||
//that._redisCmd('SMEMBERS', [ map_sig_key ], this);
|
||||
},
|
||||
function getCertificates(err, crt_lst) {
|
||||
if ( err ) throw err;
|
||||
if ( debug ) {
|
||||
console.log("Map '" + map_id + "' is signed by " + crt_lst.length + " certificates of user '" + signer);
|
||||
}
|
||||
certificate_id_list = crt_lst;
|
||||
if ( ! crt_lst.length ) {
|
||||
// No certs, avoid calling redis with short args list.
|
||||
// Next step expects a list of certificate values so
|
||||
// we directly send the empty list.
|
||||
return crt_lst;
|
||||
}
|
||||
var map_crt_key = _.template(that.key_map_crt, {signer:signer});
|
||||
//that._redisCmd('HMGET', [ map_crt_key ].concat(crt_lst), this);
|
||||
redisClient.HMGET(map_crt_key, crt_lst, this);
|
||||
},
|
||||
function checkCertificates(err, certs) {
|
||||
if ( err ) throw err;
|
||||
for (var i=0; i<certs.length; ++i) {
|
||||
var crt_id = certificate_id_list[i];
|
||||
if ( _.isNull(certs[i]) ) {
|
||||
missing_certificates.push(crt_id);
|
||||
continue;
|
||||
}
|
||||
var cert;
|
||||
try {
|
||||
//console.log("cert " + crt_id + ": " + certs[i]);
|
||||
cert = JSON.parse(certs[i]);
|
||||
authorized = that.authorizedByCert(cert, auth);
|
||||
} catch (err) {
|
||||
console.log("Certificate " + certificate_id_list[i] + " by user '" + signer + "' is malformed: " + err);
|
||||
continue;
|
||||
}
|
||||
if ( authorized ) {
|
||||
if ( debug ) {
|
||||
console.log("Access to map '" + map_id + "' authorized by cert '"
|
||||
+ certificate_id_list[i] + "' of user '" + signer + "'");
|
||||
}
|
||||
//console.dir(cert);
|
||||
break; // no need to further check certs
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( missing_certificates.length ) {
|
||||
console.log("WARNING: map '" + map_id + "' is signed by '" + signer
|
||||
+ "' with " + missing_certificates.length
|
||||
+ " missing certificates: "
|
||||
+ missing_certificates + " (TODO: give cleanup instructions)");
|
||||
}
|
||||
if ( redisClient ) that.redis_pool.release(db, redisClient);
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Add an authorization certificate from a user.
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param cert certificate object, see
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
||||
//
|
||||
// @param callback function(err, crt_id) return certificate id
|
||||
//
|
||||
// TODO: allow for requesting error when certificate already exists ?
|
||||
//
|
||||
o.addCertificate = function(signer, cert, callback) {
|
||||
var crt_val = JSON.stringify(cert);
|
||||
var crt_id = crypto.createHash('md5').update(crt_val).digest('hex');
|
||||
|
||||
var usr_crt_key = _.template(this.key_map_crt, {signer:signer});
|
||||
this._redisCmd('HSET', [ usr_crt_key, crt_id, crt_val ], function(err, created) {
|
||||
// NOTE: created would be 0 if the field already existed, 1 otherwise
|
||||
callback(err, crt_id);
|
||||
});
|
||||
};
|
||||
|
||||
// Remove an authorization certificate of a user, also removing
|
||||
// any signature made with the certificate.
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param crt_id certificate identifier, as returned by addCertificate
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delCertificate = function(signer, crt_id, callback) {
|
||||
var db = this.db_signatures;
|
||||
var crt_sig_key = _.template(this.key_crt_sig, {signer:signer, crt_id:crt_id});
|
||||
var signed_map_list;
|
||||
var redis_client;
|
||||
var that = this;
|
||||
Step (
|
||||
function getRedisClient() {
|
||||
that._acquireRedis(this);
|
||||
},
|
||||
function removeCertificate(err, data) {
|
||||
if ( err ) throw err;
|
||||
redis_client = data;
|
||||
// Remove the certificate (would be enough to stop authorizing uses)
|
||||
var usr_crt_key = _.template(that.key_map_crt, {signer:signer});
|
||||
redis_client.HDEL(usr_crt_key, crt_id, this);
|
||||
},
|
||||
function getMapSignatures(err, deleted) {
|
||||
if ( err ) throw err;
|
||||
if ( ! deleted ) {
|
||||
// debugging (how can this be possible?)
|
||||
console.log("WARNING: authorization certificate '" + crt_id
|
||||
+ "' by user '" + signer + "' did not exist on delete request");
|
||||
}
|
||||
// Get all signatures by this certificate
|
||||
redis_client.SMEMBERS(crt_sig_key, this);
|
||||
},
|
||||
function delMapSignaturesReference(err, map_id_list) {
|
||||
if ( err ) throw err;
|
||||
signed_map_list = map_id_list;
|
||||
if ( debug ) {
|
||||
console.log("Certificate '" + crt_id + "' from user '" + signer
|
||||
+ "' was used to sign " + signed_map_list.length + " maps");
|
||||
}
|
||||
redis_client.DEL(crt_sig_key, this);
|
||||
},
|
||||
function delMapSignatures(err) {
|
||||
if ( err ) throw err;
|
||||
var crt_sig_key = _.template(that.key_crt_sig, {signer:signer, crt_id:crt_id});
|
||||
var tx = redis_client.MULTI();
|
||||
for (var i=0; i<signed_map_list.length; ++i) {
|
||||
var map_id = signed_map_list[i];
|
||||
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
|
||||
//console.log("Queuing removal of '" + crt_id + "' from '" + map_sig_key + "'");
|
||||
tx.SREM( map_sig_key, crt_id )
|
||||
}
|
||||
tx.EXEC(this);
|
||||
},
|
||||
function reportTransaction(err, rets) {
|
||||
if ( err ) throw err;
|
||||
if ( debug ) {
|
||||
for (var i=0; i<signed_map_list.length; ++i) {
|
||||
var ret = rets[i];
|
||||
if ( ! ret ) {
|
||||
console.log("No signature with certificate '" + crt_id
|
||||
+ "' of user '" + signer + "' found in map '"
|
||||
+ signed_map_list[i] + "'");
|
||||
} else {
|
||||
console.log("Signature with certificate '" + crt_id
|
||||
+ "' of user '" + signer + "' removed from map '"
|
||||
+ signed_map_list[i] + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( ! _.isUndefined(redis_client) ) {
|
||||
that._releaseRedis(redis_client);
|
||||
}
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Sign a map with a certificate reference
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param map_id a layergroup_id
|
||||
// @param crt_id signature certificate identifier
|
||||
//
|
||||
// @param callback function(Error)
|
||||
//
|
||||
o.signMap = function(signer, map_id, crt_id, callback) {
|
||||
var that = this;
|
||||
Step(
|
||||
function addMapSignature() {
|
||||
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
|
||||
if ( debug ) {
|
||||
console.log("Adding " + crt_id + " to " + map_sig_key);
|
||||
}
|
||||
that._redisCmd('SADD', [ map_sig_key, crt_id ], this);
|
||||
},
|
||||
function addCertificateUsage(err) {
|
||||
// Add the map to the set of maps signed by the given cert
|
||||
if ( err ) throw err;
|
||||
var crt_sig_key = _.template(that.key_crt_sig, {signer:signer, crt_id:crt_id});
|
||||
that._redisCmd('SADD', [ crt_sig_key, map_id ], this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Sign a map with a full certificate
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param map_id a layergroup_id
|
||||
// @param cert_id signature certificate identifier
|
||||
//
|
||||
// @param callback function(Error, String) return certificate id
|
||||
//
|
||||
o.addSignature = function(signer, map_id, cert, callback) {
|
||||
var that = this;
|
||||
var certificate_id;
|
||||
Step(
|
||||
function addCertificate() {
|
||||
that.addCertificate(signer, cert, this);
|
||||
},
|
||||
function signMap(err, cert_id) {
|
||||
if ( err ) throw err;
|
||||
if ( ! cert_id ) throw new Error("addCertificate returned no certificate id");
|
||||
certificate_id = cert_id;
|
||||
that.signMap(signer, map_id, cert_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err, certificate_id);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = SignedMaps;
|
||||
@@ -1,12 +1,10 @@
|
||||
var crypto = require('crypto');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
var crypto = require('crypto');
|
||||
var Step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
// Templates in this hash (keyed as <username>@<template_name>)
|
||||
// are being worked on.
|
||||
var user_template_locks = {};
|
||||
|
||||
// Class handling map templates
|
||||
//
|
||||
@@ -16,16 +14,16 @@ var util = require('util');
|
||||
// See https://github.com/CartoDB/node-redis-mpool
|
||||
// Needs version 0.x.x of the API.
|
||||
//
|
||||
// @param signed_maps an instance of a "signed_maps" class,
|
||||
// See signed_maps.js
|
||||
//
|
||||
// @param opts TemplateMap options. Supported elements:
|
||||
// 'max_user_templates' limit on the number of per-user
|
||||
//
|
||||
//
|
||||
function TemplateMaps(redis_pool, opts) {
|
||||
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
function TemplateMaps(redis_pool, signed_maps, opts) {
|
||||
this.redis_pool = redis_pool;
|
||||
this.signed_maps = signed_maps;
|
||||
this.opts = opts || {};
|
||||
|
||||
// Database containing templates
|
||||
@@ -40,23 +38,32 @@ function TemplateMaps(redis_pool, opts) {
|
||||
//
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User templates: set of per-user map templates
|
||||
// 1. User teplates: set of per-user map templates
|
||||
// NOTE: each template would have an associated auth
|
||||
// reference, see signed_maps.js
|
||||
|
||||
// User templates (HASH:tpl_id->tpl_val)
|
||||
this.key_usr_tpl = dot.template("map_tpl|{{=it.owner}}");
|
||||
}
|
||||
this.key_usr_tpl = "map_tpl|<%= owner %>";
|
||||
|
||||
util.inherits(TemplateMaps, EventEmitter);
|
||||
|
||||
module.exports = TemplateMaps;
|
||||
// User template locks (HASH:tpl_id->ctime)
|
||||
this.key_usr_tpl_lck = "map_tpl|<%= owner %>|locks";
|
||||
|
||||
};
|
||||
|
||||
var o = TemplateMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
|
||||
o._userTemplateLimit = function() {
|
||||
return this.opts.max_user_templates || 0;
|
||||
return this.opts['max_user_templates'] || 0;
|
||||
};
|
||||
|
||||
o._acquireRedis = function(callback) {
|
||||
this.redis_pool.acquire(this.db_signatures, callback);
|
||||
};
|
||||
|
||||
o._releaseRedis = function(client) {
|
||||
this.redis_pool.release(this.db_signatures, client);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,7 +78,7 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var that = this;
|
||||
var db = that.db_signatures;
|
||||
|
||||
step(
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
@@ -88,8 +95,38 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
|
||||
// jshint maxcomplexity:15
|
||||
// @param callback function(err, obtained)
|
||||
o._obtainTemplateLock = function(owner, tpl_id, callback) {
|
||||
var usr_tpl_lck_key = _.template(this.key_usr_tpl_lck, {owner:owner});
|
||||
var that = this;
|
||||
var gotLock = false;
|
||||
Step (
|
||||
function obtainLock() {
|
||||
var ctime = Date.now();
|
||||
that._redisCmd('HSETNX', [usr_tpl_lck_key, tpl_id, ctime], this);
|
||||
},
|
||||
function checkLock(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
// TODO: unlock if expired ?
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
return gotLock = true;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err, gotLock);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// @param callback function(err, deleted)
|
||||
o._releaseTemplateLock = function(owner, tpl_id, callback) {
|
||||
var usr_tpl_lck_key = _.template(this.key_usr_tpl_lck, {owner:owner});
|
||||
this._redisCmd('HDEL', [usr_tpl_lck_key, tpl_id], callback);
|
||||
};
|
||||
|
||||
o._reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
|
||||
o._checkInvalidTemplate = function(template) {
|
||||
if ( template.version != '0.0.1' ) {
|
||||
return new Error("Unsupported template version " + template.version);
|
||||
@@ -98,87 +135,47 @@ o._checkInvalidTemplate = function(template) {
|
||||
if ( ! tplname ) {
|
||||
return new Error("Missing template name");
|
||||
}
|
||||
if ( ! tplname.match(_reValidIdentifier) ) {
|
||||
if ( ! tplname.match(this._reValidIdentifier) ) {
|
||||
return new Error("Invalid characters in template name '" + tplname + "'");
|
||||
}
|
||||
|
||||
var invalidError = isInvalidLayergroup(template.layergroup);
|
||||
if (invalidError) {
|
||||
return invalidError;
|
||||
}
|
||||
|
||||
var placeholders = template.placeholders || {};
|
||||
|
||||
var placeholderKeys = Object.keys(placeholders);
|
||||
for (var i = 0, len = placeholderKeys.length; i < len; i++) {
|
||||
var placeholderKey = placeholderKeys[i];
|
||||
|
||||
if (!placeholderKey.match(_reValidIdentifier)) {
|
||||
return new Error("Invalid characters in placeholder name '" + placeholderKey + "'");
|
||||
}
|
||||
if ( ! placeholders[placeholderKey].hasOwnProperty('default') ) {
|
||||
return new Error("Missing default for placeholder '" + placeholderKey + "'");
|
||||
}
|
||||
if ( ! placeholders[placeholderKey].hasOwnProperty('type') ) {
|
||||
return new Error("Missing type for placeholder '" + placeholderKey + "'");
|
||||
}
|
||||
}
|
||||
|
||||
var auth = template.auth || {};
|
||||
|
||||
switch ( auth.method ) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(auth.valid_tokens) )
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! auth.valid_tokens.length )
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + auth.method);
|
||||
var phold = template.placeholders;
|
||||
for (var k in phold) {
|
||||
if ( ! k.match(this._reValidIdentifier) ) {
|
||||
return new Error("Invalid characters in placeholder name '" + k + "'");
|
||||
}
|
||||
if ( ! phold[k].hasOwnProperty('default') ) {
|
||||
return new Error("Missing default for placeholder '" + k + "'");
|
||||
}
|
||||
if ( ! phold[k].hasOwnProperty('type') ) {
|
||||
return new Error("Missing type for placeholder '" + k + "'");
|
||||
}
|
||||
};
|
||||
|
||||
return false;
|
||||
// Check certificate validity
|
||||
var cert = this.getTemplateCertificate(template);
|
||||
var err = this.signed_maps.checkInvalidCertificate(cert);
|
||||
if ( err ) return err;
|
||||
|
||||
// TODO: run more checks over template format ?
|
||||
};
|
||||
|
||||
function isInvalidLayergroup(layergroup) {
|
||||
if (!layergroup) {
|
||||
return new Error('Missing layergroup');
|
||||
}
|
||||
|
||||
var layers = layergroup.layers;
|
||||
|
||||
if (!_.isArray(layers) || layers.length === 0) {
|
||||
return new Error('Missing or empty layers array from layergroup config');
|
||||
}
|
||||
|
||||
var invalidLayers = layers
|
||||
.map(function(layer, layerIndex) {
|
||||
return layer.options ? null : layerIndex;
|
||||
})
|
||||
.filter(function(layerIndex) {
|
||||
return layerIndex !== null;
|
||||
});
|
||||
|
||||
if (invalidLayers.length) {
|
||||
return new Error('Missing `options` in layergroup config for layers: ' + invalidLayers.join(', '));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateDefaults(template) {
|
||||
var templateAuth = _.defaults({}, template.auth || {}, {
|
||||
method: 'open'
|
||||
});
|
||||
return _.defaults({ auth: templateAuth }, template, {
|
||||
placeholders: {}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------- PUBLIC API -------------------------------------
|
||||
|
||||
// Extract a signature certificate from a template
|
||||
//
|
||||
// The certificate will be ready to be passed to
|
||||
// SignedMaps.addCertificate or SignedMaps.authorizedByCert
|
||||
//
|
||||
o.getTemplateCertificate = function(template) {
|
||||
var cert = {
|
||||
version: '0.0.1',
|
||||
template_id: template.name,
|
||||
auth: template.auth
|
||||
};
|
||||
return cert;
|
||||
};
|
||||
|
||||
// Add a template
|
||||
//
|
||||
// NOTE: locks user+template_name or fails
|
||||
@@ -192,58 +189,102 @@ function templateDefaults(template) {
|
||||
// Return template identifier (only valid for given user)
|
||||
//
|
||||
o.addTemplate = function(owner, template, callback) {
|
||||
var self = this;
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
callback(invalidError);
|
||||
return;
|
||||
}
|
||||
var tplname = template.name;
|
||||
|
||||
template = templateDefaults(template);
|
||||
// Procedure:
|
||||
//
|
||||
// - Check against limit
|
||||
// 0. Obtain a lock for user+template_name, fail if impossible
|
||||
// 1. Check no other template exists with the same name
|
||||
// 2. Install certificate extracted from template, extending
|
||||
// it to contain a name to properly salt things out.
|
||||
// 3. Modify the template object to reference certificate by id
|
||||
// 4. Install template
|
||||
// 5. Release lock
|
||||
//
|
||||
//
|
||||
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
return callback(invalidError);
|
||||
}
|
||||
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
|
||||
var gotLock = false;
|
||||
var that = this;
|
||||
var limit = that._userTemplateLimit();
|
||||
Step(
|
||||
function checkLimit() {
|
||||
if ( ! limit ) return 0;
|
||||
that._redisCmd('HLEN', [ usr_tpl_key ], this);
|
||||
},
|
||||
// try to obtain a lock
|
||||
function obtainLock(err, len) {
|
||||
if ( err ) throw err;
|
||||
if ( limit && len >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates (" + len + "/" + limit + ")");
|
||||
}
|
||||
that._obtainTemplateLock(owner, tplname, this);
|
||||
},
|
||||
function getExistingTemplate(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
throw new Error("Template '" + tplname + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
gotLock = true;
|
||||
that._redisCmd('HEXISTS', [ usr_tpl_key, tplname ], this);
|
||||
},
|
||||
function installCertificate(err, exists) {
|
||||
if ( err ) throw err;
|
||||
if ( exists ) {
|
||||
throw new Error("Template '" + tplname + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
var cert = that.getTemplateCertificate(template);
|
||||
that.signed_maps.addCertificate(owner, cert, this);
|
||||
},
|
||||
function installTemplate(err, crt_id) {
|
||||
if ( err ) throw err;
|
||||
template.auth_id = crt_id;
|
||||
var tpl_val = JSON.stringify(template);
|
||||
that._redisCmd('HSET', [ usr_tpl_key, tplname, tpl_val ], this);
|
||||
},
|
||||
function releaseLock(err, newfield) {
|
||||
if ( ! err && ! newfield ) {
|
||||
console.log("ERROR: addTemplate overridden existing template '"
|
||||
+ tplname + "' of '" + owner
|
||||
+ "' -- HSET returned " + overridden + ": someone added it without locking ?");
|
||||
// TODO: how to recover this ?!
|
||||
}
|
||||
|
||||
var templateName = template.name;
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
var limit = this._userTemplateLimit();
|
||||
if ( err && ! gotLock ) throw err;
|
||||
|
||||
step(
|
||||
function checkLimit() {
|
||||
if ( ! limit ) {
|
||||
return 0;
|
||||
}
|
||||
self._redisCmd('HLEN', [ userTemplatesKey ], this);
|
||||
},
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
if ( limit && numberOfTemplates >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates " +
|
||||
"("+ numberOfTemplates + "/" + limit + ")");
|
||||
}
|
||||
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function validateInstallation(err, wasSet) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
if ( ! wasSet ) {
|
||||
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('add', owner, templateName, template);
|
||||
}
|
||||
|
||||
callback(err, templateName, template);
|
||||
// release the lock
|
||||
var next = this;
|
||||
that._releaseTemplateLock(owner, tplname, function(e, d) {
|
||||
if ( e ) {
|
||||
console.log("Error removing lock on template '" + tplname
|
||||
+ "' of user '" + owner + "': " + e);
|
||||
} else if ( ! d ) {
|
||||
console.log("ERROR: lock on template '" + tplname
|
||||
+ "' of user '" + owner + "' externally removed during insert!");
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err, tplname);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a template
|
||||
//
|
||||
// NOTE: locks user+template_name or fails
|
||||
//
|
||||
// Also deletes associated authentication certificate, which
|
||||
// in turn deletes all instance signatures
|
||||
//
|
||||
// @param owner cartodb username of the template owner
|
||||
//
|
||||
// @param tpl_id template identifier as returned
|
||||
@@ -252,28 +293,82 @@ o.addTemplate = function(owner, template, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function deleteTemplate() {
|
||||
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
|
||||
},
|
||||
function handleDeletion(err, deleted) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!deleted) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('delete', owner, tpl_id);
|
||||
}
|
||||
|
||||
callback(err);
|
||||
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
|
||||
var gotLock = false;
|
||||
var that = this;
|
||||
Step(
|
||||
// try to obtain a lock
|
||||
function obtainLock() {
|
||||
that._obtainTemplateLock(owner, tpl_id, this);
|
||||
},
|
||||
function getExistingTemplate(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
gotLock = true;
|
||||
that._redisCmd('HGET', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function delCertificate(err, tplval) {
|
||||
if ( err ) throw err;
|
||||
if ( ! tplval ) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
var tpl = JSON.parse(tplval);
|
||||
if ( ! tpl.auth_id ) {
|
||||
// not sure this is an error, in case we'll ever
|
||||
// allow unsigned templates...
|
||||
console.log("ERROR: installed template '" + tpl_id
|
||||
+ "' of user '" + owner + "' has no auth_id reference: "); console.dir(tpl);
|
||||
return null;
|
||||
}
|
||||
var next = this;
|
||||
that.signed_maps.delCertificate(owner, tpl.auth_id, function(err) {
|
||||
if ( err ) {
|
||||
var msg = "ERROR: could not delete certificate '"
|
||||
+ tpl.auth_id + "' associated with template '"
|
||||
+ tpl_id + "' of user '" + owner + "': " + err;
|
||||
// I'm actually not sure we want this event to be fatal
|
||||
// (avoiding a deletion of the template itself)
|
||||
next(new Error(msg));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
function delTemplate(err) {
|
||||
if ( err ) throw err;
|
||||
that._redisCmd('HDEL', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function releaseLock(err, deleted) {
|
||||
if ( ! err && ! deleted ) {
|
||||
console.log("ERROR: template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during delete!");
|
||||
}
|
||||
|
||||
if ( ! gotLock ) {
|
||||
if ( err ) throw err;
|
||||
return null;
|
||||
}
|
||||
|
||||
// release the lock
|
||||
var next = this;
|
||||
that._releaseTemplateLock(owner, tpl_id, function(e, d) {
|
||||
if ( e ) {
|
||||
console.log("Error removing lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "': " + e);
|
||||
} else if ( ! d ) {
|
||||
console.log("ERROR: lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during delete!");
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
@@ -293,54 +388,104 @@ o.delTemplate = function(owner, tpl_id, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
callback(invalidError);
|
||||
return;
|
||||
}
|
||||
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
var tplname = template.name;
|
||||
|
||||
if ( invalidError ) {
|
||||
return callback(invalidError);
|
||||
}
|
||||
if ( tpl_id != tplname ) {
|
||||
callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + tplname + "')"));
|
||||
return;
|
||||
}
|
||||
|
||||
var templateName = template.name;
|
||||
|
||||
if ( tpl_id != templateName ) {
|
||||
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
|
||||
}
|
||||
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
|
||||
step(
|
||||
function getExistingTemplate() {
|
||||
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
|
||||
},
|
||||
function updateTemplate(err, currentTemplate) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!currentTemplate) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function handleTemplateUpdate(err, didSetNewField) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (didSetNewField) {
|
||||
console.warn('New template created on update operation');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
}
|
||||
|
||||
callback(err, template);
|
||||
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
|
||||
var gotLock = false;
|
||||
var that = this;
|
||||
Step(
|
||||
// try to obtain a lock
|
||||
function obtainLock() {
|
||||
that._obtainTemplateLock(owner, tpl_id, this);
|
||||
},
|
||||
function getExistingTemplate(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
gotLock = true;
|
||||
that._redisCmd('HGET', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function delOldCertificate(err, tplval) {
|
||||
if ( err ) throw err;
|
||||
if ( ! tplval ) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '"
|
||||
+ owner +"' does not exist");
|
||||
}
|
||||
var tpl = JSON.parse(tplval);
|
||||
if ( ! tpl.auth_id ) {
|
||||
// not sure this is an error, in case we'll ever
|
||||
// allow unsigned templates...
|
||||
console.log("ERROR: installed template '" + tpl_id
|
||||
+ "' of user '" + owner + "' has no auth_id reference: "); console.dir(tpl);
|
||||
return null;
|
||||
}
|
||||
var next = this;
|
||||
that.signed_maps.delCertificate(owner, tpl.auth_id, function(err) {
|
||||
if ( err ) {
|
||||
var msg = "ERROR: could not delete certificate '"
|
||||
+ tpl.auth_id + "' associated with template '"
|
||||
+ tpl_id + "' of user '" + owner + "': " + err;
|
||||
// I'm actually not sure we want this event to be fatal
|
||||
// (avoiding a deletion of the template itself)
|
||||
next(new Error(msg));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
function installNewCertificate(err) {
|
||||
if ( err ) throw err;
|
||||
var cert = that.getTemplateCertificate(template);
|
||||
that.signed_maps.addCertificate(owner, cert, this);
|
||||
},
|
||||
function updTemplate(err, crt_id) {
|
||||
if ( err ) throw err;
|
||||
template.auth_id = crt_id;
|
||||
var tpl_val = JSON.stringify(template);
|
||||
that._redisCmd('HSET', [ usr_tpl_key, tplname, tpl_val ], this);
|
||||
},
|
||||
function releaseLock(err, newfield) {
|
||||
if ( ! err && newfield ) {
|
||||
console.log("ERROR: template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during update!");
|
||||
}
|
||||
|
||||
if ( ! gotLock ) {
|
||||
if ( err ) throw err;
|
||||
return null;
|
||||
}
|
||||
|
||||
// release the lock
|
||||
var next = this;
|
||||
that._releaseTemplateLock(owner, tpl_id, function(e, d) {
|
||||
if ( e ) {
|
||||
console.log("Error removing lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "': " + e);
|
||||
} else if ( ! d ) {
|
||||
console.log("ERROR: lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during update!");
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// List user templates
|
||||
@@ -351,7 +496,8 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
// Returns a list of template identifiers
|
||||
//
|
||||
o.listTemplates = function(owner, callback) {
|
||||
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
|
||||
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
|
||||
this._redisCmd('HKEYS', [ usr_tpl_key ], callback);
|
||||
};
|
||||
|
||||
// Get a templates
|
||||
@@ -365,47 +511,22 @@ o.listTemplates = function(owner, callback) {
|
||||
// Return full template definition
|
||||
//
|
||||
o.getTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function getTemplate() {
|
||||
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
return JSON.parse(tpl_val);
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
callback(err, tpl);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
o.isAuthorized = function(template, authTokens) {
|
||||
if (!template) {
|
||||
return false;
|
||||
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
|
||||
var that = this;
|
||||
Step(
|
||||
function getTemplate() {
|
||||
that._redisCmd('HGET', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
var tpl = JSON.parse(tpl_val);
|
||||
// Should we strip auth_id ?
|
||||
return tpl;
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
callback(err, tpl);
|
||||
}
|
||||
|
||||
authTokens = _.isArray(authTokens) ? authTokens : [authTokens];
|
||||
|
||||
var templateAuth = template.auth;
|
||||
|
||||
if (!templateAuth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.isString(templateAuth) && templateAuth === 'open') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (templateAuth.method === 'open') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (templateAuth.method === 'token') {
|
||||
return _.intersection(templateAuth.valid_tokens, authTokens).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
);
|
||||
};
|
||||
|
||||
// Perform placeholder substitutions on a template
|
||||
@@ -420,22 +541,25 @@ o.isAuthorized = function(template, authTokens) {
|
||||
//
|
||||
// @throws Error on malformed template or parameter
|
||||
//
|
||||
var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
|
||||
_reCSSColorName = /^[a-zA-Z]+$/,
|
||||
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
|
||||
function _replaceVars (str, params) {
|
||||
o._reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/;
|
||||
o._reCSSColorName = /^[a-zA-Z]+$/;
|
||||
o._reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
o._replaceVars = function(str, params) {
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
// Construct regular expressions for each param
|
||||
Object.keys(params).forEach(function(k) {
|
||||
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
|
||||
});
|
||||
return str;
|
||||
}
|
||||
if ( ! params._re ) {
|
||||
params._re = {};
|
||||
for (var k in params) {
|
||||
params._re[k] = RegExp("<%=\\s*" + k + "\\s*%>", "g");
|
||||
}
|
||||
}
|
||||
for (var k in params) str = str.replace(params._re[k], params[k]);
|
||||
return str;
|
||||
};
|
||||
o.instance = function(template, params) {
|
||||
var all_params = {};
|
||||
var phold = template.placeholders || {};
|
||||
Object.keys(phold).forEach(function(k) {
|
||||
var phold = template.placeholders;
|
||||
for (var k in phold) {
|
||||
var val = params.hasOwnProperty(k) ? params[k] : phold[k].default;
|
||||
var type = phold[k].type;
|
||||
// properly escape
|
||||
@@ -449,15 +573,17 @@ o.instance = function(template, params) {
|
||||
}
|
||||
else if ( type === 'number' ) {
|
||||
// check it's a number
|
||||
if ( typeof(val) !== 'number' && ! val.match(_reNumber) ) {
|
||||
throw new Error("Invalid number value for template parameter '" + k + "': " + val);
|
||||
if ( typeof(val) !== 'number' && ! val.match(this._reNumber) ) {
|
||||
throw new Error("Invalid number value for template parameter '"
|
||||
+ k + "': " + val);
|
||||
}
|
||||
}
|
||||
else if ( type === 'css_color' ) {
|
||||
// check it only contains letters or
|
||||
// starts with # and only contains hexdigits
|
||||
if ( ! val.match(_reCSSColorName) && ! val.match(_reCSSColorVal) ) {
|
||||
throw new Error("Invalid css_color value for template parameter '" + k + "': " + val);
|
||||
if ( ! val.match(this._reCSSColorName) && ! val.match(this._reCSSColorVal) ) {
|
||||
throw new Error("Invalid css_color value for template parameter '"
|
||||
+ k + "': " + val);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -465,23 +591,16 @@ o.instance = function(template, params) {
|
||||
throw new Error("Invalid placeholder type '" + type + "'");
|
||||
}
|
||||
all_params[k] = val;
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: we're deep-cloning the layergroup here
|
||||
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
|
||||
for (var i=0; i<layergroup.layers.length; ++i) {
|
||||
var lyropt = layergroup.layers[i].options;
|
||||
if ( lyropt.cartocss ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
|
||||
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
if ( lyropt.cartocss ) lyropt.cartocss = this._replaceVars(lyropt.cartocss, all_params);
|
||||
if ( lyropt.sql) lyropt.sql = this._replaceVars(lyropt.sql, all_params);
|
||||
// Anything else ?
|
||||
}
|
||||
|
||||
// extra information about the template
|
||||
layergroup.template = {
|
||||
name: template.name,
|
||||
auth: template.auth
|
||||
};
|
||||
|
||||
return layergroup;
|
||||
};
|
||||
|
||||
@@ -493,13 +612,4 @@ o.fingerPrint = function(template) {
|
||||
;
|
||||
};
|
||||
|
||||
module.exports.templateName = function templateName(templateId) {
|
||||
var templateIdTokens = templateId.split('@');
|
||||
var name = templateIdTokens[0];
|
||||
|
||||
if (templateIdTokens.length > 1) {
|
||||
name = templateIdTokens[1];
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
module.exports = TemplateMaps;
|
||||
|
||||
2819
npm-shrinkwrap.json
generated
2819
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.2.0",
|
||||
"version": "1.8.5",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -22,34 +22,24 @@
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "0.43.0",
|
||||
"step": "~0.0.5",
|
||||
"queue-async": "~1.0.7",
|
||||
"request": "~2.9.203",
|
||||
"cartodb-redis": "~0.12.1",
|
||||
"cartodb-psql": "~0.4.0",
|
||||
"fastly-purge": "~1.0.0",
|
||||
"redis-mpool": "~0.3.0",
|
||||
"lzma": "~1.3.7",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb"
|
||||
"node-varnish": "http://github.com/Vizzuality/node-varnish/tarball/v0.2.0",
|
||||
"underscore" : "~1.3.3",
|
||||
"windshaft" : "http://github.com/CartoDB/Windshaft/tarball/0.19.4",
|
||||
"step": "0.0.x",
|
||||
"request": "2.9.202",
|
||||
"cartodb-redis": "~0.3.0",
|
||||
"redis-mpool": "http://github.com/CartoDB/node-redis-mpool/tarball/0.0.4",
|
||||
"mapnik": "~0.7.22",
|
||||
"lzma": "~1.2.3",
|
||||
"log4js": "~0.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.3.6",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~1.3.0",
|
||||
"jshint": "~2.6.0",
|
||||
"redis": "~0.8.6",
|
||||
"strftime": "~0.8.2",
|
||||
"semver": "~1.1.4"
|
||||
"mocha": "1.14.0",
|
||||
"redis": "~0.8.3",
|
||||
"strftime": "~0.6.0",
|
||||
"semver": "~1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=1.2.1"
|
||||
"test": "make check"
|
||||
}
|
||||
}
|
||||
|
||||
17
run_tests.sh
17
run_tests.sh
@@ -4,9 +4,6 @@ OPT_CREATE_REDIS=yes # create the redis test environment
|
||||
OPT_CREATE_PGSQL=yes # create the PostgreSQL test environment
|
||||
OPT_DROP_REDIS=yes # drop the redis test environment
|
||||
OPT_DROP_PGSQL=yes # drop the PostgreSQL test environment
|
||||
OPT_COVERAGE=no # run tests with coverage
|
||||
|
||||
export PGAPPNAME=cartodb_tiler_tester
|
||||
|
||||
cd $(dirname $0)
|
||||
BASEDIR=$(pwd)
|
||||
@@ -70,10 +67,6 @@ while [ -n "$1" ]; do
|
||||
OPT_CREATE_REDIS=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--with-coverage"; then
|
||||
OPT_COVERAGE=yes
|
||||
shift
|
||||
continue
|
||||
# This is kept for backward compatibility
|
||||
elif test "$1" = "--nocreate"; then
|
||||
OPT_CREATE_REDIS=no
|
||||
@@ -90,7 +83,6 @@ if [ -z "$1" ]; then
|
||||
echo "Options:" >&2
|
||||
echo " --nocreate do not create the test environment on start" >&2
|
||||
echo " --nodrop do not drop the test environment on exit" >&2
|
||||
echo " --with-coverage use istanbul to determine code coverage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -118,13 +110,8 @@ cd -
|
||||
|
||||
PATH=node_modules/.bin/:$PATH
|
||||
|
||||
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}
|
||||
else
|
||||
echo "Running tests"
|
||||
mocha -u tdd -t 5000 ${TESTS}
|
||||
fi
|
||||
echo "Running tests"
|
||||
mocha -t 10000 -u tdd ${MOCHA_OPTS} ${TESTS}
|
||||
ret=$?
|
||||
|
||||
cleanup
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
CAIRO_PKG_CONFIG=`pkg-config cairo --cflags-only-I 2> /dev/null`
|
||||
RESULT=$?
|
||||
|
||||
if [[ ${RESULT} -ne 0 ]]; then
|
||||
echo "###################################################################################"
|
||||
echo "# PREINSTALL HOOK ERROR #"
|
||||
echo "#---------------------------------------------------------------------------------#"
|
||||
echo "# #"
|
||||
echo "# node-canvas install error: some packages required by 'cairo' are not found #"
|
||||
echo "# #"
|
||||
echo -e "# Use '\033[1mmake all\033[0m', it will take care of common/known issues #"
|
||||
echo "# #"
|
||||
echo "# As an alternative try: #"
|
||||
echo "# Try to 'export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig' #"
|
||||
echo "# #"
|
||||
echo "# If problems persist visit: https://github.com/Automattic/node-canvas/wiki #"
|
||||
echo "# #"
|
||||
echo "###################################################################################"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig
|
||||
fi
|
||||
|
||||
npm install
|
||||
@@ -1,18 +0,0 @@
|
||||
if (process.argv.length !== 3) {
|
||||
console.error('Usage: node %s lzma_string', __filename);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var lzmaWorker = new LZMA();
|
||||
var lzmaInput = decodeURIComponent(process.argv[2]);
|
||||
var lzmaBuffer = new Buffer(lzmaInput, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128
|
||||
});
|
||||
|
||||
lzmaWorker.decompress(lzmaBuffer, function(result) {
|
||||
console.log(JSON.stringify(JSON.parse(JSON.parse(result).config), null, 4));
|
||||
});
|
||||
313
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
313
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
@@ -1,313 +0,0 @@
|
||||
require(__dirname + '/../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var redis = require('redis');
|
||||
var step = require('step');
|
||||
var FastlyPurge = require('fastly-purge');
|
||||
|
||||
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
|
||||
|
||||
|
||||
describe('templates surrogate keys', function() {
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
|
||||
// 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 fastlyConfig = global.environment.fastly;
|
||||
var FAKE_FASTLY_API_KEY = 'fastly-api-key';
|
||||
var FAKE_FASTLY_SERVICE_ID = 'fake-service-id';
|
||||
global.environment.fastly = {
|
||||
enabled: true,
|
||||
// the fastly api key
|
||||
apiKey: FAKE_FASTLY_API_KEY,
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: FAKE_FASTLY_SERVICE_ID
|
||||
};
|
||||
|
||||
var serverOptions = require('../../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var templateOwner = 'localhost',
|
||||
templateName = 'acceptance',
|
||||
expectedTemplateId = templateName,
|
||||
template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
expectedBody = { template_id: expectedTemplateId };
|
||||
|
||||
var varnishHttpUrl = [
|
||||
'http://', global.environment.varnish.host, ':', global.environment.varnish.http_port
|
||||
].join('');
|
||||
|
||||
var cacheEntryKey = new NamedMapsCacheEntry(templateOwner, templateName).key();
|
||||
var invalidationMatchHeader = '\\b' + cacheEntryKey + '\\b';
|
||||
var fastlyPurgePath = '/service/' + FAKE_FASTLY_SERVICE_ID + '/purge/' + encodeURIComponent(cacheEntryKey);
|
||||
|
||||
var nock = require('nock');
|
||||
nock.enableNetConnect(/(127.0.0.1:5555|cartocdn.com)/);
|
||||
|
||||
after(function(done) {
|
||||
serverOptions.varnish_purge_enabled = false;
|
||||
global.environment.varnish.host = varnishHost;
|
||||
global.environment.varnish.purge_enabled = varnishPurgeEnabled;
|
||||
|
||||
global.environment.fastly = fastlyConfig;
|
||||
|
||||
nock.restore();
|
||||
done();
|
||||
});
|
||||
|
||||
function createTemplate(callback) {
|
||||
var postTemplateRequest = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
|
||||
step(
|
||||
function postTemplate() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
postTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function rePostTemplate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it("invalidates surrogate keys on template update", function(done) {
|
||||
|
||||
var scope = nock(varnishHttpUrl)
|
||||
.intercept('/key', 'PURGE')
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(204, '');
|
||||
|
||||
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);
|
||||
},
|
||||
function putValidTemplate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var updateTemplateRequest = {
|
||||
url: '/api/v1/map/named/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
updateTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
|
||||
assert.equal(scope.pendingMocks().length, 0);
|
||||
assert.equal(fastlyScope.pendingMocks().length, 0);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.keys("map_*|localhost", function(err, keys) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.del(keys, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("invalidates surrogate on template deletion", function(done) {
|
||||
|
||||
var scope = nock(varnishHttpUrl)
|
||||
.intercept('/key', 'PURGE')
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(204, '');
|
||||
|
||||
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 createTemplateToDelete() {
|
||||
createTemplate(this);
|
||||
},
|
||||
function deleteValidTemplate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var deleteTemplateRequest = {
|
||||
url: '/api/v1/map/named/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
deleteTemplateRequest,
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
assert.equal(scope.pendingMocks().length, 0);
|
||||
assert.equal(fastlyScope.pendingMocks().length, 0);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should update template even if surrogate key invalidation fails", function(done) {
|
||||
|
||||
var scope = nock(varnishHttpUrl)
|
||||
.intercept('/key', 'PURGE')
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(503, '');
|
||||
|
||||
step(
|
||||
function createTemplateToUpdate() {
|
||||
createTemplate(this);
|
||||
},
|
||||
function putValidTemplate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var updateTemplateRequest = {
|
||||
url: '/api/v1/map/named/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
updateTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
|
||||
assert.equal(scope.pendingMocks().length, 0);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.keys("map_*|localhost", function(err, keys) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.del(keys, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
21
test/acceptance/cache_validator.js
Normal file
21
test/acceptance/cache_validator.js
Normal file
@@ -0,0 +1,21 @@
|
||||
var assert = require('../support/assert');
|
||||
require(__dirname + '/../support/test_helper');
|
||||
var CacheValidator = require(__dirname + '/../../lib/cartodb/cache_validator');
|
||||
|
||||
var VarnishEmu = require('../support/VarnishEmu');
|
||||
|
||||
suite('cache_validator', function() {
|
||||
|
||||
test('should call purge on varnish when invalidate database', function(done) {
|
||||
var varnish = new VarnishEmu(function(cmds) {
|
||||
assert.ok(cmds.length == 1);
|
||||
assert.equal('purge obj.http.X-Cache-Channel ~ \"^test_db:(.*test_cache.*)|(table)$\"\n', cmds[0].toString('utf8'));
|
||||
done();
|
||||
},
|
||||
function() {
|
||||
CacheValidator.init('localhost', 1337);
|
||||
CacheValidator.invalidate_db('test_db', 'test_cache');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
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 metadataBackend = {};
|
||||
var tilelive = {};
|
||||
var HealthCheck = require('../../lib/cartodb/monitoring/health_check');
|
||||
var healthCheck = new HealthCheck(metadataBackend, tilelive);
|
||||
|
||||
describe('health checks', function () {
|
||||
|
||||
function resetHealthConfig() {
|
||||
global.environment.health = {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
var healthCheckRequest = {
|
||||
url: '/health',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
it('returns 200 and ok=true with enabled configuration', function (done) {
|
||||
resetHealthConfig();
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(parsed.ok);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('error if disabled file exists', function(done) {
|
||||
var fs = require('fs');
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('not err if disabled file does not exists', function(done) {
|
||||
resetHealthConfig();
|
||||
|
||||
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.equal(parsed.enabled, true);
|
||||
assert.equal(parsed.ok, true);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,307 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
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;
|
||||
beforeEach(function() {
|
||||
server = new CartodbWindshaft(serverOptions());
|
||||
server.setMaxListeners(0);
|
||||
});
|
||||
|
||||
var keysToDelete = [];
|
||||
afterEach(function(done) {
|
||||
redisClient.DEL(keysToDelete, function() {
|
||||
keysToDelete = [];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var user = 'localhost';
|
||||
|
||||
var pointSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" 'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator, 1 cartodb_id";
|
||||
var pointCartoCss = '#layer { marker-fill:red; }';
|
||||
var polygonSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" ST_Buffer('SRID=3857;POINT(0 0)'::geometry, 100000000) the_geom_webmercator, 1 cartodb_id";
|
||||
var polygonCartoCss = '#layer { polygon-fill:red; }';
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost) {
|
||||
return {
|
||||
url: layergroupUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
function withRenderLimit(user, renderLimit, callback) {
|
||||
redisClient.SELECT(5, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var userLimitsKey = 'limits:tiler:' + user;
|
||||
redisClient.HSET(userLimitsKey, 'render', renderLimit, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
keysToDelete.push(userLimitsKey);
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategyEnabled;
|
||||
before(function() {
|
||||
onTileErrorStrategyEnabled = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategyEnabled;
|
||||
});
|
||||
|
||||
it("layergroup creation fails if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Render timed out' ] });
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation does not fail if user limit is high enough even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("layergroup creation works if test tile is fast but tile request fails if they are slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { error: 'Render timed out' });
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("tile request does not fail if user limit is high enough", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy', function() {
|
||||
|
||||
it("layergroup creation works even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation and tile requests works even if they are slow but returns fallback", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/render-timeout-fallback.png', 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,338 +0,0 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
|
||||
var redis = require('redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
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 wadusSql = 'select 1 as cartodb_id, null::geometry as the_geom_webmercator';
|
||||
var pointSql = "SELECT 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator, 1::int as cartodb_id";
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost, apiKey) {
|
||||
var url = layergroupUrl;
|
||||
if (apiKey) {
|
||||
url += '?api_key=' + apiKey;
|
||||
}
|
||||
return {
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost || 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
it("layergroup creation fails if CartoCSS is bogus", function(done) {
|
||||
var layergroup = singleLayergroupConfig(wadusSql, '#my_table3{');
|
||||
assert.response(server,
|
||||
createRequest(layergroup),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors[0].match(/^style0/));
|
||||
assert.ok(parsed.errors[0].match(/missing closing/));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("multiple bad styles returns 400 with all errors", function(done) {
|
||||
var layergroup = singleLayergroupConfig(wadusSql, '#my_table4{backgxxxxxround-color:#fff;foo:bar}');
|
||||
assert.response(server,
|
||||
createRequest(layergroup),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
assert.ok(parsed.errors[0].match(/^style0/));
|
||||
assert.ok(parsed.errors[0].match(/Unrecognized rule: backgxxxxxround-color/));
|
||||
assert.ok(parsed.errors[0].match(/Unrecognized rule: foo/));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Zoom is a special variable
|
||||
it("Specifying zoom level in CartoCSS does not need a 'zoom' variable in SQL output", function(done) {
|
||||
var layergroup = singleLayergroupConfig(pointSql, '#gadm4 [ zoom>=3] { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/88
|
||||
it("getting a tile from a user-specific database should return an expected tile", function(done) {
|
||||
var layergroup = singleLayergroupConfig(pointSql, '#layer { marker-fill:red; }');
|
||||
|
||||
var backupDBHost = global.environment.postgres.host;
|
||||
global.environment.postgres.host = '6.6.6.6';
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'cartodb250user'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
global.environment.postgres.host = backupDBHost;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/89
|
||||
it("getting a tile with a user-specific database password", function(done) {
|
||||
var layergroup = singleLayergroupConfig(pointSql, '#layer { marker-fill:red; }');
|
||||
|
||||
var backupDBPass = global.environment.postgres_auth_pass;
|
||||
global.environment.postgres_auth_pass = '<%= user_password %>';
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'cartodb250user', '4321'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
global.environment.postgres_auth_pass = backupDBPass;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("creating a layergroup from lzma param", function(done){
|
||||
var params = {
|
||||
config: JSON.stringify(singleLayergroupConfig(pointSql, '#layer { marker-fill:red; }'))
|
||||
};
|
||||
|
||||
testHelper.lzma_compress_to_base64(JSON.stringify(params), 1, function(err, lzma) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?lzma=' + encodeURIComponent(lzma),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("creating a layergroup from lzma param, invalid json input", function(done) {
|
||||
var params = {
|
||||
config: 'WADUS'
|
||||
};
|
||||
|
||||
testHelper.lzma_compress_to_base64(JSON.stringify(params), 1, function(err, lzma) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?lzma=' + encodeURIComponent(lzma),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Unexpected token W' ] });
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses queries postgresql to figure affected tables in query", function(done) {
|
||||
var tableName = 'gadm4';
|
||||
var expectedCacheChannel = _.template('<%= databaseName %>:public.<%= tableName %>', {
|
||||
databaseName: _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db',
|
||||
tableName: tableName
|
||||
});
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from ' + tableName, '#gadm4 { marker-fill: red; }');
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
|
||||
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("creates layergroup fails when postgresql queries fail to figure affected tables in query", function(done) {
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('fake error message'), [], callback);
|
||||
};
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from gadm4', '#gadm4 { marker-fill: red; }');
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
PgQueryRunner.prototype.run = runQueryFn;
|
||||
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["Error: could not fetch affected tables and last updated time: fake error message"]
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("tile requests works when postgresql queries fail to figure affected tables in query", function(done) {
|
||||
var layergroup = singleLayergroupConfig('select * from gadm4', '#gadm4 { marker-fill: red; }');
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
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 = {};
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
PgQueryRunner.prototype.run = runQueryFn;
|
||||
done();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,654 +0,0 @@
|
||||
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 server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
|
||||
var step = require('step');
|
||||
|
||||
describe('named_layers', 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) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for non-existing template name', function(done) {
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: 'nonexistent'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: ["Template 'nonexistent' of user 'localhost' not found"] });
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: tokenAuthTemplateName,
|
||||
config: {},
|
||||
auth_tokens: ['token1']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 403
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(
|
||||
parsedBody,
|
||||
{ errors: [ "Unauthorized 'auth_valid_template' template instantiation" ] }
|
||||
);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 200 and layergroup if properly authorized', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: tokenAuthTemplateName,
|
||||
config: {},
|
||||
auth_tokens: ['valid1']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 400 for nested named map layers', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: nestedNamedMapTemplateName
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: [ 'Nested named layers are not allowed' ] });
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 200 and layergroup with private tables', function(done) {
|
||||
|
||||
var privateTableTemplateName = 'private_table_template';
|
||||
var privateTableTemplate = {
|
||||
version: '0.0.1',
|
||||
name: privateTableTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from test_table_private_1',
|
||||
cartocss: '#layer { marker-fill: #cc3300; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: privateTableTemplateName
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createTemplate() {
|
||||
templateMaps.addTemplate(username, privateTableTemplate, this);
|
||||
},
|
||||
function createLayergroup(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupId + '/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, privateTableTemplate, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 200 and layergroup with private tables and interactivity', function(done) {
|
||||
|
||||
var privateTableTemplateNameInteractivity = 'private_table_template_interactivity';
|
||||
var privateTableTemplate = {
|
||||
"version": "0.0.1",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"name": privateTableTemplateNameInteractivity,
|
||||
"layergroup": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-fill: #cc3300; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"sql": "select * from test_table_private_1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: privateTableTemplateNameInteractivity
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createTemplate() {
|
||||
templateMaps.addTemplate(username, privateTableTemplate, this);
|
||||
},
|
||||
function createLayergroup(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupId + '/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, privateTableTemplate, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 403 when private table is accessed from non named layer', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from test_table_private_1',
|
||||
cartocss: '#layer { marker-fill: #cc3300; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 403
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.errors[0].match(/permission denied for relation test_table_private_1/));
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
|
||||
after(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,295 +0,0 @@
|
||||
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 server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
describe('get requests x-cache-channel', function() {
|
||||
|
||||
var statusOkResponse = {
|
||||
status: 200
|
||||
};
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.3.0',
|
||||
attributes: {
|
||||
id:'cartodb_id',
|
||||
columns: [
|
||||
'name',
|
||||
'address'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var layergroupRequest = {
|
||||
url: '/api/v1/map?config=' + encodeURIComponent(JSON.stringify(mapConfig)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
function getRequest(url, addApiKey, callbackName) {
|
||||
var params = {};
|
||||
if (!!addApiKey) {
|
||||
params.api_key = '1234';
|
||||
}
|
||||
if (!!callbackName) {
|
||||
params.callback = callbackName;
|
||||
}
|
||||
|
||||
return {
|
||||
url: url + '?' + qs.stringify(params),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validateXCacheChannel(done, expectedCacheChannel) {
|
||||
return function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(res.headers['x-cache-channel']);
|
||||
if (expectedCacheChannel) {
|
||||
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function noXCacheChannelHeader(done) {
|
||||
return function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
!res.headers['x-cache-channel'],
|
||||
'did not expect x-cache-channel header, got: `' + res.headers['x-cache-channel'] + '`'
|
||||
);
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function withLayergroupId(callback) {
|
||||
assert.response(
|
||||
server,
|
||||
layergroupRequest,
|
||||
statusOkResponse,
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, JSON.parse(res.body).layergroupid);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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')
|
||||
);
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0@2x.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik tiles', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:layer/:z/:x/:y.(:format) Per :layer rendering', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0/0.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:layer/attributes/:fid endpoint for info windows', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/attributes/1'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format static maps', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/static/center/' + layergroupId + '/0/0/0/400/300.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/static/bbox/:token/:bbox/:width/:height.:format static maps', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/static/bbox/' + layergroupId + '/-45,-45,45,45/400/300.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('header should NOT be present', function() {
|
||||
|
||||
it('/', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/version', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/version'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/health', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/health'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/api/v1/map/named list named maps', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named', true),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
describe('with named maps', function() {
|
||||
|
||||
var templateName = 'x_cache';
|
||||
|
||||
before(function(done) {
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: mapConfig
|
||||
};
|
||||
|
||||
var namedMapRequest = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
|
||||
assert.response(
|
||||
server,
|
||||
namedMapRequest,
|
||||
statusOkResponse,
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/named/' + templateName + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('/api/v1/map/named/:template_id Named map retrieval', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named/' + templateName, true),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/api/v1/map/named/:template_id/jsonp Named map retrieval', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named/' + templateName, true, 'cb'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
BIN
test/fixtures/render-timeout-fallback.png
vendored
BIN
test/fixtures/render-timeout-fallback.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -1,304 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection');
|
||||
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool }));
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
var wadusSql = 'select 1 wadusLayer, null::geometry the_geom_webmercator';
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: wadusSql,
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var wadusTemplateSql = 'select 1 wadusTemplateLayer, null::geometry the_geom_webmercator';
|
||||
var wadusTemplateLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: wadusTemplateSql,
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var wadusMapnikSql = 'select 1 wadusMapnikLayer, null::geometry the_geom_webmercator';
|
||||
var wadusMapnikLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: wadusMapnikSql,
|
||||
cartocss: '#layer { polygon-fill: <%= polygon_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: [
|
||||
wadusTemplateLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var multipleLayersTemplateName = 'multiple_valid_template';
|
||||
var multipleLayersTemplate = {
|
||||
version: '0.0.1',
|
||||
name: multipleLayersTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
"placeholders": {
|
||||
"polygon_color": {
|
||||
"type": "css_color",
|
||||
"default": "green"
|
||||
},
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusMapnikLayer,
|
||||
wadusTemplateLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
describe('named_layers datasources', function() {
|
||||
before(function(done) {
|
||||
templateMaps.addTemplate(username, template, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, multipleLayersTemplate, done);
|
||||
});
|
||||
});
|
||||
|
||||
function makeNamedMapLayerConfig(layers) {
|
||||
return {
|
||||
version: '1.3.0',
|
||||
layers: layers
|
||||
};
|
||||
}
|
||||
|
||||
var simpleNamedLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName
|
||||
}
|
||||
};
|
||||
|
||||
var multipleLayersNamedLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: multipleLayersTemplateName,
|
||||
auth_tokens: ['valid2']
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'without datasource for non-named layers',
|
||||
config: makeNamedMapLayerConfig([wadusLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 1);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'with datasource for the named layer but not for the normal',
|
||||
config: makeNamedMapLayerConfig([wadusLayer, simpleNamedLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 2);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.sql, wadusTemplateSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'with datasource for the multiple layers in the named but not for the normal',
|
||||
config: makeNamedMapLayerConfig([wadusLayer, multipleLayersNamedLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 3);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'mapnik');
|
||||
assert.equal(layers[1].options.sql, wadusMapnikSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[2].type, 'cartodb');
|
||||
assert.equal(layers[2].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(2);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'all with datasource because all are named',
|
||||
config: makeNamedMapLayerConfig([multipleLayersNamedLayer, simpleNamedLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 3);
|
||||
|
||||
assert.equal(layers[0].type, 'mapnik');
|
||||
assert.equal(layers[0].options.sql, wadusMapnikSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(0);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[2].type, 'cartodb');
|
||||
assert.equal(layers[2].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(2);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'with a mix of datasource and no datasource depending if layers are named or not',
|
||||
config: makeNamedMapLayerConfig([
|
||||
simpleNamedLayer,
|
||||
multipleLayersNamedLayer,
|
||||
wadusLayer,
|
||||
simpleNamedLayer,
|
||||
wadusLayer,
|
||||
multipleLayersNamedLayer
|
||||
]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 8);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusTemplateSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(0);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[1].type, 'mapnik');
|
||||
assert.equal(layers[1].options.sql, wadusMapnikSql);
|
||||
layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[2].type, 'cartodb');
|
||||
assert.equal(layers[2].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(2);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[3].type, 'cartodb');
|
||||
assert.equal(layers[3].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(3), undefined);
|
||||
|
||||
assert.equal(layers[4].type, 'cartodb');
|
||||
assert.equal(layers[4].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(4);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[5].type, 'cartodb');
|
||||
assert.equal(layers[5].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(5), undefined);
|
||||
|
||||
assert.equal(layers[6].type, 'mapnik');
|
||||
assert.equal(layers[6].options.sql, wadusMapnikSql);
|
||||
layerDatasource = datasource.getLayerDatasource(6);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[7].type, 'cartodb');
|
||||
assert.equal(layers[7].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(7);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
it('should return a list of layers ' + testScenario.desc, function(done) {
|
||||
mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
testScenario.test(err, layers, datasource, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
templateMaps.delTemplate(username, templateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,327 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection');
|
||||
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');
|
||||
|
||||
describe('mapconfig_named_layers_adapter', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool }));
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
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 wadusMapnikLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill: <%= polygon_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']
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var multipleLayersTemplateName = 'multiple_valid_template';
|
||||
var multipleLayersTemplate = {
|
||||
version: '0.0.1',
|
||||
name: multipleLayersTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
"placeholders": {
|
||||
"polygon_color": {
|
||||
"type": "css_color",
|
||||
"default": "green"
|
||||
},
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusMapnikLayer,
|
||||
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
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function makeNamedMapLayerConfig(options) {
|
||||
return {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: options
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
before(function(done) {
|
||||
templateMaps.addTemplate(username, template, done);
|
||||
});
|
||||
|
||||
it('should fail for named map layer with missing name', function(done) {
|
||||
var missingNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
config: {}
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(err.message, 'Missing Named Map `name` in layer options');
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail for non-existing template name', function(done) {
|
||||
var missingTemplateName = 'wadus';
|
||||
var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: missingTemplateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(
|
||||
err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found"
|
||||
);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if not properly authorized', function(done) {
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: tokenAuthTemplateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation");
|
||||
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for nested named map layers', function(done) {
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: nestedNamedMapTemplateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(err.message, 'Nested named layers are not allowed');
|
||||
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an expanded list of layers for a named map layer', function(done) {
|
||||
var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: templateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.ok(layers.length, 1);
|
||||
assert.ok(layers[0].type, 'cartodb');
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return on auth=token with valid tokens provided', function(done) {
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var validAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: tokenAuthTemplateName,
|
||||
auth_tokens: ['valid1']
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 1);
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an expanded list of layers for a named map layer, multiple layers version', function(done) {
|
||||
templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: multipleLayersTemplateName,
|
||||
auth_tokens: ['valid2']
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 2);
|
||||
|
||||
assert.equal(layers[0].type, 'mapnik');
|
||||
assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(1), undefined);
|
||||
|
||||
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace template params with the given config', function(done) {
|
||||
templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var color = '#cc3300',
|
||||
polygonColor = '#ff9900';
|
||||
|
||||
var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: multipleLayersTemplateName,
|
||||
config: {
|
||||
polygon_color: polygonColor,
|
||||
color: color
|
||||
},
|
||||
auth_tokens: ['valid2']
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 2);
|
||||
|
||||
assert.equal(layers[0].type, 'mapnik');
|
||||
assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(1), undefined);
|
||||
|
||||
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
templateMaps.delTemplate(username, templateName, done);
|
||||
});
|
||||
});
|
||||
83
test/support/SQLAPIEmu.js
Normal file
83
test/support/SQLAPIEmu.js
Normal file
@@ -0,0 +1,83 @@
|
||||
var http = require('http');
|
||||
var url = require('url');
|
||||
var _ = require('underscore');
|
||||
|
||||
var o = function(port, cb) {
|
||||
|
||||
this.queries = [];
|
||||
var that = this;
|
||||
this.requests = [];
|
||||
|
||||
this.sqlapi_server = http.createServer(function(req,res) {
|
||||
//console.log("server got request with method " + req.method);
|
||||
var query;
|
||||
|
||||
that.requests.push(req);
|
||||
|
||||
if ( req.method == 'GET' ) {
|
||||
query = url.parse(req.url, true).query;
|
||||
that.handleQuery(query, res);
|
||||
}
|
||||
else if ( req.method == 'POST') {
|
||||
var data = '';
|
||||
req.on('data', function(chunk) {
|
||||
//console.log("GOT Chunk " + chunk);
|
||||
data += chunk;
|
||||
});
|
||||
req.on('end', function() {
|
||||
//console.log("Data is: "); console.dir(data);
|
||||
query = JSON.parse(data);
|
||||
//console.log("handleQuery is " + that.handleQuery);
|
||||
that.handleQuery(query, res);
|
||||
});
|
||||
}
|
||||
else {
|
||||
that.handleQuery('SQLAPIEmu does not support method' + req.method, res);
|
||||
}
|
||||
}).listen(port, cb);
|
||||
};
|
||||
|
||||
o.prototype.handleQuery = function(query, res) {
|
||||
this.queries.push(query);
|
||||
if ( query.q.match('SQLAPIERROR') ) {
|
||||
res.statusCode = 400;
|
||||
res.write(JSON.stringify({'error':'Some error occurred'}));
|
||||
} else if ( query.q.match('SQLAPINOANSWER') ) {
|
||||
console.log("SQLAPIEmulator will never respond, on request");
|
||||
return;
|
||||
} else if ( query.q.match('EPOCH.* as max') ) {
|
||||
// This is the structure of the known query sent by tiler
|
||||
var row = {
|
||||
'max': 1234567890.123
|
||||
};
|
||||
res.write(JSON.stringify({rows: [ row ]}));
|
||||
} else {
|
||||
if ( query.q.match('_private_') && query.api_key === undefined) {
|
||||
res.statusCode = 403;
|
||||
res.write(JSON.stringify({'error':'forbidden: ' + JSON.stringify(query)}));
|
||||
} else {
|
||||
var qs = JSON.stringify(query);
|
||||
var row = {
|
||||
// This is the structure of the known query sent by tiler
|
||||
'cdb_querytables': '{' + qs + '}',
|
||||
'max': qs
|
||||
};
|
||||
var out_obj = {rows: [ row ]};
|
||||
var out = JSON.stringify(out_obj);
|
||||
res.write(out);
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
};
|
||||
|
||||
|
||||
o.prototype.close = function(cb) {
|
||||
this.sqlapi_server.close(cb);
|
||||
};
|
||||
|
||||
o.prototype.getLastRequest = function() {
|
||||
return this.requests.pop();
|
||||
};
|
||||
|
||||
module.exports = o;
|
||||
|
||||
25
test/support/VarnishEmu.js
Normal file
25
test/support/VarnishEmu.js
Normal file
@@ -0,0 +1,25 @@
|
||||
var net = require('net');
|
||||
|
||||
module.exports = function(on_cmd_recieved, test_callback) {
|
||||
var self = this;
|
||||
var welcome_msg = 'hi, im a varnish emu, right?';
|
||||
|
||||
self.commands_recieved = [];
|
||||
|
||||
var server = net.createServer(function (socket) {
|
||||
var command = '';
|
||||
socket.write("200 " + welcome_msg.length + "\n");
|
||||
socket.write(welcome_msg);
|
||||
socket.on('data', function(data) {
|
||||
self.commands_recieved.push(data);
|
||||
on_cmd_recieved && on_cmd_recieved(self.commands_recieved);
|
||||
socket.write('200 0\n');
|
||||
});
|
||||
});
|
||||
server.listen(1337, "127.0.0.1");
|
||||
|
||||
server.on('listening', function(){
|
||||
test_callback();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Cribbed from the ever prolific Konstantin Kaefer
|
||||
// https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js
|
||||
|
||||
var exec = require('child_process').exec,
|
||||
fs = require('fs'),
|
||||
http = require('http'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
var path = require('path');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var assert = module.exports = exports = require('assert');
|
||||
|
||||
@@ -67,51 +66,35 @@ assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
|
||||
callback(err);
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an image data as an input and an image path and compare them using ImageMagick fuzz algorithm, if case the
|
||||
* similarity is not within the tolerance limit it will callback with an error.
|
||||
*
|
||||
* @param buffer The image data to compare from
|
||||
* @param {string} referenceImageRelativeFilePath The relative file to compare against
|
||||
* @param {number} tolerance tolerated mean color distance, as a per mil (‰)
|
||||
* @param {function} callback Will call to home with null in case there is no error, otherwise with the error itself
|
||||
* @see FUZZY in http://www.imagemagick.org/script/command-line-options.php#metric
|
||||
*/
|
||||
assert.imageEqualsFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
//
|
||||
// @param tol tolerated color distance as a percent over max channel value
|
||||
// by default this is zero. For meaningful values, see
|
||||
// http://www.imagemagick.org/script/command-line-options.php#metric
|
||||
//
|
||||
assert.imageEqualsFile = function(buffer, file_b, tol, callback) {
|
||||
if (!callback) callback = function(err) { if (err) throw err; };
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath),
|
||||
testImageFilePath = '/tmp/windshaft-test-image-' + (Math.random() * 1e16); // TODO: make predictable
|
||||
var err = fs.writeFileSync(testImageFilePath, buffer, 'binary');
|
||||
file_b = path.resolve(file_b);
|
||||
var file_a = '/tmp/windshaft-test-image-test.png'; // + (Math.random() * 1e16); // TODO: make predictable
|
||||
var err = fs.writeFileSync(file_a, buffer, 'binary');
|
||||
if (err) throw err;
|
||||
|
||||
var imageMagickCmd = util.format(
|
||||
'compare -metric fuzz "%s" "%s" /dev/null',
|
||||
testImageFilePath, referenceImageFilePath
|
||||
);
|
||||
|
||||
exec(imageMagickCmd, function(err, stdout, stderr) {
|
||||
var fuzz = tol + '%';
|
||||
exec('compare -fuzz ' + fuzz + ' -metric AE "' + file_a + '" "' +
|
||||
file_b + '" /dev/null', function(err, stdout, stderr) {
|
||||
if (err) {
|
||||
fs.unlinkSync(testImageFilePath);
|
||||
fs.unlinkSync(file_a);
|
||||
callback(err);
|
||||
} else {
|
||||
stderr = stderr.trim();
|
||||
var metrics = stderr.match(/([0-9]*) \((.*)\)/);
|
||||
if ( ! metrics ) {
|
||||
callback(new Error("No match for " + stderr));
|
||||
return;
|
||||
}
|
||||
var similarity = parseFloat(metrics[2]),
|
||||
tolerancePerMil = (tolerance / 1000);
|
||||
if (similarity > tolerancePerMil) {
|
||||
err = new Error(util.format(
|
||||
'Images %s and %s are not equal (got %d similarity, expected %d)',
|
||||
testImageFilePath, referenceImageFilePath, similarity, tolerancePerMil)
|
||||
);
|
||||
err.similarity = similarity;
|
||||
callback(err);
|
||||
var similarity = parseFloat(stderr);
|
||||
if ( similarity > 0 ) {
|
||||
var err = new Error('Images not equal(' + similarity + '): ' +
|
||||
file_a + ' ' + file_b);
|
||||
err.similarity = similarity;
|
||||
callback(err);
|
||||
} else {
|
||||
fs.unlinkSync(testImageFilePath);
|
||||
callback(null);
|
||||
fs.unlinkSync(file_a);
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -226,8 +209,6 @@ assert.response = function(server, req, res, msg){
|
||||
response.on('end', function(){
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
check();
|
||||
|
||||
// Assert response body
|
||||
if (res.body !== undefined) {
|
||||
var eql = res.body instanceof RegExp
|
||||
@@ -246,10 +227,9 @@ assert.response = function(server, req, res, msg){
|
||||
assert.equal(
|
||||
response.statusCode,
|
||||
status,
|
||||
msg + colorize('Invalid response status code.\n'
|
||||
msg + 'Invalid response status code.\n'
|
||||
+ ' Expected: [green]{' + status + '}\n'
|
||||
+ ' Got: [red]{' + response.statusCode + '}\n'
|
||||
+ ' Response body: ' + response.body)
|
||||
+ ' Got: [red]{' + response.statusCode + '}'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -274,6 +254,7 @@ assert.response = function(server, req, res, msg){
|
||||
|
||||
// Callback
|
||||
callback(response);
|
||||
check();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,16 +262,3 @@ assert.response = function(server, req, res, msg){
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Colorize the given string using ansi-escape sequences.
|
||||
* Disabled when --boring is set.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {String}
|
||||
*/
|
||||
function colorize(str) {
|
||||
var colors = { bold: 1, red: 31, green: 32, yellow: 33 };
|
||||
return str.replace(/\[(\w+)\]\{([^]*?)\}/g, function(_, color, str) {
|
||||
return '\x1B[' + colors[color] + 'm' + str + '\x1B[0m';
|
||||
});
|
||||
}
|
||||
@@ -76,13 +76,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
sed "s/:PUBLICPASS/${PUBLICPASS}/" |
|
||||
sed "s/:TESTUSER/${TESTUSER}/" |
|
||||
sed "s/:TESTPASS/${TESTPASS}/" |
|
||||
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
|
||||
|
||||
psql -c "CREATE EXTENSION plpythonu;" ${TEST_DB}
|
||||
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql
|
||||
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql
|
||||
cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql |
|
||||
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
|
||||
psql ${TEST_DB}
|
||||
|
||||
fi
|
||||
|
||||
@@ -92,8 +86,7 @@ if test x"$PREPARE_REDIS" = xyes; then
|
||||
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET rails:users:localhost id ${TESTUSERID} \
|
||||
database_name "${TEST_DB}" \
|
||||
database_host localhost \
|
||||
database_name '${TEST_DB}' \
|
||||
map_key 1234
|
||||
SADD rails:users:localhost:map_key 1235
|
||||
EOF
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- Return an array of statements found in the given query text
|
||||
--
|
||||
-- Regexp curtesy of Hubert Lubaczewski (depesz)
|
||||
-- Implemented in plpython for performance reasons
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION CDB_QueryStatements(query text)
|
||||
RETURNS SETOF TEXT AS $$
|
||||
import re
|
||||
pat = re.compile( r'''((?:[^'"$;]+|"[^"]*"|'[^']*'|(\$[^$]*\$).*?\2)+)''', re.DOTALL )
|
||||
for match in pat.findall(query):
|
||||
cleaned = match[0].strip()
|
||||
if ( cleaned ):
|
||||
yield cleaned
|
||||
$$ language 'plpythonu' IMMUTABLE STRICT;
|
||||
@@ -1,67 +0,0 @@
|
||||
-- Return an array of table names scanned by a given query
|
||||
--
|
||||
-- Requires PostgreSQL 9.x+
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION CDB_QueryTables(query text)
|
||||
RETURNS name[]
|
||||
AS $$
|
||||
DECLARE
|
||||
exp XML;
|
||||
tables NAME[];
|
||||
rec RECORD;
|
||||
rec2 RECORD;
|
||||
BEGIN
|
||||
|
||||
tables := '{}';
|
||||
|
||||
FOR rec IN SELECT CDB_QueryStatements(query) q LOOP
|
||||
|
||||
IF NOT ( rec.q ilike 'select%' or rec.q ilike 'with%' ) THEN
|
||||
--RAISE WARNING 'Skipping %', rec.q;
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
EXECUTE 'EXPLAIN (FORMAT XML, VERBOSE) ' || rec.q INTO STRICT exp;
|
||||
EXCEPTION WHEN others THEN
|
||||
-- TODO: if error is 'relation "xxxxxx" does not exist', take xxxxxx as
|
||||
-- the affected table ?
|
||||
RAISE WARNING 'CDB_QueryTables cannot explain query: % (%: %)', rec.q, SQLSTATE, SQLERRM;
|
||||
RAISE EXCEPTION '%', SQLERRM;
|
||||
CONTINUE;
|
||||
END;
|
||||
|
||||
-- Now need to extract all values of <Relation-Name>
|
||||
|
||||
-- RAISE DEBUG 'Explain: %', exp;
|
||||
|
||||
FOR rec2 IN WITH
|
||||
inp AS (
|
||||
SELECT
|
||||
xpath('//x:Relation-Name/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as x,
|
||||
xpath('//x:Relation-Name/../x:Schema/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as s
|
||||
)
|
||||
SELECT unnest(x)::name as p, unnest(s)::name as sc from inp
|
||||
LOOP
|
||||
-- RAISE DEBUG 'tab: %', rec2.p;
|
||||
-- RAISE DEBUG 'sc: %', rec2.sc;
|
||||
tables := array_append(tables, (rec2.sc || '.' || rec2.p)::name);
|
||||
END LOOP;
|
||||
|
||||
-- RAISE DEBUG 'Tables: %', tables;
|
||||
|
||||
END LOOP;
|
||||
|
||||
-- RAISE DEBUG 'Tables: %', tables;
|
||||
|
||||
-- Remove duplicates and sort by name
|
||||
IF array_upper(tables, 1) > 0 THEN
|
||||
WITH dist as ( SELECT DISTINCT unnest(tables)::text as p ORDER BY p )
|
||||
SELECT array_agg(p) from dist into tables;
|
||||
END IF;
|
||||
|
||||
--RAISE DEBUG 'Tables: %', tables;
|
||||
|
||||
return tables;
|
||||
END
|
||||
$$ LANGUAGE 'plpgsql' VOLATILE STRICT;
|
||||
@@ -177,15 +177,3 @@ CREATE TABLE test_table_private_1 (
|
||||
INSERT INTO test_table_private_1 SELECT * from test_table;
|
||||
|
||||
GRANT ALL ON TABLE test_table_private_1 TO :TESTUSER;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
CDB_TableMetadata (
|
||||
tabname regclass not null primary key,
|
||||
updated_at timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table'::regclass, '2009-02-13T23:31:30.123Z');
|
||||
INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table_private_1'::regclass, '2009-02-13T23:31:30.123Z');
|
||||
|
||||
-- GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER;
|
||||
GRANT SELECT ON CDB_TableMetadata TO :TESTUSER;
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var LZMA = require('lzma').LZMA;
|
||||
|
||||
var lzmaWorker = new LZMA();
|
||||
var LZMA = require('lzma/lzma_worker.js').LZMA;
|
||||
|
||||
// set environment specific variables
|
||||
global.settings = require(__dirname + '/../../config/settings');
|
||||
global.environment = require(__dirname + '/../../config/environments/test');
|
||||
_.extend(global.settings, global.environment);
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
|
||||
// Utility function to compress & encode LZMA
|
||||
function lzma_compress_to_base64(payload, mode, callback) {
|
||||
lzmaWorker.compress(payload, mode,
|
||||
LZMA.compress(payload, mode,
|
||||
function(ints) {
|
||||
ints = ints.map(function(c) { return String.fromCharCode(c + 128) }).join('')
|
||||
var base64 = new Buffer(ints, 'binary').toString('base64');
|
||||
@@ -39,27 +39,8 @@ function checkNoCache(res) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that the response headers do not request caching
|
||||
* @see checkNoCache
|
||||
* @param res
|
||||
*/
|
||||
function checkCache(res) {
|
||||
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
|
||||
assert.ok(res.headers.hasOwnProperty('cache-control'));
|
||||
assert.ok(res.headers.hasOwnProperty('last-modified'));
|
||||
}
|
||||
|
||||
function checkSurrogateKey(res, expectedKey) {
|
||||
assert.ok(res.headers.hasOwnProperty('surrogate-key'));
|
||||
assert.equal(res.headers['surrogate-key'], expectedKey);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
lzma_compress_to_base64: lzma_compress_to_base64,
|
||||
checkNoCache: checkNoCache,
|
||||
checkSurrogateKey: checkSurrogateKey,
|
||||
checkCache: checkCache
|
||||
};
|
||||
checkNoCache: checkNoCache
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../../../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
suite('cache named_maps_entry', function() {
|
||||
|
||||
var namedMapOwner = 'foo',
|
||||
namedMapName = 'wadus_name',
|
||||
namedMapsCacheEntry = new NamedMapsCacheEntry(namedMapOwner, namedMapName),
|
||||
entryKey = namedMapsCacheEntry.key();
|
||||
|
||||
test('key is a string', function() {
|
||||
assert.ok(_.isString(entryKey));
|
||||
});
|
||||
|
||||
test('key is 8 chars length', function() {
|
||||
assert.equal(entryKey.length, 8);
|
||||
var entryKeyParts = entryKey.split(':');
|
||||
assert.equal(entryKeyParts.length, 2);
|
||||
assert.equal(entryKeyParts[0], 'n');
|
||||
});
|
||||
|
||||
test('key is name spaced for named maps', function() {
|
||||
var entryKeyParts = entryKey.split(':');
|
||||
assert.equal(entryKeyParts.length, 2);
|
||||
assert.equal(entryKeyParts[0], 'n');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
var assert = require('assert');
|
||||
|
||||
var CdbRequest = require('../../../lib/cartodb/models/cdb_request');
|
||||
|
||||
describe('req2params', function() {
|
||||
|
||||
function createRequest(host, userParam) {
|
||||
var req = {
|
||||
params: {},
|
||||
headers: {
|
||||
host: host
|
||||
}
|
||||
};
|
||||
|
||||
if (userParam) {
|
||||
req.params.user = userParam;
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
it('extracts name from host header', function() {
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('localhost'));
|
||||
|
||||
assert.equal(user, 'localhost');
|
||||
});
|
||||
|
||||
it('extracts name from subdomain host header in case of no config', function() {
|
||||
var userFromHostConfig = global.environment.user_from_host;
|
||||
global.environment.user_from_host = null;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('development.localhost.lan'));
|
||||
|
||||
global.environment.user_from_host = userFromHostConfig;
|
||||
|
||||
assert.equal(user, 'development');
|
||||
});
|
||||
|
||||
it('considers user param before headers', function() {
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('localhost', 'development'));
|
||||
|
||||
assert.equal(user, 'development');
|
||||
});
|
||||
|
||||
it('returns undefined when it cannot extract username', function() {
|
||||
var userFromHostConfig = global.environment.user_from_host;
|
||||
global.environment.user_from_host = null;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('localhost'));
|
||||
|
||||
global.environment.user_from_host = userFromHostConfig;
|
||||
|
||||
assert.equal(user, undefined);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var assert = require('assert')
|
||||
, _ = require('underscore')
|
||||
, redis = require('redis')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, tests = module.exports = {};
|
||||
|
||||
suite('req2params', function() {
|
||||
|
||||
@@ -22,7 +24,7 @@ suite('req2params', function() {
|
||||
assert.ok(_.isObject(req.query), 'request has query');
|
||||
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
|
||||
assert.ok(req.hasOwnProperty('params'), 'request has params');
|
||||
assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
|
||||
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
|
||||
assert.equal(req.params.dbname, test_database, 'could forge dbname: '+ req.params.dbname);
|
||||
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
|
||||
done();
|
||||
@@ -36,7 +38,7 @@ suite('req2params', function() {
|
||||
assert.ok(_.isObject(req.query), 'request has query');
|
||||
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
|
||||
assert.ok(req.hasOwnProperty('params'), 'request has params');
|
||||
assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
|
||||
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
|
||||
assert.equal(req.params.dbname, test_database);
|
||||
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
|
||||
done();
|
||||
@@ -50,7 +52,7 @@ suite('req2params', function() {
|
||||
assert.ok(_.isObject(req.query), 'request has query');
|
||||
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
|
||||
assert.ok(req.hasOwnProperty('params'), 'request has params');
|
||||
assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
|
||||
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
|
||||
assert.equal(req.params.dbname, test_database);
|
||||
assert.equal(req.params.dbuser, test_user);
|
||||
|
||||
@@ -63,34 +65,23 @@ suite('req2params', function() {
|
||||
});
|
||||
|
||||
test('it should extend params with decoded lzma', function(done) {
|
||||
var qo = {
|
||||
config: {
|
||||
version: '1.3.0'
|
||||
}
|
||||
};
|
||||
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
var req = {
|
||||
headers: {
|
||||
host:'localhost'
|
||||
},
|
||||
query: {
|
||||
non_included: 'toberemoved',
|
||||
api_key: 'test',
|
||||
style: 'override',
|
||||
lzma: data
|
||||
}
|
||||
};
|
||||
opts.req2params(req, function(err, req) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
var query = req.params;
|
||||
assert.deepEqual(qo.config, query.config);
|
||||
assert.equal('test', query.api_key);
|
||||
assert.equal(undefined, query.non_included);
|
||||
done();
|
||||
});
|
||||
var qo = {
|
||||
style: 'test',
|
||||
style_version: '2.1.0',
|
||||
cache_buster: 5
|
||||
};
|
||||
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
opts.req2params({ headers: { host:'localhost' }, query: { non_included: 'toberemoved', api_key: 'test', style: 'override', lzma: data }}, function(err, req) {
|
||||
if ( err ) { done(err); return; }
|
||||
var query = req.params
|
||||
assert.equal(qo.style, query.style)
|
||||
assert.equal(qo.style_version, query.style_version)
|
||||
assert.equal(qo.cache_buster, query.cache_buster)
|
||||
assert.equal('test', query.api_key)
|
||||
assert.equal(undefined, query.non_included)
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
109
test/unit/cartodb/signed_maps.test.js
Normal file
109
test/unit/cartodb/signed_maps.test.js
Normal file
@@ -0,0 +1,109 @@
|
||||
var assert = require('assert')
|
||||
//, _ = require('underscore')
|
||||
, RedisPool = require('redis-mpool')
|
||||
, SignedMaps = require('../../../lib/cartodb/signed_maps.js')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, Step = require('step')
|
||||
, tests = module.exports = {};
|
||||
|
||||
suite('signed_maps', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redis_pool = RedisPool(global.environment.redis);
|
||||
|
||||
test('can sign map with open and token-based auth', function(done) {
|
||||
var smap = new SignedMaps(redis_pool);
|
||||
assert.ok(smap);
|
||||
var sig = 'sig1';
|
||||
var map = 'map1';
|
||||
var tok = 'tok1';
|
||||
var crt = {
|
||||
version:'0.0.1',
|
||||
layergroup_id:map,
|
||||
auth: {}
|
||||
};
|
||||
var crt1_id; // by token
|
||||
var crt2_id; // open
|
||||
Step(
|
||||
function() {
|
||||
smap.isAuthorized(sig,map,tok,this);
|
||||
},
|
||||
function checkAuthFailure1(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(!authorized, "unexpectedly authorized");
|
||||
crt.auth.method = 'token';
|
||||
crt.auth.valid_tokens = [tok];
|
||||
smap.addSignature(sig, map, crt, this)
|
||||
},
|
||||
function getCert1(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id, "undefined signature id");
|
||||
crt1_id = id; // keep note of it
|
||||
//console.log("Certificate 1 is " + crt1_id);
|
||||
smap.isAuthorized(sig,map,'',this);
|
||||
},
|
||||
function checkAuthFailure2(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(!authorized, "unexpectedly authorized");
|
||||
smap.isAuthorized(sig,map,tok,this);
|
||||
},
|
||||
function checkAuthSuccess1(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(authorized, "unauthorized :(");
|
||||
crt.auth.method = 'open';
|
||||
delete crt.auth.valid_tokens;
|
||||
smap.addSignature(sig, map, crt, this)
|
||||
},
|
||||
function getCert2(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id, "undefined signature id");
|
||||
crt2_id = id; // keep note of it
|
||||
//console.log("Certificate 2 is " + crt2_id);
|
||||
smap.isAuthorized(sig,map,'arbitrary',this);
|
||||
},
|
||||
function checkAuthSuccess2_delCert2(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(authorized, "unauthorized :(");
|
||||
var next = this;
|
||||
smap.delCertificate(sig, crt2_id, function(e) {
|
||||
if (e) next(e);
|
||||
else smap.isAuthorized(sig,map,'arbitrary',next);
|
||||
});
|
||||
},
|
||||
function checkAuthFailure3_delCert2(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(!authorized, "unexpectedly authorized");
|
||||
smap.delCertificate(sig, crt1_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('can validate certificates', function(done) {
|
||||
var smap = new SignedMaps(redis_pool);
|
||||
assert.ok(smap);
|
||||
Step(
|
||||
function invalidVersion() {
|
||||
var cert = { version: '-1' };
|
||||
var err = smap.checkInvalidCertificate(cert);
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, "Unsupported certificate version -1");
|
||||
return null;
|
||||
},
|
||||
function invalidTokenAuth() {
|
||||
var cert = { version: '0.0.1', auth: { method:'token', valid_token:[] } };
|
||||
var err = smap.checkInvalidCertificate(cert);
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, "Invalid 'token' authentication: missing valid_tokens");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,30 +1,24 @@
|
||||
require('../../support/test_helper');
|
||||
var assert = require('assert')
|
||||
//, _ = require('underscore')
|
||||
, RedisPool = require('redis-mpool')
|
||||
, SignedMaps = require('../../../lib/cartodb/signed_maps.js')
|
||||
, TemplateMaps = require('../../../lib/cartodb/template_maps.js')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, Step = require('step')
|
||||
, tests = module.exports = {};
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('template_maps', function() {
|
||||
suite('template_maps', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redis_pool = new RedisPool(global.environment.redis);
|
||||
|
||||
var wadusLayer = {
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
it('does not accept template with unsupported version', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
var redis_pool = RedisPool(global.environment.redis);
|
||||
var signed_maps = new SignedMaps(redis_pool);
|
||||
|
||||
test('does not accept template with unsupported version', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'6.6.6',
|
||||
name:'k', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
step(
|
||||
name:'k', auth: {}, layergroup: {} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
@@ -39,12 +33,12 @@ describe('template_maps', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not accept template with missing name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('does not accept template with missing name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
step(
|
||||
auth: {}, layergroup: {} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
@@ -59,11 +53,11 @@ describe('template_maps', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not accept template with invalid name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('does not accept template with invalid name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
auth: {}, layergroup: {} };
|
||||
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
|
||||
var testNext = function() {
|
||||
if ( ! invalidnames.length ) { done(); return; }
|
||||
@@ -74,7 +68,8 @@ describe('template_maps', function() {
|
||||
done(new Error("Unexpected success with invalid name '" + n + "'"));
|
||||
}
|
||||
else if ( ! err.message.match(/template.*name/i) ) {
|
||||
done(new Error("Unexpected error message with invalid name '" + n + "': " + err));
|
||||
done(new Error("Unexpected error message with invalid name '" + n
|
||||
+ "': " + err));
|
||||
}
|
||||
else {
|
||||
testNext();
|
||||
@@ -84,12 +79,12 @@ describe('template_maps', function() {
|
||||
testNext();
|
||||
});
|
||||
|
||||
it('does not accept template with invalid placeholder name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('does not accept template with invalid placeholder name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: {},
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
auth: {}, layergroup: {} };
|
||||
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
|
||||
var testNext = function() {
|
||||
if ( ! invalidnames.length ) { done(); return; }
|
||||
@@ -101,7 +96,8 @@ describe('template_maps', function() {
|
||||
done(new Error("Unexpected success with invalid name '" + n + "'"));
|
||||
}
|
||||
else if ( ! err.message.match(/invalid.*name/i) ) {
|
||||
done(new Error("Unexpected error message with invalid name '" + n + "': " + err));
|
||||
done(new Error("Unexpected error message with invalid name '" + n
|
||||
+ "': " + err));
|
||||
}
|
||||
else {
|
||||
testNext();
|
||||
@@ -111,18 +107,19 @@ describe('template_maps', function() {
|
||||
testNext();
|
||||
});
|
||||
|
||||
it('does not accept template with missing placeholder default', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('does not accept template with missing placeholder default', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: { v: {} },
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
auth: {}, layergroup: {} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with missing placeholder default"));
|
||||
}
|
||||
else if ( ! err.message.match(/missing default/i) ) {
|
||||
done(new Error("Unexpected error message with missing placeholder default: " + err));
|
||||
done(new Error("Unexpected error message with missing placeholder default: "
|
||||
+ err));
|
||||
}
|
||||
else {
|
||||
done();
|
||||
@@ -130,18 +127,19 @@ describe('template_maps', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not accept template with missing placeholder type', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('does not accept template with missing placeholder type', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: { v: { default:1 } },
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
auth: {}, layergroup: {} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with missing placeholder type"));
|
||||
}
|
||||
else if ( ! err.message.match(/missing type/i) ) {
|
||||
done(new Error("Unexpected error message with missing placeholder default: " + err));
|
||||
done(new Error("Unexpected error message with missing placeholder default: "
|
||||
+ err));
|
||||
}
|
||||
else {
|
||||
done();
|
||||
@@ -150,19 +148,20 @@ describe('template_maps', function() {
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/128
|
||||
it('does not accept template with invalid token auth (undefined tokens)',
|
||||
test('does not accept template with invalid token auth (undefined tokens)',
|
||||
function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "invalid_auth1", placeholders: { },
|
||||
auth: { method: 'token' }, layergroup: {layers:[wadusLayer]} };
|
||||
auth: { method: 'token' }, layergroup: {} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with invalid token auth (undefined tokens)"));
|
||||
}
|
||||
else if ( ! err.message.match(/invalid 'token' authentication/i) ) {
|
||||
done(new Error("Unexpected error message with invalid token auth (undefined tokens): " + err));
|
||||
done(new Error("Unexpected error message with invalid token auth (undefined tokens): "
|
||||
+ err));
|
||||
}
|
||||
else {
|
||||
done();
|
||||
@@ -170,14 +169,14 @@ describe('template_maps', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('add, get and delete a valid template', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('add, get and delete a valid template', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var tpl_id;
|
||||
var tpl = { version:'0.0.1',
|
||||
name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
step(
|
||||
name: 'first', auth: {}, layergroup: {} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
@@ -197,7 +196,7 @@ describe('template_maps', function() {
|
||||
},
|
||||
function delTemplate(err, got_tpl) {
|
||||
if ( err ) throw err;
|
||||
assert.deepEqual(got_tpl, _.extend({}, tpl, {auth: {method: 'open'}, placeholders: {}}));
|
||||
assert.deepEqual(got_tpl, tpl);
|
||||
tmap.delTemplate('me', tpl_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -206,14 +205,15 @@ describe('template_maps', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('add multiple templates, list them', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('add multiple templates, list them', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var expected_failure = false;
|
||||
var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {} };
|
||||
var tpl1_id;
|
||||
var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {} };
|
||||
var tpl2_id;
|
||||
step(
|
||||
Step(
|
||||
function addTemplate1() {
|
||||
tmap.addTemplate('me', tpl1, this);
|
||||
},
|
||||
@@ -264,18 +264,18 @@ describe('template_maps', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('update templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('update templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var owner = 'me';
|
||||
var tpl = { version:'0.0.1',
|
||||
name: 'first',
|
||||
auth: { method: 'open' },
|
||||
layergroup: {layers:[wadusLayer]}
|
||||
layergroup: {}
|
||||
};
|
||||
var tpl_id;
|
||||
step(
|
||||
Step(
|
||||
function addTemplate() {
|
||||
tmap.addTemplate(owner, tpl, this);
|
||||
},
|
||||
@@ -304,6 +304,7 @@ describe('template_maps', function() {
|
||||
},
|
||||
function updateUnexistentTemplate(err) {
|
||||
if ( err && ! expected_failure) throw err;
|
||||
expected_failure = false;
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/unsupported.*version/i), err);
|
||||
tpl.version = '0.0.1';
|
||||
@@ -323,8 +324,8 @@ describe('template_maps', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('instanciate templates', function() {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
test('instanciate templates', function() {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
assert.ok(tmap);
|
||||
|
||||
var tpl1 = {
|
||||
@@ -336,7 +337,7 @@ describe('template_maps', function() {
|
||||
color: { type: "css_color", default: "#a0fF9A" },
|
||||
name: { type: "sql_literal", default: "test" },
|
||||
zoom: { type: "number", default: "0" },
|
||||
test_number: { type: "number", default: 23 }
|
||||
test_number: { type: "number", default: 23 },
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
@@ -380,57 +381,57 @@ describe('template_maps', function() {
|
||||
|
||||
// Invalid css_color
|
||||
var err = null;
|
||||
try { tmap.instance(tpl1, {color:'##ff00ff'}); }
|
||||
try { inst = tmap.instance(tpl1, {color:'##ff00ff'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid css_color 2 (too few digits)
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {color:'#ff'}); }
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {color:'#ff'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid css_color 3 (too many digits)
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {color:'#1234567'}); }
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {color:'#1234567'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid number
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {zoom:'#'}); }
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {zoom:'#'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid number/i), err);
|
||||
|
||||
// Invalid number 2
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {zoom:'23e'}); }
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {zoom:'23e'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid number/i), err);
|
||||
|
||||
// Valid number
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {zoom:'-.23e10'}); }
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {zoom:'-.23e10'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(!err);
|
||||
});
|
||||
|
||||
// Can set a limit on the number of user templates
|
||||
it('can limit number of user templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, {
|
||||
test('can limit number of user templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps, {
|
||||
max_user_templates: 2
|
||||
});
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var tpl = { version:'0.0.1', auth: {}, layergroup: {} };
|
||||
var expectErr = false;
|
||||
var idMe = [];
|
||||
var idYou = [];
|
||||
step(
|
||||
Step(
|
||||
function oneForMe() {
|
||||
tpl.name = 'oneForMe';
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
@@ -450,7 +451,7 @@ describe('template_maps', function() {
|
||||
expectErr = true;
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function errForMe(err/*, id*/) {
|
||||
function errForMe(err, id) {
|
||||
if ( err && ! expectErr ) throw err;
|
||||
expectErr = false;
|
||||
assert.ok(err);
|
||||
@@ -488,7 +489,7 @@ describe('template_maps', function() {
|
||||
expectErr = true;
|
||||
tmap.addTemplate('you', tpl, this);
|
||||
},
|
||||
function errForYou(err/*, id*/) {
|
||||
function errForYou(err, id) {
|
||||
if ( err && ! expectErr ) throw err;
|
||||
expectErr = false;
|
||||
assert.ok(err);
|
||||
@@ -501,5 +502,5 @@ describe('template_maps', function() {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps');
|
||||
|
||||
describe('template_maps_auth', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool, {max_user_templates: 1000});
|
||||
|
||||
function makeTemplate(method, validTokens) {
|
||||
var template = {
|
||||
name: 'wadus_template',
|
||||
auth: {
|
||||
method: method
|
||||
}
|
||||
};
|
||||
|
||||
if (method === 'token') {
|
||||
template.auth.valid_tokens = validTokens || [];
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
var methodToken = 'token',
|
||||
methodOpen = 'open';
|
||||
|
||||
var tokenFoo = 'foo',
|
||||
tokenBar = 'bar';
|
||||
|
||||
var authorizationTestScenarios = [
|
||||
{
|
||||
desc: 'open method is always authorized',
|
||||
template: makeTemplate(methodOpen),
|
||||
token: undefined,
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method is authorized for valid token',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: tokenFoo,
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method not authorized for invalid token',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: tokenBar,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'token method is authorized for valid token array',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: [tokenFoo],
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method not authorized for invalid token array',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: [tokenBar],
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'wadus method not authorized',
|
||||
template: makeTemplate('wadus', [tokenFoo]),
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'undefined template result in not authorized',
|
||||
template: undefined,
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'undefined template auth result in not authorized',
|
||||
template: {},
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
}
|
||||
];
|
||||
|
||||
authorizationTestScenarios.forEach(function(testScenario) {
|
||||
it(testScenario.desc, function(done) {
|
||||
var debugMessage = testScenario.expected ? 'should be authorized' : 'unexpectedly authorized';
|
||||
var result = templateMaps.isAuthorized(testScenario.template, testScenario.token);
|
||||
assert.equal(result, testScenario.expected, debugMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("auth as 'open' string is authorized", function(done) {
|
||||
var template = {
|
||||
name: 'wadus_template',
|
||||
auth: 'open'
|
||||
};
|
||||
|
||||
assert.ok(templateMaps.isAuthorized(template));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('template_maps', function() {
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
var owner = 'me';
|
||||
var templateName = 'wadus';
|
||||
|
||||
|
||||
var defaultTemplate = {
|
||||
version:'0.0.1',
|
||||
name: templateName,
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function makeTemplate(auth, placeholders) {
|
||||
return _.extend({}, defaultTemplate, {
|
||||
auth: auth,
|
||||
placeholders: placeholders
|
||||
});
|
||||
}
|
||||
|
||||
var defaultAuth = {
|
||||
method: 'open'
|
||||
};
|
||||
|
||||
var authTokenSample = {
|
||||
method: 'token',
|
||||
valid_tokens: ['wadus_token']
|
||||
};
|
||||
|
||||
var placeholdersSample = {
|
||||
wadus: {
|
||||
type: 'number',
|
||||
default: 1
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'default auth and placeholders values',
|
||||
template: defaultTemplate,
|
||||
expected: {
|
||||
auth: defaultAuth,
|
||||
placeholders: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default placeholders but specified auth',
|
||||
template: makeTemplate(authTokenSample),
|
||||
expected: {
|
||||
auth: authTokenSample,
|
||||
placeholders: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default auth but specified placeholders',
|
||||
template: makeTemplate(undefined, placeholdersSample),
|
||||
expected: {
|
||||
auth: defaultAuth,
|
||||
placeholders: placeholdersSample
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'specified auth and placeholders',
|
||||
template: makeTemplate(authTokenSample, placeholdersSample),
|
||||
expected: {
|
||||
auth: authTokenSample,
|
||||
placeholders: placeholdersSample
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
it('adding template returns a new instance with ' + testScenario.desc, function(done) {
|
||||
|
||||
templateMaps.addTemplate(owner, testScenario.template, function(err, templateId, template) {
|
||||
assert.ok(!err, 'Unexpected error adding template: ' + (err && err.message));
|
||||
assert.ok(testScenario.template !== template, 'template instances should be different');
|
||||
assert.equal(template.name, templateName);
|
||||
assert.deepEqual(template.auth, testScenario.expected.auth);
|
||||
assert.deepEqual(template.placeholders, testScenario.expected.placeholders);
|
||||
|
||||
templateMaps.delTemplate(owner, templateName, done);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('template_maps', function() {
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
var owner = 'me';
|
||||
var templateName = 'wadus';
|
||||
|
||||
|
||||
var defaultTemplate = {
|
||||
version:'0.0.1',
|
||||
name: templateName
|
||||
};
|
||||
|
||||
function makeTemplate(layers) {
|
||||
var layergroup = {
|
||||
layers: layers
|
||||
};
|
||||
return _.extend({}, defaultTemplate, {
|
||||
layergroup: layergroup
|
||||
});
|
||||
}
|
||||
|
||||
var layerWithMissingOptions = {},
|
||||
minimumValidLayer = {
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'Missing layers array does not validate',
|
||||
template: makeTemplate(),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing or empty layers array from layergroup config'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Empty layers array does not validate',
|
||||
template: makeTemplate([]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing or empty layers array from layergroup config'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Layer with missing options does not validate',
|
||||
template: makeTemplate([
|
||||
layerWithMissingOptions
|
||||
]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing `options` in layergroup config for layers: 0'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Multiple layers report invalid layer',
|
||||
template: makeTemplate([
|
||||
minimumValidLayer,
|
||||
layerWithMissingOptions
|
||||
]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing `options` in layergroup config for layers: 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default auth but specified placeholders',
|
||||
template: makeTemplate([
|
||||
minimumValidLayer
|
||||
]),
|
||||
expected: {
|
||||
isValid: true,
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
it(testScenario.desc, function(done) {
|
||||
|
||||
templateMaps.addTemplate(owner, testScenario.template, function(err) {
|
||||
|
||||
if (testScenario.expected.isValid) {
|
||||
|
||||
assert.ok(!err);
|
||||
templateMaps.delTemplate(owner, templateName, done);
|
||||
|
||||
} else {
|
||||
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, testScenario.expected.message);
|
||||
done();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
{"version":"1.0.1",
|
||||
"layers":[{
|
||||
"type":"cartodb",
|
||||
"options":{
|
||||
"sql":"select 1 as id, ST_Transform(ST_SetSRID(ST_MakePoint(x/1000,x/2000),4326),3857) as the_geom_webmercator FROM generate_series(-170000,170000) x",
|
||||
"cartocss":"#style{ marker-width: 12;}",
|
||||
"cartocss_version":"2.1.1",
|
||||
"Interactivity":"id"
|
||||
}
|
||||
}]
|
||||
}
|
||||
59
tools/flush_cache
Executable file
59
tools/flush_cache
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var path = require('path');
|
||||
var request = require('request');
|
||||
|
||||
function usage(me, exitcode) {
|
||||
console.log("Usage: " + me + " [--env <environment>] <username> <tablename>");
|
||||
process.exit(exitcode);
|
||||
}
|
||||
|
||||
var node_path = process.argv.shift();
|
||||
var script_path = process.argv.shift();
|
||||
var basedir = path.dirname(script_path);
|
||||
var me = path.basename(script_path);
|
||||
|
||||
var ENV = 'development.js';
|
||||
var username, table;
|
||||
var arg;
|
||||
while ( arg = process.argv.shift() ) {
|
||||
if ( arg == '--env' ) {
|
||||
ENV = process.argv.shift();
|
||||
}
|
||||
else if ( ! username ) {
|
||||
username = arg;
|
||||
}
|
||||
else if ( ! table ) {
|
||||
table = arg;
|
||||
}
|
||||
else {
|
||||
console.warn("Unused parameter " + arg);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! table ) {
|
||||
usage(me, 1);
|
||||
}
|
||||
|
||||
global.environment = require('../config/environments/' + ENV);
|
||||
|
||||
// _after_ setting global.environment
|
||||
var serverOptions = require('../lib/cartodb/server_options');
|
||||
|
||||
var host = global.environment.host;
|
||||
var port = global.environment.port;
|
||||
var re = ''+serverOptions.re_userFromHost;
|
||||
var hostname = re.replace(/^\/\^/, '')
|
||||
.replace(/\/$$/, '')
|
||||
.replace(/\\/g,'')
|
||||
.replace(/\([^)]*\)/,username)
|
||||
;
|
||||
//console.log("re: " + re);
|
||||
//console.log("hostname: " + hostname);
|
||||
|
||||
var url = 'http://' + host + ':' + port + '/tiles/' + table + '/flush_cache';
|
||||
request.del({ url: url, headers: { host: hostname } },
|
||||
function(err, res, body) {
|
||||
if ( err ) throw err;
|
||||
console.log(res.body);
|
||||
});
|
||||
@@ -38,8 +38,7 @@ if ( ! username ) usage(me, 1);
|
||||
console.log("Using environment " + ENV);
|
||||
|
||||
global.environment = require('../config/environments/' + ENV);
|
||||
// _after_ setting global.environment
|
||||
var serverOptions = require('../lib/cartodb/server_options')();
|
||||
var serverOptions = require('../lib/cartodb/server_options'); // _after_ setting global.environment
|
||||
|
||||
var client;
|
||||
var dbname;
|
||||
|
||||
Reference in New Issue
Block a user