├── .cloudbuild ├── .id_github_cartofante.enc ├── build-windshaft.yaml ├── known_hosts └── pr-tests-windshaft.yaml ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitmodules ├── HOW_TO_RELEASE.md ├── LICENSE ├── LOGGING.md ├── NEWS.md ├── README.md ├── app.js ├── assets ├── default-placeholder.png ├── default-placeholder@2x.png ├── render-timeout-fallback.mvt ├── render-timeout-fallback.png └── render-timeout-fallback@2x.png ├── carto-package.json ├── config └── environments │ ├── config.js │ ├── development.js.example │ ├── production.js.example │ ├── staging.js.example │ └── test.js.example ├── docs ├── examples │ └── 01-example.md ├── guides │ ├── 01-quickstart.md │ ├── 02-general-concepts.md │ ├── 03-anonymous-maps.md │ ├── 04-named-maps.md │ ├── 05-static-maps-API.md │ ├── 06-tile-aggregation.md │ ├── 07-MapConfig-file-format.md │ ├── 08-MapConfig-aggregation-extension.md │ ├── 09-MapConfig-analyses-extension.md │ ├── 10-MapConfig-dataviews-extension.md │ └── 11-Mapconfig-named-maps-extension.md ├── internal │ └── multilayer-API.md ├── reference │ └── swagger.yaml └── support │ ├── 01-support-options.md │ ├── 02-contribute.md │ ├── 03-rate-limiting.md │ ├── 04-timeout-limiting.md │ ├── 05-quota-limiting.md │ └── 06-metrics.md ├── lib ├── api │ ├── api-router.js │ ├── map │ │ ├── analyses-catalog-controller.js │ │ ├── analysis-layergroup-controller.js │ │ ├── anonymous-map-controller.js │ │ ├── attributes-layergroup-controller.js │ │ ├── clustered-features-layergroup-controller.js │ │ ├── dataview-layergroup-controller.js │ │ ├── map-router.js │ │ ├── preview-layergroup-controller.js │ │ ├── preview-template-controller.js │ │ └── tile-layergroup-controller.js │ ├── middlewares │ │ ├── augment-layergroup-data.js │ │ ├── authorize.js │ │ ├── cache-channel-header.js │ │ ├── cache-control-header.js │ │ ├── check-json-content-type.js │ │ ├── check-static-image-format.js │ │ ├── clean-up-query-params.js │ │ ├── client-header.js │ │ ├── coordinates.js │ │ ├── cors.js │ │ ├── credentials.js │ │ ├── db-conn-setup.js │ │ ├── error-middleware.js │ │ ├── increment-map-view-count.js │ │ ├── initialize-status-code.js │ │ ├── last-modified-header.js │ │ ├── last-updated-time-layergroup.js │ │ ├── layer-stats.js │ │ ├── layergroup-id-header.js │ │ ├── layergroup-metadata.js │ │ ├── layergroup-token.js │ │ ├── logger.js │ │ ├── lzma.js │ │ ├── map-error.js │ │ ├── map-store-map-config-provider.js │ │ ├── metrics.js │ │ ├── named-map-provider.js │ │ ├── noop.js │ │ ├── profiler.js │ │ ├── rate-limit.js │ │ ├── send-response.js │ │ ├── served-by-host-header.js │ │ ├── surrogate-key-header.js │ │ ├── syntax-error.js │ │ ├── tag.js │ │ ├── user.js │ │ └── vector-error.js │ └── template │ │ ├── admin-template-controller.js │ │ ├── named-template-controller.js │ │ ├── template-router.js │ │ └── tile-template-controller.js ├── backends │ ├── analysis-status.js │ ├── analysis.js │ ├── auth.js │ ├── cluster.js │ ├── dataview.js │ ├── filter-stats.js │ ├── layer-stats │ │ ├── empty-layer-stats.js │ │ ├── factory.js │ │ ├── layer-stats.js │ │ ├── mapnik-layer-stats.js │ │ └── torque-layer-stats.js │ ├── metrics.js │ ├── overviews-metadata.js │ ├── pg-connection.js │ ├── pg-query-runner.js │ ├── stats.js │ ├── tables-extent.js │ ├── template-maps.js │ ├── turbo-carto-postgres-datasource.js │ └── user-limits.js ├── cache │ ├── backend │ │ ├── fastly.js │ │ └── varnish-http.js │ ├── layergroup-affected-tables.js │ ├── model │ │ └── named-maps-entry.js │ ├── named-map-provider-cache.js │ └── surrogate-keys-cache.js ├── models │ ├── aggregation │ │ ├── aggregation-mapconfig.js │ │ ├── aggregation-query.js │ │ ├── aggregation-validator.js │ │ └── time-dimension.js │ ├── cdb-request.js │ ├── dataview │ │ ├── aggregation.js │ │ ├── base.js │ │ ├── factory.js │ │ ├── formula.js │ │ ├── histogram.js │ │ ├── histograms │ │ │ ├── base-histogram.js │ │ │ ├── date-histogram.js │ │ │ └── numeric-histogram.js │ │ ├── index.js │ │ └── overviews │ │ │ ├── aggregation.js │ │ │ ├── base.js │ │ │ ├── factory.js │ │ │ ├── formula.js │ │ │ ├── histogram.js │ │ │ └── index.js │ ├── filter │ │ ├── analysis.js │ │ ├── analysis │ │ │ ├── category.js │ │ │ └── range.js │ │ ├── bbox.js │ │ ├── circle.js │ │ └── polygon.js │ ├── layergroup-token.js │ ├── mapconfig │ │ ├── adapter │ │ │ ├── aggregation-mapconfig-adapter.js │ │ │ ├── analysis-mapconfig-adapter.js │ │ │ ├── dataviews-widgets-adapter.js │ │ │ ├── index.js │ │ │ ├── mapconfig-buffer-size-adapter.js │ │ │ ├── mapconfig-named-layers-adapter.js │ │ │ ├── mapconfig-overviews-adapter.js │ │ │ ├── sql-wrap-mapconfig-adapter.js │ │ │ ├── turbo-carto-adapter.js │ │ │ └── vector-mapconfig-adapter.js │ │ └── provider │ │ │ ├── base-mapconfig-adapter.js │ │ │ ├── create-layergroup-provider.js │ │ │ ├── map-store-provider.js │ │ │ └── named-map-provider.js │ └── resource-locator.js ├── monitoring │ └── health-check.js ├── server-info-controller.js ├── server-options.js ├── server.js ├── stats │ ├── client.js │ ├── profiler-proxy.js │ ├── reporter │ │ ├── named-map-provider-cache.js │ │ └── renderer.js │ └── timer.js └── utils │ ├── common-headers.js │ ├── database-params.js │ ├── date-wrapper.js │ ├── icu-data-env-setter.js │ ├── json-replacer.js │ ├── layergroup-metadata.js │ ├── logger.js │ ├── on-tile-error-strategy.js │ ├── overviews-query-rewriter.js │ ├── query-utils.js │ ├── substitution-tokens.js │ └── table-name-parser.js ├── metro ├── config.json ├── index.js ├── log-collector.js ├── metrics-collector.js └── metro.js ├── package-lock.json ├── package.json ├── scripts ├── darwin-pre-install.sh ├── lzma2config.js └── mvt-timeout-error.py └── test ├── acceptance ├── aggregation-test.js ├── analysis │ ├── analyses-controller-test.js │ ├── analyses-filters-params-test.js │ ├── analyses-filters-test.js │ ├── analysis-layers-dataviews-test.js │ ├── analysis-layers-test.js │ ├── analysis-layers-use-cases-test.js │ ├── error-cases-test.js │ ├── named-maps-test.js │ └── regressions-test.js ├── auth │ ├── authorization-basic-use-cases-test.js │ └── authorization-test.js ├── buffer-size-format-test.js ├── cache │ ├── cache-control-header-test.js │ ├── cache-headers-test.js │ └── surrogate-keys-invalidation-test.js ├── cluster-test.js ├── custom-middlewares-test.js ├── dataviews │ ├── aggregation-test.js │ ├── error-cases-test.js │ ├── formula-test.js │ ├── histogram-test.js │ ├── overviews-test.js │ └── spatial-filters-test.js ├── date-wrapping-test.js ├── dynamic-styling-named-maps-test.js ├── errors-with-context-test.js ├── health-check-test.js ├── label-wrap-test.js ├── layergroup-metadata-test.js ├── layergroupid-test.js ├── layers-filters-test.js ├── map-view-headers-test.js ├── max-waiting-workers-test.js ├── multilayer-server-test.js ├── multilayer-test.js ├── mvt-regressions-test.js ├── mvt-test.js ├── named-layers-test.js ├── named-layers-visibility-test.js ├── named-map-cache-regressions-test.js ├── named-maps-authentication-test.js ├── named-maps-cache-test.js ├── named-maps-static-view-test.js ├── named-maps-stats-test.js ├── overviews-metadata-named-maps-test.js ├── overviews-metadata-test.js ├── overviews-queries-test.js ├── ported │ ├── attributes-test.js │ ├── blend-filtering-test.js │ ├── blend-http-fallback-test.js │ ├── blend-http-timeout-test.js │ ├── blend-test.js │ ├── external-resources-test.js │ ├── fixtures │ │ ├── test_table_0_0_0_multilayer1.layer0.grid.json │ │ ├── test_table_0_0_0_multilayer1.layer1.grid.json │ │ ├── test_table_0_0_0_multilayer1.png │ │ ├── test_table_0_0_0_multilayer2.png │ │ ├── test_table_0_0_0_multilayer3.png │ │ └── test_table_0_0_0_multilayer4.png │ ├── limits-test.js │ ├── multilayer-error-cases-test.js │ ├── multilayer-interactivity-test.js │ ├── multilayer-test.js │ ├── raster-test.js │ ├── regressions-test.js │ ├── retina-test.js │ ├── server-gettile-test.js │ ├── server-png8-format-test.js │ ├── server-test.js │ ├── static-maps-test.js │ ├── support │ │ ├── ported-server-options.js │ │ └── test-client.js │ ├── torque-boundaries-test.js │ ├── torque-png-test.js │ ├── torque-test.js │ ├── torque-zero-zero-test.js │ └── wrap-test.js ├── rate-limit-test.js ├── regressions-test.js ├── server-test.js ├── special-numeric-values-test.js ├── sql-wrap-test.js ├── stats │ ├── mapnik-stats-layergroup-test.js │ └── multilayer-stats-test.js ├── templates-test.js ├── tilejson-test.js ├── turbo-carto │ ├── anonymous-maps-test.js │ ├── error-cases-test.js │ ├── named-maps-test.js │ └── regressions-test.js ├── user-database-timeout-limit-test.js ├── user-render-timeout-limit-test.js ├── vector-layergroup-test.js └── widgets │ ├── aggregation-test.js │ ├── histogram-test.js │ ├── named-maps-test.js │ ├── ported │ ├── aggregation-test.js │ ├── formula-test.js │ └── histogram-test.js │ ├── regressions-test.js │ └── widgets-test.js ├── fixtures ├── _vovw_1_test_table_1_0_0.png ├── _vovw_2_test_table_2_1_1.png ├── analysis │ ├── adm0cap-source-id-mapnik-layer.png │ ├── basic-source-id-mapnik-layer.png │ ├── buffer-over-source.png │ ├── named-map-buffer-layergroup-static-preview.png │ ├── named-map-buffer-static-preview.png │ └── named-map-buffer.png ├── blank.png ├── blend │ ├── blend-filtering-layers-0.1-zxy-1.0.0.png │ ├── blend-filtering-layers-0.2-zxy-1.0.0.png │ ├── blend-filtering-layers-0.3-zxy-1.0.0.png │ ├── blend-filtering-layers-1.2-zxy-1.0.0.png │ ├── blend-filtering-layers-1.2.3.4-zxy-1.0.0.png │ ├── blend-filtering-layers-1.2.5-zxy-1.0.0.png │ ├── blend-filtering-layers-1.3-zxy-1.0.0.png │ ├── blend-filtering-layers-2.1-zxy-1.0.0.png │ ├── blend-filtering-layers-2.2-zxy-1.0.0.png │ ├── blend-plain-torque-2.1.1.png │ ├── blend-plain-torque-2.2.1.png │ └── http_fallback │ │ ├── blend-layers-0.3-zxy-1.0.0.png │ │ ├── blend-layers-0.4-zxy-1.0.0.png │ │ ├── blend-layers-1.2-zxy-1.0.0.png │ │ ├── blend-layers-1.3-zxy-1.0.0.png │ │ ├── blend-layers-2.3-zxy-1.0.0.png │ │ ├── blend-layers-3.4-zxy-1.0.0.png │ │ └── blend-layers-all-zxy-1.0.0.png ├── buffer-size │ ├── tile-7.64.48-buffer-size-0.png │ ├── tile-7.64.48-buffer-size-128.geojson │ ├── tile-7.64.48-buffer-size-128.grid.json │ ├── tile-7.64.48-buffer-size-128.png │ ├── tile-grid.json.7.64.48-buffer-size-0.grid.json │ ├── tile-mvt-7.64.48-buffer-size-0.geojson │ ├── tile-mvt-7.64.48-buffer-size-0.mvt │ └── tile-mvt-7.64.48-buffer-size-128.mvt ├── http │ ├── basemap.png │ ├── dark_nolabels-1-0-0.png │ └── light_nolabels-1-0-0.png ├── limits │ └── fallback.png ├── markers │ ├── circle.svg │ └── square.svg ├── previews │ ├── populated_places_simple_reduced-bounds.png │ ├── populated_places_simple_reduced-estimated-proj5.png │ ├── populated_places_simple_reduced-estimated.png │ ├── populated_places_simple_reduced-override-bbox.png │ ├── populated_places_simple_reduced-override-zoom.png │ ├── populated_places_simple_reduced-preview_layers_all.png │ ├── populated_places_simple_reduced-preview_layers_blue.png │ ├── populated_places_simple_reduced-preview_layers_orange.png │ ├── populated_places_simple_reduced-preview_layers_orange_blue.png │ ├── populated_places_simple_reduced-preview_layers_red.png │ ├── populated_places_simple_reduced-preview_layers_red_blue.png │ ├── populated_places_simple_reduced-preview_layers_red_orange.png │ ├── populated_places_simple_reduced-preview_layers_red_orange_blue.png │ ├── populated_places_simple_reduced-preview_layers_undefined.png │ ├── populated_places_simple_reduced-zoom-center.jpeg │ └── populated_places_simple_reduced-zoom-center.png ├── provider │ ├── populated_places_simple_reduced-black.png │ ├── populated_places_simple_reduced-blue.png │ ├── populated_places_simple_reduced-green.png │ └── populated_places_simple_reduced-red.png ├── raster_gray_rect.png ├── render-timeout-fallback.png ├── sql-wrap-usa-filter.png ├── test_bigpoint_red.png ├── test_default_mapnik_point.png ├── test_mapconfigFactory.js ├── test_multilayer_bbox.grid.json ├── test_multilayer_bbox.png ├── test_table_0_0_0_multilayer1.layer0.grid.json ├── test_table_0_0_0_multilayer1.layer1.grid.json ├── test_table_0_0_0_multilayer1.png ├── test_table_0_0_0_multilayer2.png ├── test_table_0_0_0_multilayer3.png ├── test_table_0_0_0_multilayer4.png ├── test_table_13_4011_3088.grid.json ├── test_table_13_4011_3088.png ├── test_table_13_4011_3088_empty.grid.json ├── test_table_13_4011_3088_limit_2.grid.json ├── test_table_13_4011_3088_limit_2.png ├── test_table_13_4011_3088_styled.png ├── test_table_13_4011_3088_styled_black.png ├── test_table_13_4011_3088_svg1.png ├── test_table_13_4011_3088_svg2.png ├── test_table_15_16046_12354_styled_black.png ├── test_table_15_16046_12354_styled_blue.png ├── test_table_1_0_0.png ├── test_table_2_1_1.png ├── test_table_3_3_3.png ├── test_turbo_carto_greens_13_4011_3088.png ├── test_turbo_carto_reds_13_4011_3088.png ├── text_wrap.png ├── text_wrap_bad.png ├── torque │ ├── populated_places_simple_reduced-2.1.1.png │ ├── populated_places_simple_reduced-2.2.1.png │ └── populated_places_simple_reduced-turbo-carto-2.2.1.png ├── turbo-carto-named-maps-blues.png └── turbo-carto-named-maps-reds.png ├── index.js ├── integration ├── analysis-backend-limits-test.js ├── mapconfig-named-layers-datasource-test.js ├── mapconfig-named-layers-expanded-test.js ├── mapconfig-overviews-adapter-test.js ├── metrics-test.js ├── overviews-metadata-api-test.js ├── pg-query-runner-test.js ├── profiler-test.js ├── query-tables-test.js └── template-maps-limits-test.js ├── monkey ├── images │ ├── layers.png │ ├── marker-shadow.png │ ├── marker.png │ ├── popup-close.png │ ├── zoom-in.png │ └── zoom-out.png ├── index.html ├── leaflet.css ├── leaflet.ie.css └── leaflet.js ├── results ├── jpeg │ └── .gitignore └── png │ └── .gitignore ├── support ├── assert.js ├── libredis_cell.dylib ├── libredis_cell.so ├── map-store.js ├── middlewares │ ├── teapot-conditional-response.js │ ├── teapot-headers.js │ └── teapot-response.js ├── sql │ ├── .gitignore │ ├── analysis_catalog.sql │ ├── cdb_analysis_check.sql │ ├── cdb_invalidate_varnish.sql │ ├── countries_null_values.sql │ ├── gadm4.sql │ ├── ported │ │ └── populated_places_simple_reduced.sql │ ├── users.sql │ └── windshaft.test.sql ├── test-client.js └── test-helper.js └── unit ├── backends ├── layer-stats │ ├── mapnik-layer-stats-test.js │ └── torque-layer-stats-test.js └── turbo-carto-postgres-datasource-test.js ├── cache └── model │ └── named-maps-entry-test.js ├── cdb-request-test.js ├── error-messages-test.js ├── error-middleware-test.js ├── lzma-middleware-test.js ├── mapconfig ├── adapter-test.js └── dataviews-widgets-adapter-test.js ├── middlewares └── coordinates-test.js ├── model ├── filter │ └── bbox-filters-test.js └── resource-locator-test.js ├── overviews-query-rewriter-test.js ├── ported ├── profiler-test.js ├── stats-client-test.js └── windshaft-server-test.js ├── prepare-context-test.js ├── stats └── reporter │ └── named-map-provider-test.js ├── substitution-tokens-test.js ├── table-name-parser-test.js ├── template-maps-auth-test.js ├── template-maps-defaults-test.js ├── template-maps-test.js ├── utils └── date-wrapper-test.js └── valid-template-maps-test.js /.cloudbuild/.id_github_cartofante.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/.cloudbuild/.id_github_cartofante.enc -------------------------------------------------------------------------------- /.cloudbuild/known_hosts: -------------------------------------------------------------------------------- 1 | github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/results/ 2 | test/monkey/ 3 | test/benchmark.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | mocha: true 7 | }, 8 | extends: [ 9 | 'standard' 10 | ], 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly' 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2018 17 | }, 18 | rules: { 19 | "indent": ["error", 4], 20 | "semi": ["error", "always"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules* 2 | config.status* 3 | config/environments/*.js 4 | .idea 5 | .vscode 6 | .nvmrc 7 | tools/munin/windshaft.conf 8 | logs/ 9 | pids/ 10 | redis.pid 11 | *.log 12 | coverage/ 13 | .DS_Store 14 | .nyc_output 15 | build_resources/ 16 | .dockerignore 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "private"] 2 | path = private 3 | url = git@github.com:CartoDB/Windshaft-cartodb-private.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /HOW_TO_RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to release 2 | 3 | 1. Test (npm test), fix if broken before proceeding. 4 | 2. Ensure proper version in `package.json` and `package-lock.json`. 5 | 3. Ensure NEWS section exists for the new version, review it, add release date. 6 | 4. If there are modified dependencies in `package.json`, update them with `npm upgrade {{package_name}}@{{version}}`. 7 | 5. Commit `package.json`, `package-lock.json`, NEWS. 8 | 6. Run `git tag -a Major.Minor.Patch`. Use NEWS section as content. 9 | 7. Stub NEWS/package for next version. 10 | 11 | ## Version: 12 | 13 | * Bugfix releases increment Patch component of version. 14 | * Feature releases increment Minor and set Patch to zero. 15 | * If backward compatibility is broken, increment Major and set to zero Minor and Patch. 16 | * Branches named 'b.' are kept for any critical fix that might need to be shipped before next feature release is ready. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, CartoDB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /LOGGING.md: -------------------------------------------------------------------------------- 1 | # Logging structured traces 2 | 3 | In order to have meaningful and useful log traces, you should follow 4 | some general guidelines described in the [Project Guidelines](http://doc-internal.cartodb.net/platform/guidelines.html#structured-logging). 5 | 6 | In this project there is a specific logger in place that takes care of 7 | format and context of the traces for you. Take a look at [logger.js](https://github.com/CartoDB/Windshaft-cartodb/blob/cf82e1954e2244861e47fce0c2223ee466a5cd64/lib/utils/logger.js) 8 | (NOTE: that file will be moved soon to a common module). 9 | 10 | The logger is instantiated as part of the [app startup process](https://github.com/CartoDB/Windshaft-cartodb/blob/cf82e1954e2244861e47fce0c2223ee466a5cd64/app.js#L53), 11 | then passed to middlewares and other client classes. 12 | 13 | There are many examples of how to use the logger to generate traces 14 | throughout the code. Here are a few of them: 15 | 16 | ```js 17 | lib/api/middlewares/logger.js: res.locals.logger.info({ client_request: req }, 'Incoming request'); 18 | lib/api/middlewares/logger.js: res.on('finish', () => res.locals.logger.info({ server_response: res, status: res.statusCode }, 'Response sent')); 19 | lib/api/middlewares/profiler.js: logger.info({ stats, duration: stats.response / 1000, duration_ms: stats.response }, 'Request profiling stats'); 20 | lib/api/middlewares/tag.js: res.on('finish', () => logger.info({ tags: res.locals.tags }, 'Request tagged')); 21 | ``` 22 | -------------------------------------------------------------------------------- /assets/default-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/assets/default-placeholder.png -------------------------------------------------------------------------------- /assets/default-placeholder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/assets/default-placeholder@2x.png -------------------------------------------------------------------------------- /assets/render-timeout-fallback.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/assets/render-timeout-fallback.mvt -------------------------------------------------------------------------------- /assets/render-timeout-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/assets/render-timeout-fallback.png -------------------------------------------------------------------------------- /assets/render-timeout-fallback@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/assets/render-timeout-fallback@2x.png -------------------------------------------------------------------------------- /carto-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carto_windshaft", 3 | "current_version": { 4 | "requires": { 5 | "node": "^12.16.3", 6 | "npm": "^6.14.4", 7 | "mapnik": "==3.0.15.16", 8 | "crankshaft": "~0.8.1" 9 | }, 10 | "works_with": { 11 | "redis": ">=4.0.0", 12 | "postgresql": ">=10.0.0", 13 | "postgis": ">=2.4.4.5", 14 | "carto_postgresql_ext": ">=0.35.0" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/examples/01-example.md: -------------------------------------------------------------------------------- 1 | ## Example 1 -------------------------------------------------------------------------------- /docs/guides/11-Mapconfig-named-maps-extension.md: -------------------------------------------------------------------------------- 1 | ## MapConfig Named Maps Extension 2 | 3 | ### 1. Purpose 4 | 5 | This specification describes an extension for 6 | [MapConfig 1.3.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.3.0.md) version. 7 | 8 | 9 | ### 2. Changes over specification 10 | 11 | This extension introduces a new layer type so it's possible to use a Named Map by its name as a layer. 12 | 13 | #### 2.1 Named layers definition 14 | 15 | ```javascript 16 | { 17 | // REQUIRED 18 | // string, `named` is the only supported value 19 | type: "named", 20 | 21 | // REQUIRED 22 | // object, set `named` map layers configuration 23 | options: { 24 | 25 | // REQUIRED 26 | // string, the name for the Named Map to use 27 | name: "world_borders", 28 | 29 | // OPTIONAL 30 | // object, the replacement values for the Named Map's template placeholders 31 | // See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#instantiate-1 for more details 32 | config: { 33 | "color": "#000" 34 | }, 35 | 36 | // OPTIONAL 37 | // string array, the authorized tokens in case the Named Map has auth method set to `token` 38 | // See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#named-maps-1 for more details 39 | auth_tokens: [ 40 | "token1", 41 | "token2" 42 | ] 43 | } 44 | } 45 | ``` 46 | 47 | #### 2.2 Limitations 48 | 49 | 1. A Named Map will not allow to have `named` type layers inside their templates layergroup's layers definition. 50 | 2. A `named` layer does not allow Named Maps form other accounts, it's only possible to use Named Maps from the very 51 | same user account. 52 | 53 | 54 | ### History 55 | 56 | #### 1.0.0 57 | 58 | - Initial version 59 | -------------------------------------------------------------------------------- /docs/internal/multilayer-API.md: -------------------------------------------------------------------------------- 1 | The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/CartoDB/Windshaft/blob/master/doc/internal/multilayer-API.md) in a few ways. 2 | 3 | ## Last modification timestamp embedded in the token 4 | 5 | It encodes a timestamp of 'last modification time' into the map token (token:EPOCH) returned to the client. 6 | It accepts tokens with encoded timestamp from the client considering the token suffix as a cache_buster value. 7 | 8 | Clients don't need to be aware of the extension but rather use the API as they would use the base one. 9 | The only difference will be that the _same_ layergroup configuration may result in different tokens if source data was modified between the mapview requests. 10 | 11 | ## Additional attributes in the response object 12 | 13 | Windshaft-CartoDB adds the following attributes in the response object 14 | 15 | - ``last_update`` field with ISO format (2013-11-30T12:23:10). 16 | - ``cdn_url`` object containing CDN url client should use (not mandatory) to access the tiles. It's in the form: 17 | 18 | ```json 19 | { 20 | "http": "http://cdn_url.com/", 21 | "https": "https://secure.cdn_url.com/" 22 | } 23 | ``` 24 | 25 | 26 | ## Stats tag 27 | 28 | 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. -------------------------------------------------------------------------------- /docs/support/01-support-options.md: -------------------------------------------------------------------------------- 1 | ## Support Options 2 | 3 | Feeling stuck? There are many ways to find help. 4 | 5 | * Ask a question on [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/carto) using the `CARTO` tag. 6 | * [Report an issue](https://github.com/CartoDB/cartodb/issues) in Github. 7 | * Engine Plan customers have additional access to enterprise-level support through CARTO's support representatives. 8 | 9 | If you just want to describe an issue or share an idea, just 10 | 11 | ### Issues on Github 12 | 13 | If you think you may have found a bug, or if you have a feature request that you would like to share with the Maps API team, please [open an issue](https://github.com/CartoDB/Windshaft-cartodb/issues/new). 14 | 15 | ### Community support on GIS Stack Exchange 16 | 17 | GIS Stack Exchange is the most popular community in the geospatial industry. This is a collaboratively-edited question and answer site for geospatial programmers and technicians. It is a fantastic resource for asking technical questions about developing and maintaining your application. 18 | 19 | 20 | When posting a new question, please consider the following: 21 | 22 | * Read the GIS Stack Exchange [help](https://gis.stackexchange.com/help) and [how to ask](https://gis.stackexchange.com/help/how-to-ask) pages for guidelines and tips about posting questions. 23 | * Be very clear about your question in the subject. A clear explanation helps those trying to answer your question, as well as those who may be looking for information in the future. 24 | * Be informative in your post. Details, code snippets, logs, screenshots, etc. help others to understand your problem. 25 | * Use code that demonstrates the problem. It is very hard to debug errors without sample code to reproduce the problem. 26 | 27 | ### Engine Plan Customers 28 | 29 | Engine Plan customers have additional support options beyond general community support. As per your account Terms of Service, you have access to enterprise-level support through CARTO's support representatives available at [enterprise-support@carto.com](mailto:enterprise-support@carto.com) 30 | 31 | In order to speed up the resolution of your issue, provide as much information as possible (even if it is a link from community support). This allows our engineers to investigate your problem as soon as possible. 32 | 33 | If you are not yet CARTO customer, browse our [plans & pricing](https://carto.com/pricing/) and find the right plan for you. 34 | -------------------------------------------------------------------------------- /docs/support/02-contribute.md: -------------------------------------------------------------------------------- 1 | ## Contribute 2 | 3 | CARTO platform is an open-source ecosystem. You can read about the [fundamentals]({{site.fundamental_docs}}/components/) of CARTO architecture and its components. 4 | We are more than happy to receive your contributions to the code and the documentation as well. 5 | 6 | ## Filling a ticket 7 | 8 | If you want to open a new issue in our repository, please follow these instructions: 9 | 10 | 1. Descriptive title. 11 | 2. Write a good description, it always helps. 12 | 3. Specify the steps to reproduce the problem. 13 | 4. Try to add an example showing the problem. 14 | 15 | ## Contributing code 16 | 17 | Best part of open source, collaborate in Maps API code!. We like hearing from you, so if you have any bug fixed, or a new feature ready to be merged, those are the steps you should follow: 18 | 19 | 1. Fork the repository. 20 | 2. Create a new branch in your forked repository. 21 | 3. Commit your changes. Add new tests if it is necessary. 22 | 4. Open a pull request. 23 | 5. Any of the maintainers will take a look. 24 | 6. If everything works, it will merged and released \o/. 25 | 26 | If you want more detailed information, this [GitHub guide](https://guides.github.com/activities/contributing-to-open-source/) is a must. 27 | 28 | ## Completing documentation 29 | 30 | Maps API documentation is located in ```docs/```. That folder is the content that appears in the [Developer Center](https://carto.com/developers/maps-api/). Just follow the instructions described in [contributing code](#contributing-code) and after accepting your pull request, we will make it appear online :). 31 | 32 | **Tip:** A convenient, easy way of proposing changes in documentation is by using the GitHub editor directly on the web. You can easily create a branch with your changes and make a PR from there. 33 | 34 | ## Submitting contributions 35 | 36 | You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://carto.com/contributions). 37 | -------------------------------------------------------------------------------- /docs/support/04-timeout-limiting.md: -------------------------------------------------------------------------------- 1 | ## Timeout limit 2 | 3 | Our APIs work following a request <-> response model. While CARTO is busy getting that action done or retrieving that information, part of our infrastructure is devoted to that process and is therefore unavailable for any other user. Typically this is not a problem, as most requests get serviced quickly enough. However, certain requests can take a long time to process, either by design (e.g., updating a huge table) or by mistake. To prevent this long-running queries from effectively blocking the usage of our platform resources, CARTO will discard requests that cannot be fulfilled in less than a certain amount of time. 4 | 5 | Maps API is affected by this kind of limiting. 6 | 7 | ### Per User 8 | 9 | Timeout limit is on a per-user basis (or more accurately described, per user access). 10 | 11 | ### How it works 12 | 13 | Every query has a statement timeout. When a request reaches that value, the response returns an error. 14 | 15 | ### Response Codes 16 | 17 | When query exceeds the timeout limit, the API will return an HTTP `429 Too Many Requests` error. 18 | 19 | ### Tips 20 | 21 | You are able to avoid common issues that trigger timeout limits following these actions: 22 | 23 | - Always use database indexes 24 | - Try to use batch API to insert/update/delete data 25 | 26 | ### Timeout Limits Chart 27 | 28 | Below, you can find the values of the timeout limit by user account type. 29 | 30 | |Enterprise plans |Individual plans |Free plans | 31 | | --- | --- | --- | 32 | | 25 seconds | 15 seconds | 5 seconds | 33 | -------------------------------------------------------------------------------- /docs/support/05-quota-limiting.md: -------------------------------------------------------------------------------- 1 | ## Quota limiting 2 | 3 | CARTO platform imposes limits on how much data you can store at CARTO, for every user account and organization. You can learn more about this topic by reading the [fundamentals about limits]({{site.fundamental_docs}}/limits/) of the CARTO platform. 4 | 5 | Maps API is affected by this kind of limiting. 6 | 7 | ### Quota Limits Chart 8 | 9 | Below, you can find the values of the different quota limits by user account type. 10 | 11 | |Limit |Enterprise plans |Individual plans |Free plans | 12 | | :--- | ---: | ---: | ---: | 13 | | Maximum Static Map image size |4000 X 4000 pixels |4000 X 4000 pixels |4000 X 4000 pixels | 14 | | Maximum number of Named Maps |4096 |4096 |4096 | 15 | | Maximum number of layers |10 |8 |4 | 16 | -------------------------------------------------------------------------------- /lib/api/map/analysis-layergroup-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tag = require('../middlewares/tag'); 4 | const layergroupToken = require('../middlewares/layergroup-token'); 5 | const cleanUpQueryParams = require('../middlewares/clean-up-query-params'); 6 | const credentials = require('../middlewares/credentials'); 7 | const dbConnSetup = require('../middlewares/db-conn-setup'); 8 | const authorize = require('../middlewares/authorize'); 9 | const rateLimit = require('../middlewares/rate-limit'); 10 | const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; 11 | const dbParamsFromResLocals = require('../../utils/database-params'); 12 | 13 | module.exports = class AnalysisLayergroupController { 14 | constructor (analysisStatusBackend, pgConnection, userLimitsBackend, authBackend) { 15 | this.analysisStatusBackend = analysisStatusBackend; 16 | this.pgConnection = pgConnection; 17 | this.userLimitsBackend = userLimitsBackend; 18 | this.authBackend = authBackend; 19 | } 20 | 21 | route (mapRouter) { 22 | mapRouter.get('/:token/analysis/node/:nodeId', this.middlewares()); 23 | } 24 | 25 | middlewares () { 26 | return [ 27 | tag({ tags: ['analysis', 'node'] }), 28 | layergroupToken(), 29 | credentials(), 30 | authorize(this.authBackend), 31 | dbConnSetup(this.pgConnection), 32 | rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS), 33 | cleanUpQueryParams(), 34 | analysisNodeStatus(this.analysisStatusBackend) 35 | ]; 36 | } 37 | }; 38 | 39 | function analysisNodeStatus (analysisStatusBackend) { 40 | return function analysisNodeStatusMiddleware (req, res, next) { 41 | const { nodeId } = req.params; 42 | const dbParams = dbParamsFromResLocals(res.locals); 43 | 44 | analysisStatusBackend.getNodeStatus(nodeId, dbParams, (err, nodeStatus, stats = {}) => { 45 | req.profiler.add(stats); 46 | 47 | if (err) { 48 | err.label = 'GET NODE STATUS'; 49 | return next(err); 50 | } 51 | 52 | res.set({ 53 | 'Cache-Control': 'public,max-age=5', 54 | 'Last-Modified': new Date().toUTCString() 55 | }); 56 | 57 | res.statusCode = 200; 58 | res.body = nodeStatus; 59 | 60 | next(); 61 | }); 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /lib/api/middlewares/augment-layergroup-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('underscore'); 4 | 5 | module.exports = function augmentLayergroupData () { 6 | return function augmentLayergroupDataMiddleware (req, res, next) { 7 | const layergroup = res.body; 8 | 9 | // include in layergroup response the variables in serverMedata 10 | // those variables are useful to send to the client information 11 | // about how to reach this server or information about it 12 | _.extend(layergroup, global.environment.serverMetadata); 13 | 14 | next(); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/api/middlewares/authorize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function authorize (authBackend) { 4 | return function authorizeMiddleware (req, res, next) { 5 | authBackend.authorize(req, res, (err, authorized) => { 6 | if (err) { 7 | return next(err); 8 | } 9 | 10 | if (!authorized) { 11 | err = new Error('Sorry, you are unauthorized (permission denied)'); 12 | err.http_status = 403; 13 | return next(err); 14 | } 15 | 16 | return next(); 17 | }); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/api/middlewares/cache-channel-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function setCacheChannelHeader () { 4 | return function setCacheChannelHeaderMiddleware (req, res, next) { 5 | if (req.method !== 'GET') { 6 | return next(); 7 | } 8 | 9 | const { mapConfigProvider, logger } = res.locals; 10 | 11 | mapConfigProvider.getAffectedTables((err, affectedTables) => { 12 | if (err) { 13 | logger.warn({ exception: err }, 'Error generating Cache Channel Header'); 14 | return next(); 15 | } 16 | 17 | if (!affectedTables) { 18 | return next(); 19 | } 20 | 21 | res.set('X-Cache-Channel', affectedTables.getCacheChannel()); 22 | 23 | next(); 24 | }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/api/middlewares/check-json-content-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function checkJsonContentType () { 4 | return function checkJsonContentTypeMiddleware (req, res, next) { 5 | if (req.method === 'POST' && !req.is('application/json')) { 6 | return next(new Error('POST data must be of type application/json')); 7 | } 8 | 9 | next(); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/api/middlewares/check-static-image-format.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const VALID_IMAGE_FORMATS = ['png', 'jpg']; 4 | 5 | module.exports = function checkStaticImageFormat () { 6 | return function checkStaticImageFormatMiddleware (req, res, next) { 7 | if (!VALID_IMAGE_FORMATS.includes(req.params.format)) { 8 | return next(new Error(`Unsupported image format "${req.params.format}"`)); 9 | } 10 | 11 | next(); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/api/middlewares/clean-up-query-params.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('underscore'); 4 | 5 | // Whitelist query parameters and attach format 6 | const REQUEST_QUERY_PARAMS_WHITELIST = [ 7 | 'config', 8 | 'map_key', 9 | 'api_key', 10 | 'auth_token', 11 | 'callback', 12 | 'zoom', 13 | 'lon', 14 | 'lat', 15 | // analysis 16 | 'filters' // json 17 | ]; 18 | 19 | module.exports = function cleanUpQueryParamsMiddleware (customQueryParams = []) { 20 | if (!Array.isArray(customQueryParams)) { 21 | throw new Error('customQueryParams must receive an Array of params'); 22 | } 23 | 24 | return function cleanUpQueryParams (req, res, next) { 25 | const allowedQueryParams = [...REQUEST_QUERY_PARAMS_WHITELIST, ...customQueryParams]; 26 | 27 | req.query = _.pick(req.query, allowedQueryParams); 28 | 29 | next(); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/api/middlewares/client-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function clientHeader () { 4 | return function clientHeaderMiddleware (req, res, next) { 5 | const { client } = req.query; 6 | 7 | if (client) { 8 | res.set('Carto-Client', client); 9 | } 10 | 11 | return next(); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/api/middlewares/coordinates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const positiveIntegerNumberRegExp = /^\d+$/; 4 | const integerNumberRegExp = /^-?\d+$/; 5 | const invalidZoomMessage = function (zoom) { 6 | return `Invalid zoom value (${zoom}). It should be an integer number greather than or equal to 0`; 7 | }; 8 | const invalidCoordXMessage = function (x) { 9 | return `Invalid coodinate 'x' value (${x}). It should be an integer number`; 10 | }; 11 | const invalidCoordYMessage = function (y) { 12 | return `Invalid coodinate 'y' value (${y}). It should be an integer number greather than or equal to 0`; 13 | }; 14 | 15 | module.exports = function coordinates (validate = { z: true, x: true, y: true }) { 16 | return function coordinatesMiddleware (req, res, next) { 17 | const { z, x, y } = req.params; 18 | 19 | if (validate.z && !positiveIntegerNumberRegExp.test(z)) { 20 | const err = new Error(invalidZoomMessage(z)); 21 | err.http_status = 400; 22 | 23 | return next(err); 24 | } 25 | 26 | // Negative values for x param are valid. The x param is wrapped 27 | if (validate.x && !integerNumberRegExp.test(x)) { 28 | const err = new Error(invalidCoordXMessage(x)); 29 | err.http_status = 400; 30 | 31 | return next(err); 32 | } 33 | 34 | if (validate.y && !positiveIntegerNumberRegExp.test(y)) { 35 | const err = new Error(invalidCoordYMessage(y)); 36 | err.http_status = 400; 37 | 38 | return next(err); 39 | } 40 | 41 | next(); 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/api/middlewares/cors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function cors () { 4 | return function corsMiddleware (req, res, next) { 5 | const headers = [ 6 | 'X-Requested-With', 7 | 'X-Prototype-Version', 8 | 'X-CSRF-Token', 9 | 'Authorization', 10 | 'Carto-Event', 11 | 'Carto-Event-Source', 12 | 'Carto-Event-Group-Id' 13 | ]; 14 | 15 | if (req.method === 'OPTIONS') { 16 | headers.push('Content-Type'); 17 | } 18 | 19 | res.set('Access-Control-Allow-Origin', '*'); 20 | res.set('Access-Control-Allow-Headers', headers.join(', ')); 21 | 22 | next(); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /lib/api/middlewares/credentials.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const basicAuth = require('basic-auth'); 4 | 5 | module.exports = function credentials () { 6 | return function credentialsMiddleware (req, res, next) { 7 | const apikeyCredentials = getApikeyCredentialsFromRequest(req); 8 | 9 | res.locals.api_key = apikeyCredentials.token; 10 | res.locals.basicAuthUsername = apikeyCredentials.username; 11 | res.set('vary', 'Authorization'); // Honor Authorization header when caching. 12 | 13 | return next(); 14 | }; 15 | }; 16 | 17 | function getApikeyCredentialsFromRequest (req) { 18 | let apikeyCredentials = { 19 | token: null, 20 | username: null 21 | }; 22 | 23 | for (const getter of apikeyGetters) { 24 | apikeyCredentials = getter(req); 25 | if (apikeyTokenFound(apikeyCredentials)) { 26 | break; 27 | } 28 | } 29 | 30 | return apikeyCredentials; 31 | } 32 | 33 | const apikeyGetters = [ 34 | getApikeyTokenFromHeaderAuthorization, 35 | getApikeyTokenFromRequestQueryString, 36 | getApikeyTokenFromRequestBody 37 | ]; 38 | 39 | function getApikeyTokenFromHeaderAuthorization (req) { 40 | const credentials = basicAuth(req); 41 | 42 | if (credentials) { 43 | return { 44 | username: credentials.username, 45 | token: credentials.pass 46 | }; 47 | } else { 48 | return { 49 | username: null, 50 | token: null 51 | }; 52 | } 53 | } 54 | 55 | function getApikeyTokenFromRequestQueryString (req) { 56 | let token = null; 57 | 58 | if (req.query && req.query.api_key) { 59 | token = req.query.api_key; 60 | } else if (req.query && req.query.map_key) { 61 | token = req.query.map_key; 62 | } 63 | 64 | return { 65 | username: null, 66 | token: token 67 | }; 68 | } 69 | 70 | function getApikeyTokenFromRequestBody (req) { 71 | let token = null; 72 | 73 | if (req.body && req.body.api_key) { 74 | token = req.body.api_key; 75 | } else if (req.body && req.body.map_key) { 76 | token = req.body.map_key; 77 | } 78 | 79 | return { 80 | username: null, 81 | token: token 82 | }; 83 | } 84 | 85 | function apikeyTokenFound (apikey) { 86 | return !!apikey && !!apikey.token; 87 | } 88 | -------------------------------------------------------------------------------- /lib/api/middlewares/db-conn-setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('underscore'); 4 | 5 | module.exports = function dbConnSetup (pgConnection) { 6 | return function dbConnSetupMiddleware (req, res, next) { 7 | const { user } = res.locals; 8 | 9 | pgConnection.setDBConn(user, res.locals, (err) => { 10 | if (err) { 11 | if (err.message && err.message.indexOf('name not found') !== -1) { 12 | err.http_status = 404; 13 | } 14 | 15 | return next(err); 16 | } 17 | 18 | _.defaults(res.locals, { 19 | dbuser: global.environment.postgres.user, 20 | dbpassword: global.environment.postgres.password, 21 | dbhost: global.environment.postgres.host, 22 | dbport: global.environment.postgres.port 23 | }); 24 | 25 | res.set('X-Served-By-DB-Host', res.locals.dbhost); 26 | 27 | next(); 28 | }); 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/api/middlewares/increment-map-view-count.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function incrementMapViewCount (metadataBackend) { 4 | return function incrementMapViewCountMiddleware (req, res, next) { 5 | const { mapConfig, user, logger } = res.locals; 6 | const statTag = mapConfig.obj().stat_tag; 7 | 8 | metadataBackend.incMapviewCount(user, statTag, (err) => { 9 | if (err) { 10 | logger.warn({ exception: err }, 'Failed to increment mapview count'); 11 | } 12 | 13 | next(); 14 | }); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/api/middlewares/initialize-status-code.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function initializeStatusCode () { 4 | return function initializeStatusCodeMiddleware (req, res, next) { 5 | if (req.method !== 'OPTIONS') { 6 | res.statusCode = 404; 7 | } 8 | 9 | next(); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/api/middlewares/last-modified-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function setLastModifiedHeader () { 4 | return function setLastModifiedHeaderMiddleware (req, res, next) { 5 | if (req.method !== 'GET') { 6 | return next(); 7 | } 8 | 9 | const { mapConfigProvider, cache_buster: cacheBuster, logger } = res.locals; 10 | 11 | if (cacheBuster) { 12 | const cacheBusterTimestamp = parseInt(cacheBuster, 10); 13 | const lastModifiedDate = Number.isFinite(cacheBusterTimestamp) && cacheBusterTimestamp !== 0 14 | ? new Date(cacheBusterTimestamp) 15 | : new Date(); 16 | 17 | res.set('Last-Modified', lastModifiedDate.toUTCString()); 18 | 19 | return next(); 20 | } 21 | 22 | mapConfigProvider.getAffectedTables((err, affectedTables) => { 23 | if (err) { 24 | logger.warn({ exception: err }, 'Error generating Last Modified Header'); 25 | return next(); 26 | } 27 | 28 | if (!affectedTables) { 29 | res.set('Last-Modified', new Date().toUTCString()); 30 | 31 | return next(); 32 | } 33 | 34 | const lastUpdatedAt = affectedTables.getLastUpdatedAt(); 35 | const lastModifiedDate = Number.isFinite(lastUpdatedAt) ? new Date(lastUpdatedAt) : new Date(); 36 | 37 | res.set('Last-Modified', lastModifiedDate.toUTCString()); 38 | 39 | res.locals.cache_buster = lastUpdatedAt; 40 | 41 | next(); 42 | }); 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/api/middlewares/last-updated-time-layergroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function setLastUpdatedTimeToLayergroup () { 4 | return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) { 5 | const { mapConfigProvider, analysesResults } = res.locals; 6 | const layergroup = res.body; 7 | 8 | mapConfigProvider.createAffectedTables((err, affectedTables) => { 9 | if (err) { 10 | return next(err); 11 | } 12 | 13 | if (!affectedTables) { 14 | res.locals.cache_buster = 0; 15 | layergroup.layergroupid = `${layergroup.layergroupid}:${res.locals.cache_buster}`; 16 | layergroup.last_updated = new Date(res.locals.cache_buster).toISOString(); 17 | 18 | return next(); 19 | } 20 | 21 | var lastUpdateTime = affectedTables.getLastUpdatedAt(); 22 | 23 | lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime; 24 | 25 | // last update for layergroup cache buster 26 | layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime; 27 | layergroup.last_updated = new Date(lastUpdateTime).toISOString(); 28 | 29 | res.locals.cache_buster = lastUpdateTime; 30 | 31 | next(); 32 | }); 33 | }; 34 | }; 35 | 36 | function getLastUpdatedTime (analysesResults, lastUpdateTime) { 37 | if (!Array.isArray(analysesResults)) { 38 | return lastUpdateTime; 39 | } 40 | return analysesResults.reduce(function (lastUpdateTime, analysis) { 41 | return analysis.getNodes().reduce(function (lastNodeUpdatedAtTime, node) { 42 | var nodeUpdatedAtDate = node.getUpdatedAt(); 43 | var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0; 44 | return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime; 45 | }, lastUpdateTime); 46 | }, lastUpdateTime); 47 | } 48 | -------------------------------------------------------------------------------- /lib/api/middlewares/layer-stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function setLayerStats (pgConnection, statsBackend) { 4 | return function setLayerStatsMiddleware (req, res, next) { 5 | const { user, mapConfig } = res.locals; 6 | const layergroup = res.body; 7 | 8 | pgConnection.getConnection(user, (err, connection) => { 9 | if (err) { 10 | return next(err); 11 | } 12 | 13 | statsBackend.getStats(mapConfig, connection, function (err, layersStats) { 14 | if (err) { 15 | return next(err); 16 | } 17 | 18 | if (layersStats.length > 0) { 19 | layergroup.metadata.layers.forEach(function (layer, index) { 20 | layer.meta.stats = layersStats[index]; 21 | }); 22 | } 23 | 24 | next(); 25 | }); 26 | }); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /lib/api/middlewares/layergroup-id-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function setLayergroupIdHeader (templateMaps, useTemplateHash) { 4 | return function setLayergroupIdHeaderMiddleware (req, res, next) { 5 | const { user, template } = res.locals; 6 | const layergroup = res.body; 7 | 8 | if (useTemplateHash) { 9 | const templateHash = templateMaps.fingerPrint(template).substring(0, 8); 10 | layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`; 11 | res.locals.templateHash = templateHash; 12 | } 13 | 14 | res.set('X-Layergroup-Id', layergroup.layergroupid); 15 | 16 | next(); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/api/middlewares/layergroup-metadata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function setMetadataToLayergroup (layergroupMetadata, includeQuery) { 4 | return function setMetadataToLayergroupMiddleware (req, res, next) { 5 | const { user, mapConfig, analysesResults = [], context, api_key: userApiKey } = res.locals; 6 | const layergroup = res.body; 7 | 8 | layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapConfig.obj()); 9 | layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery); 10 | layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context); 11 | layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context); 12 | layergroupMetadata.addDateWrappingMetadata(layergroup, mapConfig.obj()); 13 | layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig, userApiKey); 14 | 15 | next(); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/api/middlewares/layergroup-token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const LayergroupToken = require('../../models/layergroup-token'); 4 | const authErrorMessageTemplate = function (signer, user) { 5 | return `Cannot use map signature of user "${signer}" on db of user "${user}"`; 6 | }; 7 | 8 | module.exports = function layergroupToken () { 9 | return function layergroupTokenMiddleware (req, res, next) { 10 | const user = res.locals.user; 11 | const layergroupToken = LayergroupToken.parse(req.params.token); 12 | 13 | res.locals.token = layergroupToken.token; 14 | res.locals.cache_buster = layergroupToken.cacheBuster; 15 | 16 | if (layergroupToken.templateHash) { 17 | res.locals.templateHash = layergroupToken.templateHash; 18 | } 19 | 20 | if (layergroupToken.signer) { 21 | res.locals.signer = layergroupToken.signer; 22 | 23 | if (res.locals.signer !== user) { 24 | const err = new Error(authErrorMessageTemplate(res.locals.signer, user)); 25 | err.type = 'auth'; 26 | err.http_status = (req.query && req.query.callback) ? 200 : 403; 27 | 28 | return next(err); 29 | } 30 | } 31 | 32 | return next(); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/api/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uuid = require('uuid'); 4 | 5 | module.exports = function initLogger ({ logger }) { 6 | return function initLoggerMiddleware (req, res, next) { 7 | res.locals.logger = logger.child({ request_id: req.get('X-Request-Id') || uuid.v4() }); 8 | res.locals.logger.info({ client_request: req }, 'Incoming request'); 9 | res.on('finish', () => res.locals.logger.info({ server_response: res, status: res.statusCode }, 'Response sent')); 10 | res.on('close', () => res.locals.logger.info({ end: true }, 'Request done')); 11 | next(); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/api/middlewares/lzma.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const LZMA = require('lzma').LZMA; 4 | 5 | module.exports = function lzma () { 6 | const lzmaWorker = new LZMA(); 7 | 8 | return function lzmaMiddleware (req, res, next) { 9 | if (!Object.prototype.hasOwnProperty.call(req.query, 'lzma')) { 10 | return next(); 11 | } 12 | 13 | // Decode (from base64) 14 | var lzma = Buffer.from(req.query.lzma, 'base64') 15 | .toString('binary') 16 | .split('') 17 | .map(function (c) { 18 | return c.charCodeAt(0) - 128; 19 | }); 20 | 21 | // Decompress 22 | lzmaWorker.decompress(lzma, function (result) { 23 | try { 24 | delete req.query.lzma; 25 | Object.assign(req.query, JSON.parse(result)); 26 | 27 | next(); 28 | } catch (err) { 29 | next(new Error('Error parsing lzma as JSON: ' + err)); 30 | } 31 | }); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/api/middlewares/map-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function mapError (options) { 4 | const { addContext = false, label = 'MAPS CONTROLLER' } = options; 5 | 6 | return function mapErrorMiddleware (err, req, res, next) { 7 | const { mapConfig } = res.locals; 8 | 9 | if (addContext) { 10 | err = Number.isFinite(err.layerIndex) ? populateError(err, mapConfig) : err; 11 | } 12 | 13 | err.label = label; 14 | 15 | next(err); 16 | }; 17 | }; 18 | 19 | function populateError (err, mapConfig) { 20 | var error = new Error(err.message); 21 | error.http_status = err.http_status; 22 | 23 | if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) { 24 | error.http_status = 400; 25 | } 26 | 27 | error.type = 'layer'; 28 | error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined; 29 | error.layer = { 30 | id: mapConfig.getLayerId(err.layerIndex), 31 | index: err.layerIndex, 32 | type: mapConfig.layerType(err.layerIndex) 33 | }; 34 | 35 | return error; 36 | } 37 | -------------------------------------------------------------------------------- /lib/api/middlewares/map-store-map-config-provider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MapStoreMapConfigProvider = require('../../models/mapconfig/provider/map-store-provider'); 4 | 5 | module.exports = function createMapStoreMapConfigProvider ( 6 | mapStore, 7 | userLimitsBackend, 8 | pgConnection, 9 | affectedTablesCache, 10 | forcedFormat = null 11 | ) { 12 | return function createMapStoreMapConfigProviderMiddleware (req, res, next) { 13 | const { user, token, cache_buster: cacheBuster, api_key: apiKey } = res.locals; 14 | const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals; 15 | const { layer: layerFromParams, z, x, y, scale_factor: scaleFactor, format } = req.params; 16 | const { layer: layerFromQuery } = req.query; 17 | 18 | const params = { 19 | user, 20 | token, 21 | cache_buster: cacheBuster, 22 | api_key: apiKey, 23 | dbuser, 24 | dbname, 25 | dbpassword, 26 | dbhost, 27 | dbport, 28 | layer: (layerFromQuery || layerFromParams), 29 | z, 30 | x, 31 | y, 32 | scale_factor: scaleFactor, 33 | format 34 | }; 35 | 36 | if (forcedFormat) { 37 | params.format = forcedFormat; 38 | params.layer = params.layer || 'all'; 39 | } 40 | 41 | res.locals.mapConfigProvider = new MapStoreMapConfigProvider( 42 | mapStore, 43 | user, 44 | userLimitsBackend, 45 | pgConnection, 46 | affectedTablesCache, 47 | params 48 | ); 49 | 50 | next(); 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/api/middlewares/named-map-provider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function getNamedMapProvider ({ namedMapProviderCache, label, forcedFormat = null }) { 4 | return function getNamedMapProviderMiddleware (req, res, next) { 5 | const { user, token, cache_buster: cacheBuster, api_key: apiKey } = res.locals; 6 | const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals; 7 | const { template_id: templateId, layer: layerFromParams, z, x, y, format } = req.params; 8 | const { layer: layerFromQuery } = req.query; 9 | 10 | const params = { 11 | user, 12 | token, 13 | cache_buster: cacheBuster, 14 | api_key: apiKey, 15 | dbuser, 16 | dbname, 17 | dbpassword, 18 | dbhost, 19 | dbport, 20 | template_id: templateId, 21 | layer: (layerFromQuery || layerFromParams), 22 | z, 23 | x, 24 | y, 25 | format 26 | }; 27 | 28 | if (forcedFormat) { 29 | params.format = forcedFormat; 30 | params.layer = params.layer || 'all'; 31 | } 32 | 33 | const { config, auth_token: authToken } = req.query; 34 | 35 | namedMapProviderCache.get(user, templateId, config, authToken, params, (err, namedMapProvider) => { 36 | if (err) { 37 | err.label = label; 38 | return next(err); 39 | } 40 | 41 | res.locals.mapConfigProvider = namedMapProvider; 42 | 43 | next(); 44 | }); 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/api/middlewares/noop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function noop () { 4 | return function noopMiddleware (req, res, next) { 5 | next(); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/api/middlewares/profiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Profiler = require('../../stats/profiler-proxy'); 4 | const debug = require('debug')('windshaft:cartodb:stats'); 5 | const { name: prefix } = require('../../../package.json'); 6 | 7 | module.exports = function profiler (options) { 8 | const { enabled = true, statsClient } = options; 9 | 10 | return function profilerMiddleware (req, res, next) { 11 | const { logger } = res.locals; 12 | 13 | // TODO: stop using profiler and log stats instead of adding them to the profiler 14 | req.profiler = new Profiler({ 15 | statsd_client: statsClient, 16 | profile: enabled 17 | }); 18 | 19 | req.profiler.start(prefix); 20 | 21 | res.on('finish', () => { 22 | req.profiler.done('response'); 23 | req.profiler.end(); 24 | const stats = req.profiler.toJSON(); 25 | logger.info({ stats, duration: stats.response / 1000, duration_ms: stats.response }, 'Request profiling stats'); 26 | 27 | try { 28 | // May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166 29 | req.profiler.sendStats(); 30 | } catch (err) { 31 | debug('error sending profiling stats: ' + err); 32 | } 33 | }); 34 | 35 | next(); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/api/middlewares/rate-limit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RATE_LIMIT_ENDPOINTS_GROUPS = { 4 | ANONYMOUS: 'anonymous', 5 | STATIC: 'static', 6 | STATIC_NAMED: 'static_named', 7 | DATAVIEW: 'dataview', 8 | DATAVIEW_SEARCH: 'dataview_search', 9 | ANALYSIS: 'analysis', 10 | ANALYSIS_CATALOG: 'analysis_catalog', 11 | TILE: 'tile', 12 | ATTRIBUTES: 'attributes', 13 | NAMED_LIST: 'named_list', 14 | NAMED_CREATE: 'named_create', 15 | NAMED_GET: 'named_get', 16 | NAMED: 'named', 17 | NAMED_UPDATE: 'named_update', 18 | NAMED_DELETE: 'named_delete', 19 | NAMED_TILES: 'named_tiles' 20 | }; 21 | 22 | function rateLimit (userLimitsBackend, endpointGroup = null) { 23 | if (!isRateLimitEnabled(endpointGroup)) { 24 | return function rateLimitDisabledMiddleware (req, res, next) { next(); }; 25 | } 26 | 27 | return function rateLimitMiddleware (req, res, next) { 28 | userLimitsBackend.getRateLimit(res.locals.user, endpointGroup, function (err, userRateLimit) { 29 | if (err) { 30 | return next(err); 31 | } 32 | 33 | if (!userRateLimit) { 34 | return next(); 35 | } 36 | 37 | const [isBlocked, limit, remaining, retry, reset] = userRateLimit; 38 | 39 | res.set({ 40 | 'Carto-Rate-Limit-Limit': limit, 41 | 'Carto-Rate-Limit-Remaining': remaining, 42 | 'Carto-Rate-Limit-Reset': reset 43 | }); 44 | 45 | if (isBlocked) { 46 | // retry is floor rounded in seconds by redis-cell 47 | res.set('Retry-After', retry + 1); 48 | 49 | const rateLimitError = new Error( 50 | 'You are over platform\'s limits: too many requests.' + 51 | ' Please contact us to know more details' 52 | ); 53 | rateLimitError.http_status = 429; 54 | rateLimitError.type = 'limit'; 55 | rateLimitError.subtype = 'rate-limit'; 56 | return next(rateLimitError); 57 | } 58 | 59 | return next(); 60 | }); 61 | }; 62 | } 63 | 64 | function isRateLimitEnabled (endpointGroup) { 65 | return global.environment.enabledFeatures.rateLimitsEnabled && 66 | endpointGroup && 67 | global.environment.enabledFeatures.rateLimitsByEndpoint[endpointGroup]; 68 | } 69 | 70 | module.exports = rateLimit; 71 | module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS; 72 | -------------------------------------------------------------------------------- /lib/api/middlewares/send-response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const setCommonHeaders = require('../../utils/common-headers'); 4 | 5 | module.exports = function sendResponse () { 6 | return function sendResponseMiddleware (req, res, next) { 7 | setCommonHeaders(req, res, () => { 8 | res.status(res.statusCode); 9 | 10 | if (Buffer.isBuffer(res.body)) { 11 | res.send(res.body); 12 | return next(); 13 | } 14 | 15 | if (req.query.callback) { 16 | res.jsonp(res.body); 17 | return next(); 18 | } 19 | 20 | res.json(res.body); 21 | return next(); 22 | }); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /lib/api/middlewares/served-by-host-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | 5 | module.exports = function servedByHostHeader () { 6 | const hostname = os.hostname().split('.')[0]; 7 | 8 | return function servedByHostHeaderMiddleware (req, res, next) { 9 | res.set('X-Served-By-Host', hostname); 10 | 11 | next(); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/api/middlewares/surrogate-key-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const NamedMapsCacheEntry = require('../../cache/model/named-maps-entry'); 4 | const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider'); 5 | 6 | module.exports = function setSurrogateKeyHeader ({ surrogateKeysCache }) { 7 | return function setSurrogateKeyHeaderMiddleware (req, res, next) { 8 | const { user, mapConfigProvider, logger } = res.locals; 9 | 10 | if (mapConfigProvider instanceof NamedMapMapConfigProvider) { 11 | surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, mapConfigProvider.getTemplateName())); 12 | } 13 | 14 | if (req.method !== 'GET') { 15 | return next(); 16 | } 17 | 18 | mapConfigProvider.getAffectedTables((err, affectedTables) => { 19 | if (err) { 20 | logger.warn({ exception: err }, 'Error generating Surrogate Key Header'); 21 | return next(); 22 | } 23 | 24 | if (!affectedTables || !affectedTables.tables || affectedTables.tables.length === 0) { 25 | return next(); 26 | } 27 | 28 | surrogateKeysCache.tag(res, affectedTables); 29 | 30 | next(); 31 | }); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/api/middlewares/syntax-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function syntaxError () { 4 | return function syntaxErrorMiddleware (err, req, res, next) { 5 | if (err.name === 'SyntaxError') { 6 | err.http_status = 400; 7 | err.message = `${err.name}: ${err.message}`; 8 | } 9 | 10 | next(err); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/api/middlewares/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function tag ({ tags }) { 4 | if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === 'string')) { 5 | throw new Error('Required "tags" option must be a valid Array: [string, string, ...]'); 6 | } 7 | 8 | return function tagMiddleware (req, res, next) { 9 | const { logger } = res.locals; 10 | res.locals.tags = tags; 11 | res.on('finish', () => logger.info({ tags: res.locals.tags }, 'Request tagged')); 12 | 13 | next(); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/api/middlewares/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CdbRequest = require('../../models/cdb-request'); 4 | 5 | module.exports = function user (metadataBackend) { 6 | const cdbRequest = new CdbRequest(); 7 | 8 | return function userMiddleware (req, res, next) { 9 | try { 10 | res.locals.user = getUserNameFromRequest(req, cdbRequest); 11 | res.locals.logger.info({ 'cdb-user': res.locals.user }, 'User'); 12 | } catch (err) { 13 | return next(err); 14 | } 15 | 16 | metadataBackend.getUserId(res.locals.user, (err, userId) => { 17 | if (err || !userId) { 18 | return next(); 19 | } 20 | 21 | res.locals.userId = userId; 22 | 23 | return next(); 24 | }); 25 | }; 26 | }; 27 | 28 | function getUserNameFromRequest (req, cdbRequest) { 29 | return cdbRequest.userByReq(req); 30 | } 31 | -------------------------------------------------------------------------------- /lib/api/middlewares/vector-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const timeoutErrorVectorTile = fs.readFileSync(path.join(__dirname, '/../../../assets/render-timeout-fallback.mvt')); 6 | 7 | module.exports = function vectorError () { 8 | return function vectorErrorMiddleware (err, req, res, next) { 9 | if (req.params.format === 'mvt') { 10 | if (isTimeoutError(err) || isRateLimitError(err)) { 11 | res.set('Content-Type', 'application/x-protobuf'); 12 | return res.status(429).send(timeoutErrorVectorTile); 13 | } 14 | } 15 | 16 | next(err); 17 | }; 18 | }; 19 | 20 | function isRenderTimeoutError (err) { 21 | return err.message === 'Render timed out'; 22 | } 23 | 24 | function isDatasourceTimeoutError (err) { 25 | return err.message && err.message.match(/canceling statement due to statement timeout/i); 26 | } 27 | 28 | function isTimeoutError (err) { 29 | return isRenderTimeoutError(err) || isDatasourceTimeoutError(err); 30 | } 31 | 32 | function isRateLimitError (err) { 33 | return err.type === 'limit' && err.subtype === 'rate-limit'; 34 | } 35 | -------------------------------------------------------------------------------- /lib/backends/analysis-status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PSQL = require('cartodb-psql'); 4 | 5 | function AnalysisStatusBackend () { 6 | } 7 | 8 | module.exports = AnalysisStatusBackend; 9 | 10 | AnalysisStatusBackend.prototype.getNodeStatus = function (nodeId, dbParams, callback) { 11 | var statusQuery = [ 12 | 'SELECT node_id, status, updated_at, last_error_message as error_message', 13 | 'FROM cartodb.cdb_analysis_catalog where node_id = \'' + nodeId + '\'' 14 | ].join(' '); 15 | 16 | var pg = new PSQL(dbParams); 17 | 18 | pg.query(statusQuery, function (err, result) { 19 | if (err) { 20 | return callback(err, result); 21 | } 22 | 23 | result = result || {}; 24 | 25 | var rows = result.rows || []; 26 | 27 | var statusResponse = rows[0] || { 28 | node_id: nodeId, 29 | status: 'unknown' 30 | }; 31 | 32 | if (statusResponse.status !== 'failed') { 33 | delete statusResponse.error_message; 34 | } 35 | 36 | return callback(null, statusResponse); 37 | }, true); // use read-only transaction 38 | }; 39 | -------------------------------------------------------------------------------- /lib/backends/filter-stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | var AnalysisFilter = require('../models/filter/analysis'); 5 | 6 | function FilterStatsBackends (pgQueryRunner) { 7 | this.pgQueryRunner = pgQueryRunner; 8 | } 9 | 10 | module.exports = FilterStatsBackends; 11 | 12 | function getEstimatedRows (pgQueryRunner, username, query, callback) { 13 | pgQueryRunner.run(username, 'EXPLAIN (FORMAT JSON)' + query, function (err, resultRows) { 14 | if (err) { 15 | callback(err); 16 | return; 17 | } 18 | var rows; 19 | if (resultRows[0] && resultRows[0]['QUERY PLAN'] && 20 | resultRows[0]['QUERY PLAN'][0] && resultRows[0]['QUERY PLAN'][0].Plan) { 21 | rows = resultRows[0]['QUERY PLAN'][0].Plan['Plan Rows']; 22 | } 23 | return callback(null, rows); 24 | }); 25 | } 26 | 27 | FilterStatsBackends.prototype.getFilterStats = function (username, unfilteredQuery, filters, callback) { 28 | var stats = {}; 29 | 30 | getEstimatedRows(this.pgQueryRunner, username, unfilteredQuery, (err, rows) => { 31 | if (err) { 32 | return callback(err); 33 | } 34 | 35 | stats.unfiltered_rows = rows; 36 | 37 | if (!filters || _.isEmpty(filters)) { 38 | return callback(null, stats); 39 | } 40 | 41 | var analysisFilter = new AnalysisFilter(filters); 42 | var query = analysisFilter.sql(unfilteredQuery); 43 | 44 | getEstimatedRows(this.pgQueryRunner, username, query, (err, rows) => { 45 | if (err) { 46 | return callback(err); 47 | } 48 | 49 | stats.filtered_rows = rows; 50 | return callback(null, stats); 51 | }); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /lib/backends/layer-stats/empty-layer-stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function EmptyLayerStats (types) { 4 | this._types = types || {}; 5 | } 6 | 7 | EmptyLayerStats.prototype.is = function (type) { 8 | return this._types[type] ? this._types[type] : false; 9 | }; 10 | 11 | EmptyLayerStats.prototype.getStats = 12 | function (layer, dbConnection, callback) { 13 | setImmediate(function () { 14 | callback(null, {}); 15 | }); 16 | }; 17 | 18 | module.exports = EmptyLayerStats; 19 | -------------------------------------------------------------------------------- /lib/backends/layer-stats/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LayerStats = require('./layer-stats'); 4 | var EmptyLayerStats = require('./empty-layer-stats'); 5 | var MapnikLayerStats = require('./mapnik-layer-stats'); 6 | var TorqueLayerStats = require('./torque-layer-stats'); 7 | 8 | module.exports = function LayerStatsFactory (type) { 9 | var layerStatsIterator = []; 10 | var selectedType = type || 'ALL'; 11 | 12 | if (selectedType === 'ALL') { 13 | layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true })); 14 | layerStatsIterator.push(new MapnikLayerStats()); 15 | layerStatsIterator.push(new TorqueLayerStats()); 16 | } else if (selectedType === 'mapnik') { 17 | layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true, torque: true })); 18 | layerStatsIterator.push(new MapnikLayerStats()); 19 | } else if (selectedType === 'torque') { 20 | layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true, mapnik: true })); 21 | layerStatsIterator.push(new TorqueLayerStats()); 22 | } 23 | 24 | return new LayerStats(layerStatsIterator); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/backends/layer-stats/layer-stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var queue = require('queue-async'); 4 | 5 | function LayerStats (layerStatsIterator) { 6 | this.layerStatsIterator = layerStatsIterator; 7 | } 8 | 9 | LayerStats.prototype.getStats = function (mapConfig, dbConnection, callback) { 10 | var self = this; 11 | var stats = []; 12 | 13 | if (!mapConfig.getLayers().length) { 14 | return callback(null, stats); 15 | } 16 | var metaQueue = queue(mapConfig.getLayers().length); 17 | mapConfig.getLayers().forEach(function (layer, layerId) { 18 | var layerType = mapConfig.layerType(layerId); 19 | 20 | for (var i = 0; i < self.layerStatsIterator.length; i++) { 21 | if (self.layerStatsIterator[i].is(layerType)) { 22 | var getStats = self.layerStatsIterator[i].getStats.bind(self.layerStatsIterator[i]); 23 | metaQueue.defer(getStats, layer, dbConnection); 24 | break; 25 | } 26 | } 27 | }); 28 | 29 | metaQueue.awaitAll(function (err, results) { 30 | if (err) { 31 | return callback(err); 32 | } 33 | 34 | if (!results) { 35 | return callback(null, null); 36 | } 37 | 38 | mapConfig.getLayers().forEach(function (layer, layerIndex) { 39 | stats[layerIndex] = results[layerIndex]; 40 | }); 41 | 42 | return callback(err, stats); 43 | }); 44 | }; 45 | 46 | module.exports = LayerStats; 47 | -------------------------------------------------------------------------------- /lib/backends/layer-stats/torque-layer-stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function TorqueLayerStats () { 4 | this._types = { 5 | torque: true 6 | }; 7 | } 8 | 9 | TorqueLayerStats.prototype.is = function (type) { 10 | return this._types[type] ? this._types[type] : false; 11 | }; 12 | 13 | TorqueLayerStats.prototype.getStats = 14 | function (layer, dbConnection, callback) { 15 | return callback(null, {}); 16 | }; 17 | 18 | module.exports = TorqueLayerStats; 19 | -------------------------------------------------------------------------------- /lib/backends/metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { PubSub } = require('@google-cloud/pubsub'); 4 | 5 | module.exports = class MetricsBackend { 6 | constructor (options = {}) { 7 | const { project_id: projectId, credentials: keyFilename, topic } = options; 8 | 9 | this._metricsClient = new PubSub({ projectId, keyFilename }); 10 | this._topicName = topic; 11 | } 12 | 13 | send (event, attributes) { 14 | const data = Buffer.from(event); 15 | return this._metricsClient.topic(this._topicName).publish(data, attributes); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/backends/overviews-metadata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const queryUtils = require('../utils/query-utils'); 4 | 5 | function OverviewsMetadataBackend (pgQueryRunner) { 6 | this.pgQueryRunner = pgQueryRunner; 7 | } 8 | 9 | module.exports = OverviewsMetadataBackend; 10 | 11 | OverviewsMetadataBackend.prototype.getOverviewsMetadata = function (username, sql, callback) { 12 | // FIXME: Currently using internal function _cdb_schema_name 13 | // CDB_Overviews should provide the schema information directly. 14 | const query = ` 15 | SELECT *, cartodb._cdb_schema_name(base_table) 16 | FROM cartodb.CDB_Overviews( 17 | cartodb.CDB_QueryTablesText($windshaft$${queryUtils.substituteDummyTokens(sql)}$windshaft$) 18 | ); 19 | `; 20 | this.pgQueryRunner.run(username, query, function handleOverviewsRows (err, rows) { 21 | if (err) { 22 | callback(err); 23 | return; 24 | } 25 | var metadata = rows.reduce(function (metadata, row) { 26 | var table = row.base_table; 27 | var schema = row._cdb_schema_name; 28 | if (!metadata[table]) { 29 | metadata[table] = {}; 30 | } 31 | metadata[table][row.z] = { table: row.overview_table }; 32 | metadata[table].schema = schema; 33 | return metadata; 34 | }, {}); 35 | return callback(null, metadata); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/backends/pg-query-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PSQL = require('cartodb-psql'); 4 | const dbParamsFromReqParams = require('../utils/database-params'); 5 | 6 | function PgQueryRunner (pgConnection) { 7 | this.pgConnection = pgConnection; 8 | } 9 | 10 | module.exports = PgQueryRunner; 11 | 12 | /** 13 | * Runs `query` with `username`'s PostgreSQL role, callback receives error and rows array. 14 | * 15 | * @param {String} username 16 | * @param {String} query 17 | * @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array 18 | */ 19 | PgQueryRunner.prototype.run = function (username, query, callback) { 20 | this.pgConnection.getDatabaseParams(username, (err, databaseParams) => { 21 | if (err) { 22 | return callback(err); 23 | } 24 | 25 | const psql = new PSQL(dbParamsFromReqParams(databaseParams)); 26 | 27 | psql.query(query, function (err, resultSet) { 28 | resultSet = resultSet || {}; 29 | return callback(err, resultSet.rows || []); 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/backends/stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var layerStats = require('./layer-stats/factory'); 4 | 5 | function StatsBackend () { 6 | } 7 | 8 | module.exports = StatsBackend; 9 | 10 | StatsBackend.prototype.getStats = function (mapConfig, dbConnection, callback) { 11 | var enabledFeatures = global.environment.enabledFeatures; 12 | var layerStatsEnabled = enabledFeatures ? enabledFeatures.layerStats : false; 13 | if (layerStatsEnabled) { 14 | layerStats().getStats(mapConfig, dbConnection, callback); 15 | } else { 16 | return callback(null, []); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/backends/tables-extent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function TablesExtentBackend (pgQueryRunner) { 4 | this.pgQueryRunner = pgQueryRunner; 5 | } 6 | 7 | module.exports = TablesExtentBackend; 8 | 9 | /** 10 | * Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on 11 | * the_geom_webmercator (SRID 3857) column. 12 | * 13 | * @param {String} username 14 | * @param {Array} tableNames The named can be schema qualified, so this accepts both `schema_name.table_name` and 15 | * `table_name` format as valid input 16 | * @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north` 17 | */ 18 | TablesExtentBackend.prototype.getBounds = function (username, tables, callback) { 19 | var estimatedExtentSQLs = tables.map(function (table) { 20 | return "ST_EstimatedExtent('" + table.schema_name + "', '" + table.table_name + "', 'the_geom_webmercator')"; 21 | }); 22 | 23 | var query = [ 24 | 'WITH ext as (' + 25 | 'SELECT ST_Transform(ST_SetSRID(ST_Extent(ST_Union(ARRAY[', 26 | estimatedExtentSQLs.join(','), 27 | '])), 3857), 4326) geom)', 28 | 'SELECT', 29 | 'ST_XMin(geom) west,', 30 | 'ST_YMin(geom) south,', 31 | 'ST_XMax(geom) east,', 32 | 'ST_YMax(geom) north', 33 | 'FROM ext' 34 | ].join(' '); 35 | 36 | this.pgQueryRunner.run(username, query, function handleBoundsResult (err, rows) { 37 | if (err) { 38 | var msg = err.message ? err.message : err; 39 | return callback(new Error('could not fetch source tables: ' + msg)); 40 | } 41 | var result = null; 42 | if (rows.length > 0) { 43 | result = { 44 | bbox: rows[0] 45 | }; 46 | } 47 | callback(null, result); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/backends/user-limits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 5 | * @param metadataBackend 6 | * @param options 7 | * @constructor 8 | * @type {UserLimitsBackend} 9 | */ 10 | function UserLimitsBackend (metadataBackend, options) { 11 | this.metadataBackend = metadataBackend; 12 | this.options = options || {}; 13 | this.options.limits = this.options.limits || {}; 14 | 15 | this.preprareRateLimit(); 16 | } 17 | 18 | module.exports = UserLimitsBackend; 19 | 20 | UserLimitsBackend.prototype.getRenderLimits = function (username, apiKey, callback) { 21 | var self = this; 22 | 23 | var limits = { 24 | cacheOnTimeout: self.options.limits.cacheOnTimeout || false, 25 | render: self.options.limits.render || 0 26 | }; 27 | 28 | self.getTimeoutRenderLimit(username, apiKey, function (err, timeoutRenderLimit) { 29 | if (err) { 30 | return callback(err); 31 | } 32 | 33 | if (timeoutRenderLimit && timeoutRenderLimit.render) { 34 | if (Number.isFinite(timeoutRenderLimit.render)) { 35 | limits.render = timeoutRenderLimit.render; 36 | } 37 | } 38 | 39 | return callback(null, limits); 40 | }); 41 | }; 42 | 43 | UserLimitsBackend.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) { 44 | isAuthorized(this.metadataBackend, username, apiKey, (err, authorized) => { 45 | if (err) { 46 | return callback(err); 47 | } 48 | 49 | this.metadataBackend.getUserTimeoutRenderLimits(username, (err, timeoutRenderLimit) => { 50 | if (err) { 51 | return callback(err); 52 | } 53 | 54 | return callback( 55 | null, 56 | { render: authorized ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic } 57 | ); 58 | }); 59 | }); 60 | }; 61 | 62 | function isAuthorized (metadataBackend, username, apiKey, callback) { 63 | if (!apiKey) { 64 | return callback(null, false); 65 | } 66 | 67 | metadataBackend.getUserMapKey(username, function (err, userApiKey) { 68 | if (err) { 69 | return callback(err); 70 | } 71 | 72 | return callback(null, userApiKey === apiKey); 73 | }); 74 | } 75 | 76 | UserLimitsBackend.prototype.preprareRateLimit = function () { 77 | if (this.options.limits.rateLimitsEnabled) { 78 | this.metadataBackend.loadRateLimitsScript(); 79 | } 80 | }; 81 | 82 | UserLimitsBackend.prototype.getRateLimit = function (user, endpointGroup, callback) { 83 | this.metadataBackend.getRateLimit(user, 'maps', endpointGroup, callback); 84 | }; 85 | -------------------------------------------------------------------------------- /lib/cache/backend/fastly.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var FastlyPurge = require('fastly-purge'); 4 | 5 | function FastlyCacheBackend (apiKey, serviceId) { 6 | this.serviceId = serviceId; 7 | this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: false }); 8 | } 9 | 10 | module.exports = FastlyCacheBackend; 11 | 12 | /** 13 | * @param cacheObject should respond to `key() -> String` method 14 | * @param {Function} callback 15 | */ 16 | FastlyCacheBackend.prototype.invalidate = function (cacheObject, callback) { 17 | this.fastlyPurge.key(this.serviceId, cacheObject.key(), callback); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/cache/backend/varnish-http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | 5 | function VarnishHttpCacheBackend (host, port) { 6 | this.host = host; 7 | this.port = port; 8 | } 9 | 10 | module.exports = VarnishHttpCacheBackend; 11 | 12 | /** 13 | * @param cacheObject should respond to `key() -> String` method 14 | * @param {Function} callback 15 | */ 16 | VarnishHttpCacheBackend.prototype.invalidate = function (cacheObject, callback) { 17 | request( 18 | { 19 | method: 'PURGE', 20 | url: 'http://' + this.host + ':' + this.port + '/key', 21 | headers: { 22 | 'Invalidation-Match': '\\b' + cacheObject.key() + '\\b' 23 | } 24 | }, 25 | function (err, response) { 26 | if (err || response.statusCode !== 204) { 27 | return callback(new Error('Unable to invalidate Varnish object')); 28 | } 29 | return callback(null); 30 | } 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/cache/layergroup-affected-tables.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LruCache = require('lru-cache'); 4 | 5 | function LayergroupAffectedTables () { 6 | // dbname + layergroupId -> affected tables cache 7 | this.cache = new LruCache({ max: 2000 }); 8 | } 9 | 10 | module.exports = LayergroupAffectedTables; 11 | 12 | LayergroupAffectedTables.prototype.hasAffectedTables = function (dbName, layergroupId) { 13 | return this.cache.has(createKey(dbName, layergroupId)); 14 | }; 15 | 16 | LayergroupAffectedTables.prototype.set = function (dbName, layergroupId, affectedTables) { 17 | this.cache.set(createKey(dbName, layergroupId), affectedTables); 18 | }; 19 | 20 | LayergroupAffectedTables.prototype.get = function (dbName, layergroupId) { 21 | return this.cache.get(createKey(dbName, layergroupId)); 22 | }; 23 | 24 | function createKey (dbName, layergroupId) { 25 | return dbName + ':' + layergroupId; 26 | } 27 | -------------------------------------------------------------------------------- /lib/cache/model/named-maps-entry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | 5 | function NamedMaps (owner, name) { 6 | this.namespace = 'n'; 7 | this.owner = owner; 8 | this.name = name; 9 | } 10 | 11 | module.exports = NamedMaps; 12 | 13 | NamedMaps.prototype.key = function () { 14 | return this.namespace + ':' + shortHashKey(this.owner + ':' + this.name); 15 | }; 16 | 17 | function shortHashKey (target) { 18 | return crypto.createHash('sha256').update(target).digest('base64').substring(0, 6); 19 | } 20 | -------------------------------------------------------------------------------- /lib/cache/surrogate-keys-cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var queue = require('queue-async'); 4 | 5 | /** 6 | * @param {Array|Object} cacheBackends each backend backend should respond to `invalidate(cacheObject, callback)` method 7 | * @constructor 8 | */ 9 | function SurrogateKeysCache (cacheBackends) { 10 | this.cacheBackends = Array.isArray(cacheBackends) ? cacheBackends : [cacheBackends]; 11 | } 12 | 13 | module.exports = SurrogateKeysCache; 14 | 15 | /** 16 | * @param response should respond to `header(key, value)` method 17 | * @param cacheObject should respond to `key() -> String` method 18 | */ 19 | SurrogateKeysCache.prototype.tag = function (response, cacheObject) { 20 | var newKey = cacheObject.key(); 21 | response.set('Surrogate-Key', appendSurrogateKey( 22 | response.get('Surrogate-Key'), 23 | Array.isArray(newKey) ? cacheObject.key().join(' ') : newKey 24 | )); 25 | }; 26 | 27 | function appendSurrogateKey (currentKey, newKey) { 28 | if (currentKey) { 29 | newKey = currentKey + ' ' + newKey; 30 | } 31 | return newKey; 32 | } 33 | 34 | /** 35 | * @param cacheObject should respond to `key() -> String` method 36 | * @param {Function} callback 37 | */ 38 | SurrogateKeysCache.prototype.invalidate = function (cacheObject, callback) { 39 | var invalidationQueue = queue(this.cacheBackends.length); 40 | 41 | this.cacheBackends.forEach(function (cacheBackend) { 42 | invalidationQueue.defer(function (cacheBackend, done) { 43 | cacheBackend.invalidate(cacheObject, done); 44 | }, cacheBackend); 45 | }); 46 | 47 | invalidationQueue.awaitAll(function (err, result) { 48 | if (err) { 49 | return callback(err); 50 | } 51 | callback(null, result); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /lib/models/cdb-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class CdbRequest { 4 | constructor () { 5 | // would extract "strk" from "strk.cartodb.com" 6 | this.RE_USER_FROM_HOST = new RegExp(global.environment.user_from_host || '^([^\\.]+)\\.'); 7 | } 8 | 9 | userByReq (req) { 10 | const host = req.headers.host || ''; 11 | 12 | if (req.params.user) { 13 | return req.params.user; 14 | } 15 | 16 | const mat = host.match(this.RE_USER_FROM_HOST); 17 | 18 | if (!mat || mat.length !== 2) { 19 | throw new Error(`No username found in hostname '${host}'`); 20 | } 21 | 22 | return mat[1]; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/models/dataview/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FLOAT_OIDS = { 4 | 700: true, 5 | 701: true, 6 | 1700: true 7 | }; 8 | 9 | const DATE_OIDS = { 10 | 1082: true, 11 | 1114: true, 12 | 1184: true 13 | }; 14 | 15 | const columnTypeQueryTpl = ctx => `SELECT pg_typeof(${ctx.column})::oid FROM (${ctx.query}) _cdb_column_type limit 1`; 16 | 17 | function getPGTypeName (pgType) { 18 | return { 19 | float: Object.prototype.hasOwnProperty.call(FLOAT_OIDS, pgType), 20 | date: Object.prototype.hasOwnProperty.call(DATE_OIDS, pgType) 21 | }; 22 | } 23 | 24 | module.exports = class BaseDataview { 25 | getResult (psql, override, callback) { 26 | this.sql(psql, override, (err, query) => { 27 | if (err) { 28 | return callback(err); 29 | } 30 | 31 | psql.query(query, (err, result) => { 32 | if (err) { 33 | return callback(err, result); 34 | } 35 | 36 | result = this.format(result, override); 37 | result.type = this.getType(); 38 | 39 | return callback(null, result); 40 | }, true); // use read-only transaction 41 | }); 42 | } 43 | 44 | search (psql, userQuery, callback) { 45 | return callback(null, this.format({ rows: [] })); 46 | } 47 | 48 | getColumnType (psql, column, query, callback) { 49 | const readOnlyTransaction = true; 50 | const columnTypeQuery = columnTypeQueryTpl({ column, query }); 51 | 52 | psql.query(columnTypeQuery, (err, result) => { 53 | if (err) { 54 | return callback(err); 55 | } 56 | 57 | if (!result || !result.rows || !result.rows.length) { 58 | return callback(new Error('The column type could not be determined')); 59 | } 60 | 61 | const pgType = result.rows[0].pg_typeof; 62 | callback(null, getPGTypeName(pgType)); 63 | }, readOnlyTransaction); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /lib/models/dataview/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dataviews = require('.'); 4 | 5 | module.exports = class DataviewFactory { 6 | static get dataviews () { 7 | return Object.keys(dataviews).reduce((allDataviews, dataviewClassName) => { 8 | allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName]; 9 | return allDataviews; 10 | }, {}); 11 | } 12 | 13 | static getDataview (query, dataviewDefinition) { 14 | const { type, options, sql } = dataviewDefinition; 15 | 16 | if (!this.dataviews[type]) { 17 | throw new Error('Invalid dataview type: "' + type + '"'); 18 | } 19 | 20 | return new this.dataviews[type](query, options, sql); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/models/dataview/histograms/base-histogram.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseDataview = require('../base'); 4 | 5 | const TYPE = 'histogram'; 6 | 7 | module.exports = class BaseHistogram extends BaseDataview { 8 | constructor (query, options, queries) { 9 | super(); 10 | 11 | if (typeof options.column !== 'string') { 12 | throw new Error('Histogram expects `column` in widget options'); 13 | } 14 | 15 | this.query = query; 16 | this.queries = queries; 17 | this.column = options.column; 18 | this.bins = options.bins; 19 | 20 | this._columnType = null; 21 | } 22 | 23 | sql (psql, override, callback) { 24 | if (!callback) { 25 | callback = override; 26 | override = {}; 27 | } 28 | 29 | if (this._columnType === null) { 30 | this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => { 31 | // assume numeric, will fail later 32 | this._columnType = 'numeric'; 33 | if (!err && !!type) { 34 | this._columnType = Object.keys(type).find(function (key) { 35 | return type[key]; 36 | }); 37 | } 38 | this.sql(psql, override, callback); 39 | }, true); // use read-only transaction 40 | return null; 41 | } 42 | 43 | return this._buildQuery(psql, override, callback); 44 | } 45 | 46 | format (result, override) { 47 | const histogram = this._getSummary(result, override); 48 | histogram.bins = this._getBuckets(result); 49 | return histogram; 50 | } 51 | 52 | getType () { 53 | return TYPE; 54 | } 55 | 56 | toString () { 57 | return JSON.stringify({ 58 | _type: TYPE, 59 | _column: this.column, 60 | _query: this.query 61 | }); 62 | } 63 | 64 | _hasOverridenRange (override) { 65 | return override && Object.prototype.hasOwnProperty.call(override, 'start') && Object.prototype.hasOwnProperty.call(override, 'end'); 66 | } 67 | 68 | _getBinStart (override = {}) { 69 | if (this._hasOverridenRange(override)) { 70 | return Math.min(override.start, override.end); 71 | } 72 | 73 | return override.start || 0; 74 | } 75 | 76 | _getBinEnd (override = {}) { 77 | if (this._hasOverridenRange(override)) { 78 | return Math.max(override.start, override.end); 79 | } 80 | 81 | return override.end || 0; 82 | } 83 | 84 | _getBinsCount (override = {}) { 85 | return override.bins || 0; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /lib/models/dataview/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | Aggregation: require('./aggregation'), 5 | Formula: require('./formula'), 6 | Histogram: require('./histogram') 7 | }; 8 | -------------------------------------------------------------------------------- /lib/models/dataview/overviews/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parentFactory = require('../factory'); 4 | var dataviews = require('.'); 5 | 6 | function OverviewsDataviewFactory (queryRewriter, queryRewriteData, options) { 7 | this.queryRewriter = queryRewriter; 8 | this.queryRewriteData = queryRewriteData; 9 | this.options = options; 10 | } 11 | 12 | OverviewsDataviewFactory.prototype.getDataview = function (query, dataviewDefinition) { 13 | var type = dataviewDefinition.type; 14 | var dataviews = OverviewsDataviewMetaFactory.dataviews; 15 | if (!this.queryRewriter || !this.queryRewriteData || !dataviews[type]) { 16 | return parentFactory.getDataview(query, dataviewDefinition); 17 | } 18 | return new dataviews[type]( 19 | query, dataviewDefinition.options, this.queryRewriter, this.queryRewriteData, this.options, 20 | dataviewDefinition.sql 21 | ); 22 | }; 23 | 24 | var OverviewsDataviewMetaFactory = { 25 | dataviews: Object.keys(dataviews).reduce(function (allDataviews, dataviewClassName) { 26 | allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName]; 27 | return allDataviews; 28 | }, {}), 29 | 30 | getFactory: function (queryRewriter, queryRewriteData, options) { 31 | return new OverviewsDataviewFactory(queryRewriter, queryRewriteData, options); 32 | } 33 | }; 34 | 35 | module.exports = OverviewsDataviewMetaFactory; 36 | -------------------------------------------------------------------------------- /lib/models/dataview/overviews/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | Aggregation: require('./aggregation'), 5 | Formula: require('./formula'), 6 | Histogram: require('./histogram') 7 | }; 8 | -------------------------------------------------------------------------------- /lib/models/filter/analysis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var filters = { 4 | category: require('./analysis/category'), 5 | range: require('./analysis/range') 6 | }; 7 | 8 | function createFilter (filterDefinition) { 9 | var filterType = filterDefinition.type.toLowerCase(); 10 | if (!Object.prototype.hasOwnProperty.call(filters, filterType)) { 11 | throw new Error('Unknown filter type: ' + filterType); 12 | } 13 | return new filters[filterType](filterDefinition.column, filterDefinition.params); 14 | } 15 | 16 | function AnalysisFilters (filters) { 17 | this.filters = filters; 18 | } 19 | 20 | AnalysisFilters.prototype.sql = function (rawSql) { 21 | var filters = this.filters || {}; 22 | var applyFilters = {}; 23 | 24 | return Object.keys(filters) 25 | .filter(function (filterName) { 26 | return Object.prototype.hasOwnProperty.call(applyFilters, filterName) ? applyFilters[filterName] : true; 27 | }) 28 | .map(function (filterName) { 29 | var filterDefinition = filters[filterName]; 30 | return createFilter(filterDefinition); 31 | }) 32 | .reduce(function (sql, filter) { 33 | return filter.sql(sql); 34 | }, rawSql); 35 | }; 36 | 37 | module.exports = AnalysisFilters; 38 | -------------------------------------------------------------------------------- /lib/models/filter/analysis/range.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dot = require('dot'); 4 | dot.templateSettings.strip = false; 5 | 6 | var betweenFilterTpl = dot.template('{{=it._column}} BETWEEN {{=it._min}} AND {{=it._max}}'); 7 | var minFilterTpl = dot.template('{{=it._column}} >= {{=it._min}}'); 8 | var maxFilterTpl = dot.template('{{=it._column}} <= {{=it._max}}'); 9 | var filterQueryTpl = dot.template('SELECT * FROM ({{=it._sql}}) _analysis_range_filter WHERE {{=it._filter}}'); 10 | 11 | function Range (column, filterParams) { 12 | this.column = column; 13 | 14 | if (!Number.isFinite(filterParams.min) && !Number.isFinite(filterParams.max)) { 15 | throw new Error('Range filter expect to have at least one value in min or max numeric params'); 16 | } 17 | 18 | this.min = filterParams.min; 19 | this.max = filterParams.max; 20 | this.columnType = filterParams.columnType; 21 | } 22 | 23 | module.exports = Range; 24 | 25 | Range.prototype.sql = function (rawSql) { 26 | var minMaxFilter; 27 | if (Number.isFinite(this.min) && Number.isFinite(this.max)) { 28 | minMaxFilter = betweenFilterTpl({ 29 | _column: this.column, 30 | _min: this.min, 31 | _max: this.max 32 | }); 33 | } else if (Number.isFinite(this.min)) { 34 | minMaxFilter = minFilterTpl({ _column: this.column, _min: this.min }); 35 | } else { 36 | minMaxFilter = maxFilterTpl({ _column: this.column, _max: this.max }); 37 | } 38 | 39 | return filterQueryTpl({ 40 | _sql: rawSql, 41 | _filter: minMaxFilter 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /lib/models/filter/circle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('windshaft:filter:circle'); 4 | function filterQueryTpl ({ sql, column, srid, lng, lat, radiusInMeters } = {}) { 5 | return ` 6 | SELECT 7 | * 8 | FROM (${sql}) _cdb_circle_filter 9 | WHERE 10 | ST_DWithin( 11 | ${srid === 4326 ? `${column}::geography` : `ST_Transform(${column}, 4326)::geography`}, 12 | ST_SetSRID(ST_Point(${lng}, ${lat}), 4326)::geography, 13 | ${radiusInMeters} 14 | ) 15 | `; 16 | } 17 | 18 | module.exports = class CircleFilter { 19 | constructor (filterDefinition, filterParams) { 20 | const { circle } = filterParams; 21 | let _circle; 22 | 23 | if (!circle) { 24 | const error = new Error('Circle filter expects to have a "circle" param'); 25 | error.type = 'filter'; 26 | throw error; 27 | } 28 | 29 | try { 30 | _circle = JSON.parse(circle); 31 | } catch (err) { 32 | const error = new Error('Invalid circle parameter. Expected a valid JSON'); 33 | error.type = 'filter'; 34 | throw error; 35 | } 36 | 37 | const { lng, lat, radius } = _circle; 38 | 39 | if (!Number.isFinite(lng) || !Number.isFinite(lat) || !Number.isFinite(radius)) { 40 | const error = new Error('Missing parameter for Circle Filter, expected: "lng", "lat", and "radius"'); 41 | error.type = 'filter'; 42 | throw error; 43 | } 44 | 45 | this.column = filterDefinition.column || 'the_geom_webmercator'; 46 | this.srid = filterDefinition.srid || 3857; 47 | this.lng = lng; 48 | this.lat = lat; 49 | this.radius = radius; 50 | } 51 | 52 | sql (rawSql) { 53 | const circleSql = filterQueryTpl({ 54 | sql: rawSql, 55 | column: this.column, 56 | srid: this.srid, 57 | lng: this.lng, 58 | lat: this.lat, 59 | radiusInMeters: this.radius 60 | }); 61 | 62 | debug(circleSql); 63 | 64 | return circleSql; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /lib/models/filter/polygon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const debug = require('debug')('windshaft:filter:polygon'); 5 | function filterQueryTpl ({ sql, column, srid, geojson } = {}) { 6 | return ` 7 | SELECT 8 | * 9 | FROM (${sql}) _cdb_polygon_filter 10 | WHERE 11 | ST_Intersects( 12 | ${column}, 13 | ST_Transform( 14 | ST_SetSRID(ST_GeomFromGeoJSON('${JSON.stringify(geojson)}'), 4326), 15 | ${srid} 16 | ) 17 | ) 18 | `; 19 | } 20 | 21 | module.exports = class PolygonFilter { 22 | constructor (filterDefinition, filterParams) { 23 | const { polygon } = filterParams; 24 | 25 | if (!polygon) { 26 | const error = new Error('Polygon filter expects to have a "polygon" param'); 27 | error.type = 'filter'; 28 | throw error; 29 | } 30 | 31 | let geojson; 32 | 33 | try { 34 | geojson = JSON.parse(polygon); 35 | } catch (err) { 36 | const error = new Error('Invalid polygon parameter. Expected a valid GeoJSON'); 37 | error.type = 'filter'; 38 | throw error; 39 | } 40 | 41 | if (geojson.type !== 'Polygon') { 42 | const error = new Error('Invalid type of geometry. Valid ones: "Polygon"'); 43 | error.type = 'filter'; 44 | throw error; 45 | } 46 | 47 | try { 48 | const length = geojson.coordinates.length; 49 | assert.deepStrictEqual(geojson.coordinates[0], geojson.coordinates[length - 1]); 50 | } catch (error) { 51 | throw new Error('Invalid geometry, it must be a closed polygon'); 52 | } 53 | 54 | this.column = filterDefinition.column || 'the_geom_webmercator'; 55 | this.srid = filterDefinition.srid || 3857; 56 | this.geojson = geojson; 57 | } 58 | 59 | sql (rawSql) { 60 | const polygonSql = filterQueryTpl({ 61 | sql: rawSql, 62 | column: this.column, 63 | srid: this.srid, 64 | geojson: this.geojson 65 | }); 66 | 67 | debug(polygonSql); 68 | 69 | return polygonSql; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /lib/models/layergroup-token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {String} token might match the following pattern: {user}@{tpl_id}@{token}:{cache_buster} 5 | */ 6 | function parse (token) { 7 | var signer, cacheBuster, templateHash; 8 | 9 | var tokenSplit = token.split(':'); 10 | 11 | token = tokenSplit[0]; 12 | if (tokenSplit.length > 1) { 13 | cacheBuster = tokenSplit[1]; 14 | } 15 | 16 | tokenSplit = token.split('@'); 17 | if (tokenSplit.length > 1) { 18 | signer = tokenSplit.shift(); 19 | if (tokenSplit.length > 1) { 20 | templateHash = tokenSplit.shift(); 21 | } 22 | token = tokenSplit.shift(); 23 | } 24 | 25 | return { 26 | token: token, 27 | signer: signer, 28 | cacheBuster: cacheBuster, 29 | templateHash: templateHash 30 | }; 31 | } 32 | 33 | module.exports.parse = parse; 34 | -------------------------------------------------------------------------------- /lib/models/mapconfig/adapter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function MapConfigAdapter (adapters) { 4 | this.adapters = Array.isArray(adapters) ? adapters : Array.apply(null, arguments); 5 | } 6 | 7 | module.exports = MapConfigAdapter; 8 | 9 | MapConfigAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) { 10 | var self = this; 11 | var i = 0; 12 | var tasksLeft = this.adapters.length; 13 | 14 | let mapConfigStats = {}; 15 | 16 | function next (err, _requestMapConfig, adapterStats = {}) { 17 | if (err) { 18 | return callback(err); 19 | } 20 | 21 | mapConfigStats = Object.assign(mapConfigStats, adapterStats); 22 | 23 | if (tasksLeft-- === 0) { 24 | return callback(null, _requestMapConfig, mapConfigStats); 25 | } 26 | var nextAdapter = self.adapters[i++]; 27 | nextAdapter.getMapConfig(user, _requestMapConfig, params, context, next); 28 | } 29 | 30 | next(null, requestMapConfig, mapConfigStats); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/models/mapconfig/adapter/mapconfig-buffer-size-adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function MapConfigBufferSizeAdapter () { 4 | this.formats = ['png', 'png32', 'mvt', 'grid.json']; 5 | } 6 | 7 | module.exports = MapConfigBufferSizeAdapter; 8 | 9 | MapConfigBufferSizeAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) { 10 | if (!context.templateParams || !context.templateParams.buffersize) { 11 | return callback(null, requestMapConfig); 12 | } 13 | 14 | this.formats.forEach(function (format) { 15 | if (Number.isFinite(context.templateParams.buffersize[format])) { 16 | if (requestMapConfig.buffersize === undefined) { 17 | requestMapConfig.buffersize = {}; 18 | } 19 | 20 | requestMapConfig.buffersize[format] = context.templateParams.buffersize[format]; 21 | } 22 | }); 23 | 24 | setImmediate(function () { 25 | callback(null, requestMapConfig); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/models/mapconfig/adapter/sql-wrap-mapconfig-adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function SqlWrapMapConfigAdapter () { 4 | } 5 | 6 | module.exports = SqlWrapMapConfigAdapter; 7 | 8 | SqlWrapMapConfigAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) { 9 | if (requestMapConfig && Array.isArray(requestMapConfig.layers)) { 10 | requestMapConfig.layers = requestMapConfig.layers.map(function (layer) { 11 | if (layer.options) { 12 | var sqlQueryWrap = layer.options.sql_wrap; 13 | if (sqlQueryWrap) { 14 | var layerSql = layer.options.sql; 15 | if (layerSql) { 16 | layer.options.sql_raw = layerSql; 17 | layer.options.sql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, layerSql); 18 | } 19 | } 20 | } 21 | return layer; 22 | }); 23 | } 24 | 25 | return callback(null, requestMapConfig); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/models/mapconfig/adapter/vector-mapconfig-adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const queryUtils = require('../../../utils/query-utils'); 4 | const dateWrapper = require('../../../utils/date-wrapper'); 5 | 6 | /** 7 | * This middleware wraps the layer query transforming the date fields into numbers because mvt tiles 8 | * doesnt support dates as primitive type. 9 | * 10 | * - This middleware is ONLY activated when the `dates_as_numbers` option is enabled for some layer in the mapConfig. 11 | */ 12 | class VectorMapConfigAdapter { 13 | constructor (pgConnection) { 14 | this.pgConnection = pgConnection; 15 | } 16 | 17 | getMapConfig (user, requestMapConfig, params, context, callback) { 18 | if (!this._isDatesAsNumbersFlagEnabled(requestMapConfig)) { 19 | return callback(null, requestMapConfig); 20 | } 21 | 22 | this._wrapDates(requestMapConfig, user) 23 | .then(updatedRequestMapConfig => callback(null, updatedRequestMapConfig)) 24 | .catch(callback); 25 | } 26 | 27 | _wrapDates (requestMapConfig, user) { 28 | return Promise.all(requestMapConfig.layers.map(layer => this._wrapLayer(layer, user))) 29 | .then(() => requestMapConfig); 30 | } 31 | 32 | _wrapLayer (layer, user) { 33 | if (!layer.options.dates_as_numbers || !layer.options.sql) { 34 | return Promise.resolve(layer); 35 | } 36 | const originalQuery = layer.options.sql; 37 | return this._getColumns(user, originalQuery) 38 | .then(result => { 39 | const newSqlQuery = dateWrapper.wrapDates(originalQuery, result.fields); 40 | layer.options.sql = newSqlQuery; 41 | return layer; 42 | }); 43 | } 44 | 45 | _getColumns (user, originalQuery) { 46 | return new Promise((resolve, reject) => { 47 | this.pgConnection.getConnection(user, (err, connection) => { 48 | if (err) { 49 | return reject(err); 50 | } 51 | const query = queryUtils.getQueryLimited(queryUtils.substituteDummyTokens(originalQuery), 0); 52 | queryUtils.queryPromise(connection, query) 53 | .then(resolve) 54 | .catch(reject); 55 | }); 56 | }); 57 | } 58 | 59 | _isDatesAsNumbersFlagEnabled (requestMapConfig) { 60 | return requestMapConfig.layers && requestMapConfig.layers.some(layer => layer.options.dates_as_numbers); 61 | } 62 | } 63 | 64 | module.exports = VectorMapConfigAdapter; 65 | -------------------------------------------------------------------------------- /lib/models/mapconfig/provider/base-mapconfig-adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { queryTables } = require('cartodb-query-tables'); 4 | 5 | module.exports = class BaseMapConfigProvider { 6 | createAffectedTables (callback) { 7 | this.getMapConfig((err, mapConfig) => { 8 | if (err) { 9 | return callback(err); 10 | } 11 | 12 | const { dbname } = this.params; 13 | const token = mapConfig.id(); 14 | 15 | const queries = []; 16 | 17 | this.mapConfig.getLayers().forEach(layer => { 18 | queries.push(layer.options.sql); 19 | if (layer.options.affected_tables) { 20 | layer.options.affected_tables.map(table => { 21 | queries.push(`SELECT * FROM ${table} LIMIT 0`); 22 | }); 23 | } 24 | }); 25 | 26 | const sql = queries.length ? queries.join(';') : null; 27 | 28 | if (!sql) { 29 | return callback(); 30 | } 31 | 32 | this.pgConnection.getConnection(this.user, (err, connection) => { 33 | if (err) { 34 | return callback(err); 35 | } 36 | 37 | queryTables.getQueryMetadataModel(connection, sql) 38 | .then(affectedTables => { 39 | this.affectedTablesCache.set(dbname, token, affectedTables); 40 | callback(null, affectedTables); 41 | }) 42 | .catch(err => callback(err)); 43 | }); 44 | }); 45 | } 46 | 47 | getAffectedTables (callback) { 48 | this.getMapConfig((err, mapConfig) => { 49 | if (err) { 50 | return callback(err); 51 | } 52 | 53 | const { dbname } = this.params; 54 | const token = mapConfig.id(); 55 | 56 | if (this.affectedTablesCache.hasAffectedTables(dbname, token)) { 57 | const affectedTables = this.affectedTablesCache.get(dbname, token); 58 | return callback(null, affectedTables); 59 | } 60 | 61 | return this.createAffectedTables(callback); 62 | }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /lib/models/mapconfig/provider/create-layergroup-provider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MapStoreMapConfigProvider = require('./map-store-provider'); 4 | 5 | module.exports = class CreateLayergroupMapConfigProvider extends MapStoreMapConfigProvider { 6 | constructor (mapConfig, user, userLimitsBackend, pgConnection, affectedTablesCache, params) { 7 | super(null, user, userLimitsBackend, pgConnection, affectedTablesCache, params); 8 | this.mapConfig = mapConfig; 9 | } 10 | 11 | getMapConfig (callback) { 12 | if (this.mapConfig && this.params && this.context) { 13 | return callback(null, this.mapConfig, this.params, this.context); 14 | } 15 | 16 | const context = {}; 17 | 18 | this.userLimitsBackend.getRenderLimits(this.user, this.params.api_key, (err, renderLimits) => { 19 | if (err) { 20 | return callback(err); 21 | } 22 | 23 | context.limits = renderLimits; 24 | this.context = context; 25 | 26 | return callback(null, this.mapConfig, this.params, context); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/monitoring/health-check.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | function HealthCheck (disableFile) { 6 | this.disableFile = disableFile; 7 | } 8 | 9 | module.exports = HealthCheck; 10 | 11 | HealthCheck.prototype.check = function (callback) { 12 | fs.readFile(this.disableFile, function handleDisabledFile (err, data) { 13 | var disabledError = null; 14 | if (!err) { 15 | disabledError = new Error(data || 'Unknown error'); 16 | disabledError.http_status = 503; 17 | } 18 | return callback(disabledError); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/server-info-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HealthCheck = require('./monitoring/health-check'); 4 | 5 | var WELCOME_MSG = 'This is the CartoDB Maps API, ' + 6 | 'see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html'; 7 | 8 | function ServerInfoController () { 9 | this.healthConfig = global.environment.health || {}; 10 | this.healthCheck = new HealthCheck(global.environment.disabled_file); 11 | } 12 | 13 | module.exports = ServerInfoController; 14 | 15 | ServerInfoController.prototype.route = function (monitorRouter) { 16 | monitorRouter.get('/health', this.health.bind(this)); 17 | monitorRouter.get('/', this.welcome.bind(this)); 18 | }; 19 | 20 | ServerInfoController.prototype.welcome = function (req, res) { 21 | res.status(200).send(WELCOME_MSG); 22 | }; 23 | 24 | ServerInfoController.prototype.version = function (req, res) { 25 | res.status(200).send(this.versions); 26 | }; 27 | 28 | ServerInfoController.prototype.health = function (req, res) { 29 | if (this.healthConfig.enabled) { 30 | var startTime = Date.now(); 31 | this.healthCheck.check(function (err) { 32 | var ok = !err; 33 | var response = { 34 | enabled: true, 35 | ok: ok, 36 | elapsed: Date.now() - startTime 37 | }; 38 | if (err) { 39 | response.err = err.message; 40 | } 41 | res.status(ok ? 200 : 503).send(response); 42 | }); 43 | } else { 44 | res.status(200).send({ enabled: false, ok: true }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const jsonReplacer = require('./utils/json-replacer'); 5 | const ApiRouter = require('./api/api-router'); 6 | const ServerInfoController = require('./server-info-controller'); 7 | const StatsClient = require('./stats/client'); 8 | 9 | module.exports = function createServer (serverOptions) { 10 | if (!Object.prototype.hasOwnProperty.call(serverOptions, 'routes')) { 11 | throw new Error('Must initialise server with "routes" as base paths configuration'); 12 | } 13 | 14 | // Make stats client globally accessible 15 | global.statsClient = StatsClient.getInstance(serverOptions.statsd); 16 | 17 | const app = express(); 18 | 19 | app.enable('jsonp callback'); 20 | app.disable('x-powered-by'); 21 | app.disable('etag'); 22 | app.set('json replacer', jsonReplacer()); 23 | 24 | // FIXME: do not pass 'global.environment' as 'serverOptions' should keep defaults from 'global.environment' 25 | const apiRouter = new ApiRouter({ serverOptions, environmentOptions: global.environment }); 26 | 27 | apiRouter.route(app, serverOptions.routes.api); 28 | 29 | const serverInfoController = new ServerInfoController(); 30 | serverInfoController.route(app); 31 | 32 | return app; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/stats/profiler-proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Profiler = require('step-profiler'); 4 | 5 | /** 6 | * Proxy to encapsulate node-step-profiler module so there is no need to check if there is an instance 7 | */ 8 | function ProfilerProxy (opts) { 9 | this.profile = !!opts.profile; 10 | 11 | this.profiler = null; 12 | if (opts.profile) { 13 | this.profiler = new Profiler({ statsd_client: opts.statsd_client }); 14 | } 15 | } 16 | 17 | ProfilerProxy.prototype.done = function (what) { 18 | if (this.profile) { 19 | this.profiler.done(what); 20 | } 21 | }; 22 | 23 | ProfilerProxy.prototype.end = function () { 24 | if (this.profile) { 25 | this.profiler.end(); 26 | } 27 | }; 28 | 29 | ProfilerProxy.prototype.start = function (what) { 30 | if (this.profile) { 31 | this.profiler.start(what); 32 | } 33 | }; 34 | 35 | ProfilerProxy.prototype.add = function (what) { 36 | if (this.profile) { 37 | this.profiler.add(what || {}); 38 | } 39 | }; 40 | 41 | ProfilerProxy.prototype.sendStats = function () { 42 | if (this.profile) { 43 | this.profiler.sendStats(); 44 | } 45 | }; 46 | 47 | ProfilerProxy.prototype.toString = function () { 48 | return this.profile ? this.profiler.toString() : ''; 49 | }; 50 | 51 | ProfilerProxy.prototype.toJSONString = function () { 52 | return this.profile ? this.profiler.toJSONString() : '{}'; 53 | }; 54 | 55 | ProfilerProxy.prototype.toJSON = function () { 56 | return this.profile ? JSON.parse(this.profiler.toJSONString()) : {}; 57 | }; 58 | 59 | module.exports = ProfilerProxy; 60 | -------------------------------------------------------------------------------- /lib/stats/reporter/named-map-provider-cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const statKeyTemplate = ctx => `windshaft.named-map-provider-cache.${ctx.metric}`; 4 | 5 | module.exports = class NamedMapProviderCacheReporter { 6 | constructor ({ namedMapProviderCache, intervalInMilliseconds } = {}) { 7 | this.namedMapProviderCache = namedMapProviderCache; 8 | this.intervalInMilliseconds = intervalInMilliseconds || 60000; 9 | this.intervalId = null; 10 | } 11 | 12 | start () { 13 | const { providerCache: cache } = this.namedMapProviderCache; 14 | const { statsClient: stats } = global; 15 | 16 | this.intervalId = setInterval(() => { 17 | const providers = cache.dump(); 18 | stats.gauge(statKeyTemplate({ metric: 'named-map.count' }), providers.length); 19 | 20 | const namedMapInstantiations = providers.reduce((acc, { v: providers }) => { 21 | acc += Object.keys(providers).length; 22 | return acc; 23 | }, 0); 24 | 25 | stats.gauge(statKeyTemplate({ metric: 'named-map.instantiation.count' }), namedMapInstantiations); 26 | }, this.intervalInMilliseconds); 27 | } 28 | 29 | stop () { 30 | clearInterval(this.intervalId); 31 | this.intervalId = null; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /lib/stats/reporter/renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // - Reports stats about: 4 | // * Total number of renderers 5 | // * For mapnik renderers: 6 | // - the mapnik-pool status: count, unused and waiting 7 | // - the internally cached objects: png and grid 8 | 9 | function RendererStatsReporter (rendererCache, statsInterval) { 10 | this.rendererCache = rendererCache; 11 | this.statsInterval = statsInterval || 6e4; 12 | this.renderersStatsIntervalId = null; 13 | } 14 | 15 | module.exports = RendererStatsReporter; 16 | 17 | RendererStatsReporter.prototype.start = function () { 18 | this.renderersStatsIntervalId = setInterval(() => { 19 | const rendererStats = this.rendererCache.getStats(); 20 | 21 | for (const [stat, value] of rendererStats) { 22 | if (stat.startsWith('rendercache')) { 23 | global.statsClient.gauge(`windshaft.${stat}`, value); 24 | } else { 25 | global.statsClient.gauge(`windshaft.mapnik-${stat}`, value); 26 | } 27 | } 28 | }, this.statsInterval); 29 | 30 | this.rendererCache.on('err', rendererCacheErrorListener); 31 | this.rendererCache.on('gc', gcTimingListener); 32 | }; 33 | 34 | function rendererCacheErrorListener () { 35 | global.statsClient.increment('windshaft.rendercache.error'); 36 | } 37 | 38 | function gcTimingListener (gcTime) { 39 | global.statsClient.timing('windshaft.rendercache.gc', gcTime); 40 | } 41 | 42 | RendererStatsReporter.prototype.stop = function () { 43 | this.rendererCache.removeListener('err', rendererCacheErrorListener); 44 | this.rendererCache.removeListener('gc', gcTimingListener); 45 | 46 | clearInterval(this.renderersStatsIntervalId); 47 | this.renderersStatsIntervalId = null; 48 | }; 49 | -------------------------------------------------------------------------------- /lib/stats/timer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Timer () { 4 | this.times = {}; 5 | } 6 | 7 | module.exports = Timer; 8 | 9 | Timer.prototype.start = function (label) { 10 | this.timeIt(label, 'start'); 11 | }; 12 | 13 | Timer.prototype.end = function (label) { 14 | this.timeIt(label, 'end'); 15 | }; 16 | 17 | Timer.prototype.timeIt = function (label, what) { 18 | this.times[label] = this.times[label] || {}; 19 | this.times[label][what] = Date.now(); 20 | }; 21 | 22 | Timer.prototype.getTimes = function () { 23 | var self = this; 24 | var times = {}; 25 | 26 | Object.keys(this.times).forEach(function (label) { 27 | var stat = self.times[label]; 28 | if (stat.start && stat.end) { 29 | times[label] = Math.max(0, stat.end - stat.start); 30 | } 31 | }); 32 | 33 | return times; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/utils/database-params.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function getDatabaseConnectionParams (params) { 4 | const dbParams = {}; 5 | 6 | if (params.dbuser) { 7 | dbParams.user = params.dbuser; 8 | } 9 | 10 | if (params.dbpassword) { 11 | dbParams.pass = params.dbpassword; 12 | } 13 | 14 | if (params.dbhost) { 15 | dbParams.host = params.dbhost; 16 | } 17 | 18 | if (params.dbport) { 19 | dbParams.port = params.dbport; 20 | } 21 | 22 | if (params.dbname) { 23 | dbParams.dbname = params.dbname; 24 | } 25 | 26 | return dbParams; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/utils/date-wrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DATE_OIDS = Object.freeze({ 4 | 1082: 'DATE', 5 | 1083: 'TIME', 6 | 1114: 'TIMESTAMP', 7 | 1184: 'TIMESTAMPTZ', 8 | 1266: 'TIMETZ' 9 | }); 10 | 11 | /** 12 | * Wrap a query transforming all date columns into a unix epoch 13 | * @param {*} originalQuery 14 | * @param {*} fields 15 | */ 16 | function wrapDates (originalQuery, fields) { 17 | return ` 18 | SELECT 19 | ${fields.map(field => _isDateType(field) ? _castColumnToEpoch(field.name) : `"${field.name}"`).join(',')} 20 | FROM 21 | (${originalQuery}) _cdb_epoch_transformation `; 22 | } 23 | 24 | /** 25 | * @param {object} field 26 | */ 27 | function _isDateType (field) { 28 | return Object.prototype.hasOwnProperty.call(DATE_OIDS, field.dataTypeID); 29 | } 30 | 31 | /** 32 | * Return a sql query to transform a date column into a unix epoch 33 | * @param {string} column - The name of the date column 34 | */ 35 | function _castColumnToEpoch (columnName) { 36 | return `date_part('epoch', "${columnName}") as "${columnName}"`; 37 | } 38 | 39 | function getColumnsWithWrappedDates (query) { 40 | if (!query) { 41 | return; 42 | } 43 | if (!query.match(/\b_cdb_epoch_transformation\b/)) { 44 | return; 45 | } 46 | const columns = []; 47 | const fieldMatcher = /\bdate_part\('epoch', "([^"]+)"\) as "([^"]+)"/gmi; 48 | let match; 49 | do { 50 | match = fieldMatcher.exec(query); 51 | if (match && match[1] === match[2]) { 52 | columns.push(match[1]); 53 | } 54 | } while (match); 55 | return columns; 56 | } 57 | 58 | module.exports = { 59 | wrapDates, 60 | getColumnsWithWrappedDates 61 | }; 62 | -------------------------------------------------------------------------------- /lib/utils/icu-data-env-setter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const glob = require('glob'); 4 | const path = require('path'); 5 | 6 | // See https://github.com/CartoDB/support/issues/984 7 | // CartoCSS properties text-wrap-width/text-wrap-character not working 8 | function setICUEnvVariable () { 9 | if (process.env.ICU_DATA === undefined) { 10 | const regexedPath = '/node_modules/mapnik/lib/binding/*/share/mapnik/icu/'; 11 | const directory = glob.sync(path.join(__dirname, '../../..', regexedPath)); 12 | 13 | if (directory && directory.length > 0) { 14 | process.env.ICU_DATA = directory[0]; 15 | } 16 | } 17 | } 18 | 19 | module.exports = setICUEnvVariable; 20 | -------------------------------------------------------------------------------- /lib/utils/json-replacer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function jsonReplacerFactory () { 4 | // Fix: https://github.com/CartoDB/Windshaft-cartodb/issues/705 5 | // See: http://expressjs.com/en/4x/api.html#app.set 6 | return function jsonReplacer (key, value) { 7 | if (value !== value) { // eslint-disable-line no-self-compare 8 | return 'NaN'; 9 | } 10 | 11 | if (value === Infinity) { 12 | return 'Infinity'; 13 | } 14 | 15 | if (value === -Infinity) { 16 | return '-Infinity'; 17 | } 18 | 19 | return value; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/utils/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pino = require('pino'); 4 | const { req: requestSerializer, res: responseSerializer, err, wrapErrorSerializer } = pino.stdSerializers; 5 | const DEV_ENVS = ['test', 'development']; 6 | 7 | module.exports = class Logger { 8 | constructor () { 9 | const { LOG_LEVEL, NODE_ENV } = process.env; 10 | const logLevelFromNodeEnv = NODE_ENV === 'test' ? 'fatal' : 'info'; 11 | const errorSerializer = DEV_ENVS.includes(NODE_ENV) ? err : wrapErrorSerializer(err => { 12 | if (Object.prototype.hasOwnProperty.call(err, 'stack')) { 13 | err.stack = err.stack.split('\n').slice(0, 3).join('\n'); 14 | } 15 | return err; 16 | }); 17 | const options = { 18 | base: null, // Do not bind hostname, pid and friends by default 19 | level: LOG_LEVEL || logLevelFromNodeEnv, 20 | formatters: { 21 | level (label) { 22 | if (label === 'warn') { 23 | return { levelname: 'warning' }; 24 | } 25 | 26 | return { levelname: label }; 27 | } 28 | }, 29 | messageKey: 'event_message', 30 | timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, 31 | serializers: { 32 | client_request: requestSerializer, 33 | server_response: responseSerializer, 34 | exception: (err) => Array.isArray(err) ? err.map((err) => errorSerializer(err)) : [errorSerializer(err)] 35 | } 36 | }; 37 | const dest = pino.destination({ sync: false }); // stdout 38 | 39 | this._logger = pino(options, dest); 40 | } 41 | 42 | trace (...args) { 43 | this._logger.trace(...args); 44 | } 45 | 46 | debug (...args) { 47 | this._logger.debug(...args); 48 | } 49 | 50 | info (...args) { 51 | this._logger.info(...args); 52 | } 53 | 54 | warn (...args) { 55 | this._logger.warn(...args); 56 | } 57 | 58 | error (...args) { 59 | this._logger.error(...args); 60 | } 61 | 62 | fatal (...args) { 63 | this._logger.fatal(...args); 64 | } 65 | 66 | child (...args) { 67 | return this._logger.child(...args); 68 | } 69 | 70 | finish (callback) { 71 | return pino.final(this._logger, callback); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /lib/utils/on-tile-error-strategy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const timeoutErrorTilePath = path.join(__dirname, '/../../assets/render-timeout-fallback.png'); 5 | const timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, { encoding: null }); 6 | 7 | module.exports = function getOnTileErrorStrategy ({ enabled }) { 8 | let onTileErrorStrategy; 9 | 10 | if (enabled !== false) { 11 | onTileErrorStrategy = async function onTileErrorStrategy$TimeoutTile (err, format) { 12 | function isRenderTimeoutError (err) { 13 | return err.message === 'Render timed out'; 14 | } 15 | 16 | function isDatasourceTimeoutError (err) { 17 | return err.message && err.message.match(/canceling statement due to statement timeout/i); 18 | } 19 | 20 | function isTimeoutError (err) { 21 | return isRenderTimeoutError(err) || isDatasourceTimeoutError(err); 22 | } 23 | 24 | function isRasterFormat (format) { 25 | return format === 'png' || format === 'jpg'; 26 | } 27 | 28 | if (isTimeoutError(err) && isRasterFormat(format)) { 29 | return { buffer: timeoutErrorTile, headers: { 'Content-Type': 'image/png' }, stats: {} }; 30 | } else { 31 | throw err; 32 | } 33 | }; 34 | } 35 | 36 | return onTileErrorStrategy; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/utils/substitution-tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SUBSTITUTION_TOKENS = { 4 | bbox: /!bbox!/g, 5 | scale_denominator: /!scale_denominator!/g, 6 | pixel_width: /!pixel_width!/g, 7 | pixel_height: /!pixel_height!/g 8 | }; 9 | 10 | var SubstitutionTokens = { 11 | tokens: function (sql) { 12 | return Object.keys(SUBSTITUTION_TOKENS).filter(function (tokenName) { 13 | return !!sql.match(SUBSTITUTION_TOKENS[tokenName]); 14 | }); 15 | }, 16 | 17 | hasTokens: function (sql) { 18 | return this.tokens(sql).length > 0; 19 | }, 20 | 21 | replace: function (sql, replaceValues) { 22 | Object.keys(replaceValues).forEach(function (token) { 23 | if (SUBSTITUTION_TOKENS[token]) { 24 | sql = sql.replace(SUBSTITUTION_TOKENS[token], replaceValues[token]); 25 | } 26 | }); 27 | return sql; 28 | } 29 | }; 30 | 31 | module.exports = SubstitutionTokens; 32 | -------------------------------------------------------------------------------- /metro/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const metro = require('./metro'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | const { CONFIG_PATH = path.resolve(__dirname, './config.json') } = process.env; 8 | const existsConfigFile = fs.existsSync(CONFIG_PATH); 9 | 10 | if (!existsConfigFile) { 11 | exit(4)(new Error(`Wrong path for CONFIG_PATH env variable: ${CONFIG_PATH} no such file`)); 12 | } 13 | 14 | let config; 15 | 16 | if (existsConfigFile) { 17 | config = fs.readFileSync(CONFIG_PATH); 18 | try { 19 | config = JSON.parse(config); 20 | } catch (e) { 21 | exit(5)(new Error('Wrong config format: invalid JSON')); 22 | } 23 | } 24 | 25 | metro({ metrics: config && config.metrics }) 26 | .then(exit(0)) 27 | .catch(exit(1)); 28 | 29 | process.on('uncaughtException', exit(2)); 30 | process.on('unhandledRejection', exit(3)); 31 | 32 | function exit (code = 1) { 33 | return function (err) { 34 | if (err) { 35 | console.error(err); 36 | } 37 | 38 | process.exit(code); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /metro/metro.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const stream = require('stream'); 5 | const pipeline = util.promisify(stream.pipeline); 6 | const split = require('split2'); 7 | const logCollector = require('./log-collector'); 8 | const MetricsCollector = require('./metrics-collector'); 9 | 10 | module.exports = async function metro ({ input = process.stdin, output = process.stdout, metrics = {} } = {}) { 11 | const metricsCollector = new MetricsCollector(metrics); 12 | const { stream: metricsStream } = metricsCollector; 13 | 14 | try { 15 | await metricsCollector.start(); 16 | await pipeline(input, split(), logCollector(), metricsStream, output); 17 | } finally { 18 | await metricsCollector.stop(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /scripts/darwin-pre-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$OSTYPE" == "darwin"* ]]; then 4 | export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig 5 | CAIRO_PKG_CONFIG=`pkg-config cairo --cflags-only-I 2> /dev/null` 6 | RESULT=$? 7 | 8 | if [[ ${RESULT} -ne 0 ]]; then 9 | echo "###################################################################################" 10 | echo "# PREINSTALL HOOK ERROR #" 11 | echo "#---------------------------------------------------------------------------------#" 12 | echo "# #" 13 | echo "# node-canvas install error: some packages required by 'cairo' are not found #" 14 | echo "# #" 15 | echo -e "# Use '\033[1mmake all\033[0m', it will take care of common/known issues #" 16 | echo "# #" 17 | echo "# As an alternative try: #" 18 | echo "# Try to 'export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig' #" 19 | echo "# #" 20 | echo "# If problems persist visit: https://github.com/Automattic/node-canvas/wiki #" 21 | echo "# #" 22 | echo "###################################################################################" 23 | exit 1 24 | fi 25 | fi 26 | -------------------------------------------------------------------------------- /scripts/lzma2config.js: -------------------------------------------------------------------------------- 1 | if (process.argv.length !== 3) { 2 | console.error('Usage: node %s lzma_string', __filename); 3 | process.exit(1); 4 | } 5 | 6 | var LZMA = require('lzma').LZMA; 7 | var lzmaWorker = new LZMA(); 8 | var lzmaInput = decodeURIComponent(process.argv[2]); 9 | var lzmaBuffer = Buffer.from(lzmaInput, 'base64') 10 | .toString('binary') 11 | .split('') 12 | .map(function(c) { 13 | return c.charCodeAt(0) - 128 14 | }); 15 | 16 | lzmaWorker.decompress(lzmaBuffer, function(result) { 17 | console.log(JSON.stringify(JSON.parse(JSON.parse(result).config), null, 4)); 18 | }); 19 | -------------------------------------------------------------------------------- /scripts/mvt-timeout-error.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import mapbox_vector_tile 4 | 5 | lines_list = [] 6 | 7 | # main diagonal line 8 | lines_list.append({ "geometry":"LINESTRING (0 0, 4096 4096)"}) 9 | 10 | # diagonal lines 11 | for i in range(4096/32, 4096, 4096/32): 12 | start = i 13 | end = 4096 - i 14 | 15 | lines_list.append({ "geometry":"LINESTRING (0 " + str(start) + ", " + str(end) + " 4096)" }) 16 | lines_list.append({ "geometry":"LINESTRING (" + str(start) + " 0, 4096 " + str(end) + ")" }) 17 | 18 | # box lines 19 | lines_list.append({ "geometry":"LINESTRING (0 0, 0 4096)"}) 20 | lines_list.append({ "geometry":"LINESTRING (0 4096, 4096 4096)"}) 21 | lines_list.append({ "geometry":"LINESTRING (4096 4096, 4096 0)"}) 22 | lines_list.append({ "geometry":"LINESTRING (4096 0, 0 0)"}) 23 | 24 | 25 | tile = mapbox_vector_tile.encode([ 26 | { 27 | "name": "errorTileSquareLayer", 28 | "features": [{ "geometry":"POLYGON ((0 0, 0 4096, 4096 4096, 4096 0, 0 0))" }] 29 | }, 30 | { 31 | "name": "errorTileStripesLayer", 32 | "features": lines_list 33 | } 34 | ]) 35 | 36 | with open('./assets/render-timeout-fallback.mvt', 'w+') as f: 37 | f.write(tile) -------------------------------------------------------------------------------- /test/acceptance/analysis/analyses-filters-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../support/test-helper'); 4 | 5 | const assert = require('../../support/assert'); 6 | const TestClient = require('../../support/test-client'); 7 | 8 | describe('analysis-layers-dataviews', () => { 9 | const CARTOCSS = `#layer { 10 | marker-fill-opacity: 1; 11 | marker-line-color: white; 12 | marker-line-width: 0.5; 13 | marker-line-opacity: 1; 14 | marker-placement: point; 15 | marker-type: ellipse; 16 | marker-width: 8; 17 | marker-fill: red; 18 | marker-allow-overlap: true; 19 | }`; 20 | 21 | const mapConfig = { 22 | version: '1.6.0', 23 | layers: [ 24 | { 25 | type: 'cartodb', 26 | options: { 27 | source: { 28 | id: 'a1' 29 | }, 30 | cartocss: CARTOCSS, 31 | cartocss_version: '2.3.0' 32 | } 33 | } 34 | ], 35 | dataviews: { 36 | pop_max_histogram: { 37 | source: { 38 | id: 'a1' 39 | }, 40 | type: 'histogram', 41 | options: { 42 | column: 'pop_max' 43 | } 44 | } 45 | }, 46 | analyses: [ 47 | { 48 | id: 'a1', 49 | type: 'source', 50 | params: { 51 | query: 'select * from populated_places_simple_reduced' 52 | } 53 | } 54 | ] 55 | }; 56 | 57 | it('should get a filtered histogram dataview', function (done) { 58 | const testClient = new TestClient(mapConfig, 1234); 59 | 60 | const params = { 61 | filters: { 62 | analyses: { 63 | a1: [ 64 | { 65 | type: 'range', 66 | column: 'pop_max', 67 | params: { 68 | min: 2e6 69 | } 70 | } 71 | ] 72 | } 73 | } 74 | }; 75 | 76 | testClient.getDataview('pop_max_histogram', params, (err, dataview) => { 77 | assert.ok(!err, err); 78 | 79 | assert.strictEqual(dataview.type, 'histogram'); 80 | assert.strictEqual(dataview.bins_start, 2008000); 81 | 82 | testClient.drain(done); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/acceptance/label-wrap-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | var TestClient = require('../support/test-client'); 5 | 6 | var assert = require('../support/assert'); 7 | var IMAGE_TOLERANCE = 5; 8 | 9 | describe('CartoCSS wrap', function () { 10 | const options = { 11 | sql: ` 12 | SELECT 13 | 5 as cartodb_id, 14 | ST_Transform(ST_SetSRID(ST_MakePoint(-57.65625,-15.6230368),4326),3857) as the_geom_webmercator, 15 | ST_SetSRID(ST_MakePoint(-57.65625,-15.62303683),4326) as the_geom, 16 | 'South America' as continent 17 | `, 18 | cartocss: ` 19 | #continent_points::labels { 20 | text-name: [continent]; 21 | text-face-name: 'DejaVu Sans Book'; 22 | text-size: 10; 23 | text-fill: lighten(#000,40); 24 | text-transform: uppercase; 25 | text-wrap-width: 30; 26 | text-character-spacing: 2; 27 | text-placement: point; 28 | text-placement-type: dummy; 29 | [zoom >= 3]{ 30 | text-character-spacing: 2; 31 | text-size: 11; 32 | } 33 | } 34 | `, 35 | cartocss_version: '3.0.12' 36 | }; 37 | 38 | const type = 'mapnik'; 39 | 40 | const mapConfig = { 41 | version: '1.6.0', 42 | layers: [ 43 | { 44 | type, 45 | id: 'layerLabel', 46 | options 47 | } 48 | ] 49 | }; 50 | 51 | afterEach(function (done) { 52 | if (this.testClient) { 53 | this.testClient.drain(done); 54 | } 55 | }); 56 | 57 | it('Label should be text-wrapped', function (done) { 58 | this.testClient = new TestClient(mapConfig); 59 | this.testClient.getTile(1, 0, 1, { layers: [0] }, (err, res, body) => { 60 | assert.ifError(err); 61 | var textWrapPath = './test/fixtures/text_wrap.png'; 62 | assert.imageIsSimilarToFile(body, textWrapPath, IMAGE_TOLERANCE, done); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/acceptance/layergroup-metadata-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | const assert = require('../support/assert'); 6 | const TestClient = require('../support/test-client'); 7 | const serverOptions = require('../../lib/server-options'); 8 | 9 | describe('layergroup metadata', function () { 10 | const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS; 11 | 12 | before(function () { 13 | serverOptions.renderer.mvt.usePostGIS = true; 14 | }); 15 | 16 | after(function () { 17 | serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS; 18 | }); 19 | 20 | [1234, 'default_public', undefined].forEach(apiKey => { 21 | it(`tiles base urls ${apiKey ? `with api key: ${apiKey}` : 'without api key'}`, function (done) { 22 | const mapConfig = { 23 | version: '1.7.0', 24 | layers: [ 25 | { 26 | type: 'cartodb', 27 | options: { 28 | sql: 'select * from populated_places_simple_reduced' 29 | } 30 | } 31 | ] 32 | }; 33 | 34 | const host = `https://localhost.localhost.lan:${global.environment.port}`; 35 | 36 | const testClient = new TestClient(mapConfig, apiKey); 37 | testClient.getLayergroup((err, body) => { 38 | if (err) { 39 | return done(err); 40 | } 41 | 42 | let urlLayer = `${host}/api/v1/map/${body.layergroupid}/layer0/{z}/{x}/{y}.mvt`; 43 | let urlNoLayer = `${host}/api/v1/map/${body.layergroupid}/{z}/{x}/{y}.mvt`; 44 | 45 | if (apiKey) { 46 | urlLayer += `?api_key=${apiKey}`; 47 | urlNoLayer += `?api_key=${apiKey}`; 48 | } 49 | 50 | assert.ok(body.layergroupid); 51 | assert.strictEqual(body.metadata.layers[0].tilejson.vector.tiles[0], urlLayer); 52 | assert.strictEqual(body.metadata.tilejson.vector.tiles[0], urlNoLayer); 53 | assert.strictEqual(body.metadata.url.vector.urlTemplate, urlNoLayer); 54 | 55 | testClient.drain(done); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/acceptance/layergroupid-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | const assert = require('../support/assert'); 6 | const TestClient = require('../support/test-client'); 7 | const { parse: parseLayergroupToken } = require('../../lib/models/layergroup-token'); 8 | 9 | describe('layergroup id', function () { 10 | const suites = [ 11 | { 12 | description: 'with empty layers should respond with cache buster equal to 0', 13 | expectedCacheBuster: '0', 14 | mapConfig: { 15 | version: '1.8.0', 16 | layers: [] 17 | } 18 | }, 19 | { 20 | description: 'with layer and dumb query (no affected tables) should respond with cache buster equal to 0', 21 | expectedCacheBuster: '0', 22 | mapConfig: { 23 | version: '1.8.0', 24 | layers: [{ 25 | type: 'cartodb', 26 | options: { 27 | sql: TestClient.SQL.ONE_POINT 28 | } 29 | }] 30 | } 31 | }, 32 | { 33 | description: 'with layer and legit query should respond with cache buster', 34 | expectedCacheBuster: '1234567890123', 35 | mapConfig: { 36 | version: '1.8.0', 37 | layers: [{ 38 | type: 'cartodb', 39 | options: { 40 | sql: 'SELECT * FROM test_table' 41 | } 42 | }] 43 | } 44 | } 45 | ]; 46 | 47 | suites.forEach(({ description, expectedCacheBuster, mapConfig }) => { 48 | it(description, function (done) { 49 | const testClient = new TestClient(mapConfig); 50 | 51 | testClient.getLayergroup((err, body) => { 52 | if (err) { 53 | return done(err); 54 | } 55 | 56 | const { layergroupid } = body; 57 | assert.ok(typeof layergroupid === 'string'); 58 | 59 | const { cacheBuster } = parseLayergroupToken(layergroupid); 60 | assert.strictEqual(cacheBuster, expectedCacheBuster); 61 | 62 | testClient.drain(done); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/acceptance/layers-filters-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | var TestClient = require('../support/test-client'); 5 | 6 | describe('layers filters', function () { 7 | const type = 'mapnik'; 8 | const sql = 'select * from populated_places_simple_reduced'; 9 | const cartocss = `#points { 10 | marker-fill-opacity: 1.0; 11 | marker-line-color: #FFF; 12 | marker-line-width: 0.5; 13 | marker-line-opacity: 1.0; 14 | marker-placement: point; 15 | marker-type: ellipse; 16 | marker-width: 8; 17 | marker-fill: red; 18 | marker-allow-overlap: true; 19 | }`; 20 | const cartocssVersion = '3.0.12'; 21 | const options = { 22 | sql, 23 | cartocss, 24 | cartocss_version: cartocssVersion 25 | }; 26 | 27 | const mapConfig = { 28 | version: '1.6.0', 29 | layers: [ 30 | { 31 | type, 32 | id: 'layerA', 33 | options 34 | }, 35 | { 36 | type, 37 | id: 'layerB', 38 | options 39 | } 40 | ] 41 | }; 42 | 43 | afterEach(function (done) { 44 | if (this.testClient) { 45 | this.testClient.drain(done); 46 | } 47 | }); 48 | 49 | ['layerA', 'layerB'].forEach(layer => { 50 | it(`should work for individual layer ids: ${layer}`, function (done) { 51 | this.testClient = new TestClient(mapConfig); 52 | this.testClient.getTile(0, 0, 0, { layers: layer }, done); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png -------------------------------------------------------------------------------- /test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png -------------------------------------------------------------------------------- /test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png -------------------------------------------------------------------------------- /test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png -------------------------------------------------------------------------------- /test/acceptance/ported/regressions-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var testHelper = require('../../support/test-helper'); 4 | 5 | var assert = require('../../support/assert'); 6 | var testClient = require('./support/test-client'); 7 | 8 | describe('regressions', function () { 9 | after(function () { 10 | testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir); 11 | }); 12 | 13 | // Test that you cannot write to the database from a tile request 14 | // 15 | // See http://github.com/CartoDB/Windshaft/issues/130 16 | // [x] Needs a fix on the mapnik side: https://github.com/mapnik/mapnik/pull/2143 17 | // 18 | it('#130 database access is read-only', function (done) { 19 | var writeSqlMapConfig = testClient.singleLayerMapConfig( 20 | 'select st_point(0,0) as the_geom, * from test_table_inserter(st_setsrid(st_point(0,0),4326),\'write\')' 21 | ); 22 | 23 | var expectedResponse = { 24 | status: 400, 25 | headers: { 26 | 'Content-Type': 'application/json; charset=utf-8' 27 | } 28 | }; 29 | 30 | testClient.getTile(writeSqlMapConfig, 0, 0, 0, expectedResponse, function (err, res) { 31 | assert.ifError(err); 32 | var parsedBody = JSON.parse(res.body); 33 | assert.ok(parsedBody.errors); 34 | assert.strictEqual(parsedBody.errors.length, 1); 35 | assert.ok(parsedBody.errors[0].match(/read-only transaction/), 'read-only error message expected'); 36 | done(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/acceptance/ported/support/ported-server-options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | var serverOptions = require('../../../../lib/server-options'); 5 | const mapnik = require('@carto/mapnik'); 6 | var OverviewsQueryRewriter = require('../../../../lib/utils/overviews-query-rewriter'); 7 | var overviewsQueryRewriter = new OverviewsQueryRewriter({ 8 | zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)' 9 | }); 10 | var path = require('path'); 11 | 12 | module.exports = _.extend({}, serverOptions, { 13 | grainstore: { 14 | datasource: { 15 | geometry_field: 'the_geom', 16 | srid: 4326 17 | }, 18 | cachedir: global.environment.millstone.cache_basedir, 19 | mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik, 20 | gc_prob: 0, // run the garbage collector at each invocation 21 | default_layergroup_ttl: global.environment.mapConfigTTL || 7200 22 | }, 23 | renderer: { 24 | mapnik: { 25 | poolSize: 4, // require('os').cpus().length, 26 | poolMaxWaitingClients: 32, 27 | metatile: 1, 28 | bufferSize: 64, 29 | snapToGrid: false, 30 | clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5 31 | scale_factors: [1, 2], 32 | metrics: false, 33 | limits: { 34 | render: 0, 35 | cacheOnTimeout: true 36 | }, 37 | queryRewriter: overviewsQueryRewriter 38 | }, 39 | http: { 40 | timeout: 5000, 41 | whitelist: ['http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png'], 42 | fallbackImage: { 43 | type: 'fs', 44 | src: path.join(__dirname, '/../../../fixtures/http/basemap.png') 45 | } 46 | } 47 | }, 48 | redis: global.environment.redis, 49 | enable_cors: global.environment.enable_cors, 50 | unbuffered_logging: true, // for smoother teardown from tests 51 | log_format: null, // do not log anything 52 | useProfiler: true 53 | }); 54 | -------------------------------------------------------------------------------- /test/acceptance/server-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('../support/assert'); 6 | var querystring = require('querystring'); 7 | var step = require('step'); 8 | 9 | const createServer = require('../../lib/server'); 10 | var serverOptions = require('../../lib/server-options'); 11 | 12 | describe('server', function () { 13 | var server; 14 | 15 | before(function () { 16 | server = createServer(serverOptions); 17 | server.setMaxListeners(0); 18 | }); 19 | 20 | // TODO: I guess this should be a 404 instead... 21 | it('get call to server returns 200', function (done) { 22 | step( 23 | function doGet () { 24 | var next = this; 25 | assert.response(server, { 26 | url: '/', 27 | method: 'GET' 28 | }, {}, function (res, err) { next(err, res); }); 29 | }, 30 | function doCheck (err, res) { 31 | assert.ifError(err); 32 | assert.ok(res.statusCode, 200); 33 | var cc = res.headers['x-cache-channel']; 34 | assert.ok(!cc); 35 | return null; 36 | }, 37 | function finish (err) { 38 | done(err); 39 | } 40 | ); 41 | }); 42 | }); 43 | 44 | describe('server old_api', function () { 45 | var server; 46 | 47 | before(function () { 48 | server = createServer(serverOptions); 49 | server.setMaxListeners(0); 50 | }); 51 | 52 | // See https://github.com/CartoDB/Windshaft-cartodb/issues/115 53 | it.skip("get'ing tile with not-strictly-valid style", function (done) { 54 | var style = querystring.stringify({ style: '#test_table{line-color:black}}', style_version: '2.0.0' }); 55 | assert.response(server, { 56 | headers: { host: 'localhost' }, 57 | url: '/tiles/test_table/0/0/0.png?' + style, // madrid 58 | method: 'GET', 59 | encoding: 'binary' 60 | }, {}, function (res) { 61 | assert.strictEqual(res.statusCode, 200, res.statusCode + ': ' + res.body); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/acceptance/special-numeric-values-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('../support/assert'); 6 | var TestClient = require('../support/test-client'); 7 | 8 | describe('special numeric values', function () { 9 | afterEach(function (done) { 10 | if (this.testClient) { 11 | this.testClient.drain(done); 12 | } else { 13 | done(); 14 | } 15 | }); 16 | 17 | var ATTRIBUTES_LAYER = 1; 18 | 19 | function createMapConfig (sql, id, columns) { 20 | return { 21 | version: '1.6.0', 22 | layers: [ 23 | { 24 | type: 'mapnik', 25 | options: { 26 | sql: "select 1 as id, 'SRID=4326;POINT(0 0)'::geometry as the_geom", 27 | cartocss: '#style { }', 28 | cartocss_version: '2.0.1' 29 | } 30 | }, 31 | { 32 | type: 'mapnik', 33 | options: { 34 | sql: sql || "select 1 as i, 6 as n, 'SRID=4326;POINT(0 0)'::geometry as the_geom", 35 | attributes: { 36 | id: id || 'i', 37 | columns: columns || ['n'] 38 | }, 39 | cartocss: '#style { }', 40 | cartocss_version: '2.0.1' 41 | } 42 | } 43 | ] 44 | }; 45 | } 46 | 47 | it('should retrieve special numeric values', function (done) { 48 | var featureId = 1; 49 | var sql = [ 50 | 'SELECT', 51 | ' 1 as cartodb_id,', 52 | ' null::geometry the_geom_webmercator,', 53 | ' \'infinity\'::float as infinity,', 54 | ' \'-infinity\'::float as _infinity,', 55 | ' \'NaN\'::float as nan' 56 | ].join('\n'); 57 | var id = 'cartodb_id'; 58 | var columns = ['infinity', '_infinity', 'nan']; 59 | 60 | var mapConfig = createMapConfig(sql, id, columns); 61 | 62 | this.testClient = new TestClient(mapConfig, 1234); 63 | this.testClient.getFeatureAttributes(featureId, ATTRIBUTES_LAYER, {}, function (err, attributes) { 64 | assert.ifError(err); 65 | assert.strictEqual(attributes.infinity, 'Infinity'); 66 | assert.strictEqual(attributes._infinity, '-Infinity'); 67 | assert.strictEqual(attributes.nan, 'NaN'); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/acceptance/sql-wrap-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('../support/assert'); 6 | var TestClient = require('../support/test-client'); 7 | 8 | describe('sql-wrap', function () { 9 | afterEach(function (done) { 10 | if (this.testClient) { 11 | this.testClient.drain(done); 12 | } else { 13 | return done(); 14 | } 15 | }); 16 | 17 | it('should use sql_wrap from layer options', function (done) { 18 | var mapConfig = { 19 | version: '1.5.0', 20 | layers: [ 21 | { 22 | type: 'cartodb', 23 | options: { 24 | sql: 'SELECT * FROM populated_places_simple_reduced', 25 | sql_wrap: "SELECT * FROM (<%= sql %>) _w WHERE adm0_a3 = 'USA'", 26 | cartocss: [ 27 | '#points {', 28 | ' marker-fill-opacity: 1;', 29 | ' marker-line-color: #FFF;', 30 | ' marker-line-width: 0.5;', 31 | ' marker-line-opacity: 1;', 32 | ' marker-placement: point;', 33 | ' marker-type: ellipse;', 34 | ' marker-width: 8;', 35 | ' marker-fill: red;', 36 | ' marker-allow-overlap: true;', 37 | '}' 38 | ].join('\n'), 39 | cartocss_version: '2.3.0' 40 | } 41 | } 42 | ] 43 | }; 44 | 45 | this.testClient = new TestClient(mapConfig, 1234); 46 | this.testClient.getTile(0, 0, 0, function (err, tile, img) { 47 | assert.ok(!err, err); 48 | var fixtureImg = './test/fixtures/sql-wrap-usa-filter.png'; 49 | assert.imageIsSimilarToFile(img, fixtureImg, 20, done); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/fixtures/_vovw_1_test_table_1_0_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/_vovw_1_test_table_1_0_0.png -------------------------------------------------------------------------------- /test/fixtures/_vovw_2_test_table_2_1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/_vovw_2_test_table_2_1_1.png -------------------------------------------------------------------------------- /test/fixtures/analysis/adm0cap-source-id-mapnik-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/analysis/adm0cap-source-id-mapnik-layer.png -------------------------------------------------------------------------------- /test/fixtures/analysis/basic-source-id-mapnik-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/analysis/basic-source-id-mapnik-layer.png -------------------------------------------------------------------------------- /test/fixtures/analysis/buffer-over-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/analysis/buffer-over-source.png -------------------------------------------------------------------------------- /test/fixtures/analysis/named-map-buffer-layergroup-static-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/analysis/named-map-buffer-layergroup-static-preview.png -------------------------------------------------------------------------------- /test/fixtures/analysis/named-map-buffer-static-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/analysis/named-map-buffer-static-preview.png -------------------------------------------------------------------------------- /test/fixtures/analysis/named-map-buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/analysis/named-map-buffer.png -------------------------------------------------------------------------------- /test/fixtures/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blank.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-0.1-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-0.1-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-0.2-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-0.2-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-0.3-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-0.3-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-1.2-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-1.2-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-1.2.3.4-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-1.2.3.4-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-1.2.5-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-1.2.5-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-1.3-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-1.3-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-2.1-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-2.1-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-filtering-layers-2.2-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-filtering-layers-2.2-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-plain-torque-2.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-plain-torque-2.1.1.png -------------------------------------------------------------------------------- /test/fixtures/blend/blend-plain-torque-2.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/blend-plain-torque-2.2.1.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-0.3-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-0.3-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-0.4-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-0.4-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-1.2-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-1.2-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-1.3-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-1.3-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-2.3-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-2.3-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-3.4-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-3.4-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/blend/http_fallback/blend-layers-all-zxy-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/blend/http_fallback/blend-layers-all-zxy-1.0.0.png -------------------------------------------------------------------------------- /test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png -------------------------------------------------------------------------------- /test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.geojson: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-53839,4629161]},"properties":{"name":"Alicante","cartodb_id":1200}},{"type":"Feature","geometry":{"type":"Point","coordinates":[242835,5069332]},"properties":{"name":"Barcelona","cartodb_id":5330}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-5567,4861644]},"properties":{"name":"Castello","cartodb_id":1201}},{"type":"Feature","geometry":{"type":"Point","coordinates":[272735,5092314]},"properties":{"name":"Mataro","cartodb_id":615}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-125787,4576600]},"properties":{"name":"Murcia","cartodb_id":952}},{"type":"Feature","geometry":{"type":"Point","coordinates":[295469,4804267]},"properties":{"name":"Palma","cartodb_id":5500}},{"type":"Feature","geometry":{"type":"Point","coordinates":[139148,5030112]},"properties":{"name":"Tarragona","cartodb_id":616}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-44746,4791667]},"properties":{"name":"Valencia","cartodb_id":5942}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-99072,5108695]},"properties":{"name":"Zaragoza","cartodb_id":5932}}]} -------------------------------------------------------------------------------- /test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png -------------------------------------------------------------------------------- /test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-0.geojson: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[295469,4804267]},"properties":{"name":"Palma","cartodb_id":5500}}]} -------------------------------------------------------------------------------- /test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-0.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-0.mvt -------------------------------------------------------------------------------- /test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-128.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-128.mvt -------------------------------------------------------------------------------- /test/fixtures/http/basemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/http/basemap.png -------------------------------------------------------------------------------- /test/fixtures/http/dark_nolabels-1-0-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/http/dark_nolabels-1-0-0.png -------------------------------------------------------------------------------- /test/fixtures/http/light_nolabels-1-0-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/http/light_nolabels-1-0-0.png -------------------------------------------------------------------------------- /test/fixtures/limits/fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/limits/fallback.png -------------------------------------------------------------------------------- /test/fixtures/markers/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/markers/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-bounds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-bounds.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-estimated-proj5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-estimated-proj5.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-estimated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-estimated.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-override-bbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-override-bbox.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-override-zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-override-zoom.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_all.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_blue.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_orange.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_orange_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_orange_blue.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_red.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_red_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_red_blue.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_red_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_red_orange.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_red_orange_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_red_orange_blue.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-preview_layers_undefined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-preview_layers_undefined.png -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-zoom-center.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-zoom-center.jpeg -------------------------------------------------------------------------------- /test/fixtures/previews/populated_places_simple_reduced-zoom-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/previews/populated_places_simple_reduced-zoom-center.png -------------------------------------------------------------------------------- /test/fixtures/provider/populated_places_simple_reduced-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/provider/populated_places_simple_reduced-black.png -------------------------------------------------------------------------------- /test/fixtures/provider/populated_places_simple_reduced-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/provider/populated_places_simple_reduced-blue.png -------------------------------------------------------------------------------- /test/fixtures/provider/populated_places_simple_reduced-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/provider/populated_places_simple_reduced-green.png -------------------------------------------------------------------------------- /test/fixtures/provider/populated_places_simple_reduced-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/provider/populated_places_simple_reduced-red.png -------------------------------------------------------------------------------- /test/fixtures/raster_gray_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/raster_gray_rect.png -------------------------------------------------------------------------------- /test/fixtures/render-timeout-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/render-timeout-fallback.png -------------------------------------------------------------------------------- /test/fixtures/sql-wrap-usa-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/sql-wrap-usa-filter.png -------------------------------------------------------------------------------- /test/fixtures/test_bigpoint_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_bigpoint_red.png -------------------------------------------------------------------------------- /test/fixtures/test_default_mapnik_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_default_mapnik_point.png -------------------------------------------------------------------------------- /test/fixtures/test_mapconfigFactory.js: -------------------------------------------------------------------------------- 1 | function getVectorMapConfig (opts) { 2 | return { 3 | buffersize: { 4 | mvt: 1 5 | }, 6 | layers: _generateLayers(opts) 7 | }; 8 | } 9 | 10 | function _generateLayers (opts) { 11 | const numberOfLayers = opts.numberOfLayers || 1; 12 | const layers = []; 13 | for (let index = 0; index < numberOfLayers; index++) { 14 | const layerOptions = (opts.layerOptions || {})[index] || {}; 15 | layers.push(_generateLayerConfig(layerOptions)); 16 | } 17 | return layers; 18 | } 19 | 20 | function _generateLayerConfig (opts) { 21 | const additionalColumns = opts.additionalColumns ? opts.additionalColumns.join(',') + ',' : ''; 22 | return { 23 | type: 'mapnik', 24 | options: { 25 | sql: ` 26 | SELECT 27 | ${additionalColumns} 28 | (DATE '2018-06-01' + x) as date, 29 | x as cartodb_id, 30 | st_makepoint(x * 10, x * 10) as the_geom, 31 | st_makepoint(x * 10, x * 10) as the_geom_webmercator 32 | FROM 33 | generate_series(0, 1) x`, 34 | aggregation: { 35 | columns: {}, 36 | dimensions: { 37 | date: 'date' 38 | }, 39 | placement: 'centroid', 40 | resolution: 1, 41 | threshold: 1 42 | }, 43 | dates_as_numbers: opts.dates_as_numbers, 44 | metadata: { 45 | geometryType: true, 46 | columnStats: { 47 | topCategories: 32768, 48 | includeNulls: true 49 | }, 50 | sample: { 51 | num_rows: 1000, 52 | include_columns: [ 53 | 'date' 54 | ] 55 | } 56 | } 57 | } 58 | }; 59 | } 60 | 61 | module.exports = { getVectorMapConfig }; 62 | -------------------------------------------------------------------------------- /test/fixtures/test_multilayer_bbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_multilayer_bbox.png -------------------------------------------------------------------------------- /test/fixtures/test_table_0_0_0_multilayer1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_0_0_0_multilayer1.png -------------------------------------------------------------------------------- /test/fixtures/test_table_0_0_0_multilayer2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_0_0_0_multilayer2.png -------------------------------------------------------------------------------- /test/fixtures/test_table_0_0_0_multilayer3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_0_0_0_multilayer3.png -------------------------------------------------------------------------------- /test/fixtures/test_table_0_0_0_multilayer4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_0_0_0_multilayer4.png -------------------------------------------------------------------------------- /test/fixtures/test_table_13_4011_3088.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_13_4011_3088.png -------------------------------------------------------------------------------- /test/fixtures/test_table_13_4011_3088_limit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_13_4011_3088_limit_2.png -------------------------------------------------------------------------------- /test/fixtures/test_table_13_4011_3088_styled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_13_4011_3088_styled.png -------------------------------------------------------------------------------- /test/fixtures/test_table_13_4011_3088_styled_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_13_4011_3088_styled_black.png -------------------------------------------------------------------------------- /test/fixtures/test_table_13_4011_3088_svg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_13_4011_3088_svg1.png -------------------------------------------------------------------------------- /test/fixtures/test_table_13_4011_3088_svg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_13_4011_3088_svg2.png -------------------------------------------------------------------------------- /test/fixtures/test_table_15_16046_12354_styled_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_15_16046_12354_styled_black.png -------------------------------------------------------------------------------- /test/fixtures/test_table_15_16046_12354_styled_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_15_16046_12354_styled_blue.png -------------------------------------------------------------------------------- /test/fixtures/test_table_1_0_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_1_0_0.png -------------------------------------------------------------------------------- /test/fixtures/test_table_2_1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_2_1_1.png -------------------------------------------------------------------------------- /test/fixtures/test_table_3_3_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_table_3_3_3.png -------------------------------------------------------------------------------- /test/fixtures/test_turbo_carto_greens_13_4011_3088.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_turbo_carto_greens_13_4011_3088.png -------------------------------------------------------------------------------- /test/fixtures/test_turbo_carto_reds_13_4011_3088.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/test_turbo_carto_reds_13_4011_3088.png -------------------------------------------------------------------------------- /test/fixtures/text_wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/text_wrap.png -------------------------------------------------------------------------------- /test/fixtures/text_wrap_bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/text_wrap_bad.png -------------------------------------------------------------------------------- /test/fixtures/torque/populated_places_simple_reduced-2.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/torque/populated_places_simple_reduced-2.1.1.png -------------------------------------------------------------------------------- /test/fixtures/torque/populated_places_simple_reduced-2.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/torque/populated_places_simple_reduced-2.2.1.png -------------------------------------------------------------------------------- /test/fixtures/torque/populated_places_simple_reduced-turbo-carto-2.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/torque/populated_places_simple_reduced-turbo-carto-2.2.1.png -------------------------------------------------------------------------------- /test/fixtures/turbo-carto-named-maps-blues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/turbo-carto-named-maps-blues.png -------------------------------------------------------------------------------- /test/fixtures/turbo-carto-named-maps-reds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/fixtures/turbo-carto-named-maps-reds.png -------------------------------------------------------------------------------- /test/integration/overviews-metadata-api-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | 7 | var RedisPool = require('redis-mpool'); 8 | var cartodbRedis = require('cartodb-redis'); 9 | 10 | var PgConnection = require('../../lib/backends/pg-connection'); 11 | var PgQueryRunner = require('../../lib/backends/pg-query-runner'); 12 | var OverviewsMetadataBackend = require('../../lib/backends/overviews-metadata'); 13 | 14 | describe('OverviewsMetadataBackend', function () { 15 | var overviewsMetadataBackend; 16 | 17 | before(function () { 18 | var redisPool = new RedisPool(global.environment.redis); 19 | var metadataBackend = cartodbRedis({ pool: redisPool }); 20 | var pgConnection = new PgConnection(metadataBackend); 21 | var pgQueryRunner = new PgQueryRunner(pgConnection); 22 | overviewsMetadataBackend = new OverviewsMetadataBackend(pgQueryRunner); 23 | }); 24 | 25 | it('should return an empty relation for tables that have no overviews', function (done) { 26 | var query = 'select * from test_table'; 27 | overviewsMetadataBackend.getOverviewsMetadata('localhost', query, function (err, result) { 28 | assert.ok(!err, err); 29 | 30 | assert.deepStrictEqual(result, {}); 31 | 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should return overviews metadata', function (done) { 37 | var query = 'select * from test_table_overviews'; 38 | overviewsMetadataBackend.getOverviewsMetadata('localhost', query, function (err, result) { 39 | assert.ok(!err, err); 40 | 41 | assert.deepStrictEqual(result, { 42 | test_table_overviews: { 43 | schema: 'public', 44 | 1: { table: '_vovw_1_test_table_overviews' }, 45 | 2: { table: '_vovw_2_test_table_overviews' } 46 | } 47 | }); 48 | 49 | done(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/integration/pg-query-runner-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | 7 | var RedisPool = require('redis-mpool'); 8 | var cartodbRedis = require('cartodb-redis'); 9 | 10 | var PgConnection = require('../../lib/backends/pg-connection'); 11 | var PgQueryRunner = require('../../lib/backends/pg-query-runner'); 12 | 13 | describe('PgQueryRunner', function () { 14 | var queryRunner; 15 | 16 | before(function () { 17 | var redisPool = new RedisPool(global.environment.redis); 18 | var metadataBackend = cartodbRedis({ pool: redisPool }); 19 | var pgConnection = new PgConnection(metadataBackend); 20 | queryRunner = new PgQueryRunner(pgConnection); 21 | }); 22 | 23 | it('should work for happy case', function (done) { 24 | var query = 'select cartodb_id from test_table limit 3'; 25 | queryRunner.run('localhost', query, function (err, result) { 26 | assert.ok(!err, err); 27 | 28 | assert.ok(Array.isArray(result)); 29 | assert.strictEqual(result.length, 3); 30 | 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should receive rows array even on error', function (done) { 36 | var query = 'select __error___ from test_table'; 37 | queryRunner.run('localhost', query, function (err, result) { 38 | assert.ok(err); 39 | 40 | assert.ok(Array.isArray(result)); 41 | assert.strictEqual(result.length, 0); 42 | 43 | done(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/integration/profiler-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | var StatsClient = require('../../lib/stats/client'); 7 | var ProfilerProxy = require('../../lib/stats/profiler-proxy'); 8 | 9 | describe('profiler + statsd', function () { 10 | var statsInstance; 11 | 12 | before(function () { 13 | statsInstance = StatsClient.instance; 14 | StatsClient.instance = null; 15 | }); 16 | 17 | after(function () { 18 | StatsClient.instance = statsInstance; 19 | }); 20 | 21 | var statsdConfig = { 22 | host: 'whoami.vizzuality.com', 23 | port: 8125, 24 | prefix: 'test.', 25 | cacheDns: false 26 | // support all allowed node-statsd options 27 | }; 28 | 29 | // See https://github.com/CartoDB/Windshaft/issues/167 30 | it('profiler does not throw uncaught exception on invalid host/port', function (done) { 31 | var statsClient = StatsClient.getInstance(statsdConfig); 32 | var profiler = new ProfilerProxy({ profile: true, statsd_client: statsClient }); 33 | 34 | profiler.start('test'); 35 | profiler.done('wadus'); 36 | profiler.end(); 37 | 38 | profiler.sendStats(); 39 | 40 | // force a call to validate sendStats does not throw and uncaught exception 41 | statsClient.timing('forced', 50, 1, function (err) { 42 | assert.ok(err); 43 | assert.strictEqual(err.code, 'ENOTFOUND'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/integration/query-tables-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | 7 | var RedisPool = require('redis-mpool'); 8 | var cartodbRedis = require('cartodb-redis'); 9 | 10 | var PgConnection = require('../../lib/backends/pg-connection'); 11 | 12 | var QueryTables = require('cartodb-query-tables').queryTables; 13 | 14 | describe('QueryTables', function () { 15 | var connection; 16 | 17 | before(function (done) { 18 | var redisPool = new RedisPool(global.environment.redis); 19 | var metadataBackend = cartodbRedis({ pool: redisPool }); 20 | var pgConnection = new PgConnection(metadataBackend); 21 | pgConnection.getConnection('localhost', function (err, pgConnection) { 22 | if (err) { 23 | return done(err); 24 | } 25 | connection = pgConnection; 26 | 27 | return done(); 28 | }); 29 | }); 30 | 31 | // Check test/support/sql/windshaft.test.sql to understand where the values come from. 32 | 33 | it('should return an object with affected tables array and last updated time', function () { 34 | var query = 'select * from test_table'; 35 | return QueryTables.getQueryMetadataModel(connection, query) 36 | .then(result => { 37 | assert.strictEqual(result.getLastUpdatedAt(), 1234567890123); 38 | 39 | assert.strictEqual(result.tables.length, 1); 40 | assert.strictEqual(result.tables[0].dbname, 'test_windshaft_cartodb_user_1_db'); 41 | assert.strictEqual(result.tables[0].schema_name, 'public'); 42 | assert.strictEqual(result.tables[0].table_name, 'test_table'); 43 | }); 44 | }); 45 | 46 | it('should work with private tables', function () { 47 | var query = 'select * from test_table_private_1'; 48 | return QueryTables.getQueryMetadataModel(connection, query) 49 | .then(result => { 50 | assert.strictEqual(result.getLastUpdatedAt(), 1234567890123); 51 | 52 | assert.strictEqual(result.tables.length, 1); 53 | assert.strictEqual(result.tables[0].dbname, 'test_windshaft_cartodb_user_1_db'); 54 | assert.strictEqual(result.tables[0].schema_name, 'public'); 55 | assert.strictEqual(result.tables[0].table_name, 'test_table_private_1'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/monkey/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/monkey/images/layers.png -------------------------------------------------------------------------------- /test/monkey/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/monkey/images/marker-shadow.png -------------------------------------------------------------------------------- /test/monkey/images/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/monkey/images/marker.png -------------------------------------------------------------------------------- /test/monkey/images/popup-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/monkey/images/popup-close.png -------------------------------------------------------------------------------- /test/monkey/images/zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/monkey/images/zoom-in.png -------------------------------------------------------------------------------- /test/monkey/images/zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/monkey/images/zoom-out.png -------------------------------------------------------------------------------- /test/monkey/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Windshaft test 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 22 | 23 | -------------------------------------------------------------------------------- /test/monkey/leaflet.ie.css: -------------------------------------------------------------------------------- 1 | .leaflet-tile { 2 | filter: inherit; 3 | } 4 | 5 | .leaflet-vml-shape { 6 | width: 1px; 7 | height: 1px; 8 | } 9 | .lvml { 10 | behavior: url(#default#VML); 11 | display: inline-block; 12 | position: absolute; 13 | } 14 | 15 | .leaflet-control { 16 | display: inline; 17 | } 18 | 19 | .leaflet-popup-tip { 20 | width: 21px; 21 | _width: 27px; 22 | margin: 0 auto; 23 | _margin-top: -3px; 24 | 25 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 26 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 27 | } 28 | .leaflet-popup-tip-container { 29 | margin-top: -1px; 30 | } 31 | .leaflet-popup-content-wrapper, .leaflet-popup-tip { 32 | border: 1px solid #bbb; 33 | } 34 | 35 | .leaflet-control-zoom { 36 | filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#3F000000',EndColorStr='#3F000000'); 37 | } 38 | .leaflet-control-zoom a { 39 | background-color: #eee; 40 | } 41 | .leaflet-control-zoom a:hover { 42 | background-color: #fff; 43 | } 44 | .leaflet-control-layers-toggle { 45 | } 46 | .leaflet-control-attribution, .leaflet-control-layers { 47 | background: white; 48 | } -------------------------------------------------------------------------------- /test/results/jpeg/.gitignore: -------------------------------------------------------------------------------- 1 | *.jpeg 2 | *.jpg 3 | *.js 4 | -------------------------------------------------------------------------------- /test/results/png/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.js 3 | -------------------------------------------------------------------------------- /test/support/libredis_cell.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/support/libredis_cell.dylib -------------------------------------------------------------------------------- /test/support/libredis_cell.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/Windshaft-cartodb/302c84477dc189ff4df8ad2e1f617acb1fdf622c/test/support/libredis_cell.so -------------------------------------------------------------------------------- /test/support/map-store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { MapConfig } = require('windshaft').model; 4 | 5 | // Windshaft no longer provides the MapStore class to be used just for testing purposes 6 | // This class provides just the method needed to load a map-config from redis 7 | // It should be replaced by a new module @carto/map-config-storage (to be published) 8 | module.exports = class MapStore { 9 | constructor (pool) { 10 | this.pool = pool; 11 | } 12 | 13 | load (token, callback) { 14 | const db = 0; 15 | this.pool.acquire(db, (err, client) => { 16 | if (err) { 17 | return callback(err); 18 | } 19 | 20 | client.get(`map_cfg|${token}`, (err, data) => { 21 | this.pool.release(db, client); 22 | 23 | if (err) { 24 | return callback(err); 25 | } 26 | 27 | let mapConfig; 28 | try { 29 | mapConfig = MapConfig.create(JSON.parse(data)); 30 | } catch (err) { 31 | return callback(err); 32 | } 33 | 34 | return callback(null, mapConfig); 35 | }); 36 | }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /test/support/middlewares/teapot-conditional-response.js: -------------------------------------------------------------------------------- 1 | exports.middlewares = function () { 2 | return function teapotMiddleware (req, res, next) { 3 | if (req.path === '/') { 4 | return res.status(418).send('I\'m a teapot'); 5 | } 6 | next(); 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/support/middlewares/teapot-headers.js: -------------------------------------------------------------------------------- 1 | exports.middlewares = [ 2 | function () { 3 | return function teapotHeaderMiddleware (req, res, next) { 4 | res.header('X-What-Am-I', 'I\'m a teapot'); 5 | return next(); 6 | }; 7 | }, 8 | function () { 9 | return function teapotAnotherHeaderMiddleware (req, res, next) { 10 | res.header('X-Again-What-Am-I', 'I\'m a teapot'); 11 | return next(); 12 | }; 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /test/support/middlewares/teapot-response.js: -------------------------------------------------------------------------------- 1 | exports.middlewares = function () { 2 | return function teapotMiddleware (req, res) { 3 | res.status(418).send('I\'m a teapot'); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /test/support/sql/.gitignore: -------------------------------------------------------------------------------- 1 | CDB_*.sql 2 | -------------------------------------------------------------------------------- /test/support/sql/analysis_catalog.sql: -------------------------------------------------------------------------------- 1 | -- Table to register analysis nodes from https://github.com/cartodb/camshaft 2 | CREATE TABLE IF NOT EXISTS 3 | cartodb.cdb_analysis_catalog ( 4 | -- useful for multi account deployments 5 | username text, 6 | -- md5 hex hash 7 | node_id char(40) CONSTRAINT cdb_analysis_catalog_pkey PRIMARY KEY, 8 | -- being json allows to do queries like analysis_def->>'type' = 'buffer' 9 | analysis_def json NOT NULL, 10 | -- can reference other nodes in this very same table, allowing recursive queries 11 | input_nodes char(40) ARRAY NOT NULL DEFAULT '{}', 12 | status TEXT NOT NULL DEFAULT 'pending', 13 | CONSTRAINT valid_status CHECK ( 14 | status IN ( 'pending', 'waiting', 'running', 'canceled', 'failed', 'ready' ) 15 | ), 16 | created_at timestamp with time zone NOT NULL DEFAULT now(), 17 | -- should be updated when some operation was performed in the node 18 | -- and anything associated to it might have changed 19 | updated_at timestamp with time zone DEFAULT NULL, 20 | -- should register last time the node was used 21 | used_at timestamp with time zone NOT NULL DEFAULT now(), 22 | -- should register the number of times the node was used 23 | hits NUMERIC DEFAULT 0, 24 | -- should register what was the last node using current node 25 | last_used_from char(40), 26 | -- last job modifying the node 27 | last_modified_by uuid, 28 | -- store error message for failures 29 | last_error_message text, 30 | -- cached tables involved in the analysis 31 | cache_tables regclass[] NOT NULL DEFAULT '{}' 32 | ); 33 | -------------------------------------------------------------------------------- /test/support/sql/cdb_analysis_check.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION CDB_CheckAnalysisQuota(table_name TEXT) 2 | RETURNS void AS 3 | $$ 4 | BEGIN 5 | END; 6 | $$ LANGUAGE PLPGSQL; 7 | -------------------------------------------------------------------------------- /test/support/sql/cdb_invalidate_varnish.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION CDB_Invalidate_Varnish(table_name TEXT) 2 | RETURNS void AS 3 | $$ 4 | BEGIN 5 | END; 6 | $$ LANGUAGE PLPGSQL; -------------------------------------------------------------------------------- /test/support/sql/users.sql: -------------------------------------------------------------------------------- 1 | -- public user role 2 | DROP USER IF EXISTS :PUBLICUSER; 3 | CREATE USER :PUBLICUSER WITH PASSWORD ':PUBLICPASS'; 4 | ALTER ROLE :PUBLICUSER SET search_path = :SEARCHPATH, cartodb; 5 | 6 | -- db owner role 7 | DROP USER IF EXISTS :TESTUSER; 8 | CREATE USER :TESTUSER WITH PASSWORD ':TESTPASS'; 9 | ALTER ROLE :TESTUSER SET search_path = :SEARCHPATH, cartodb; 10 | 11 | -- regular user role 1 12 | DROP USER IF EXISTS test_windshaft_regular1; 13 | CREATE USER test_windshaft_regular1 WITH PASSWORD 'regular1'; 14 | ALTER ROLE :TESTUSER SET search_path = :SEARCHPATH, cartodb; 15 | GRANT test_windshaft_regular1 to :TESTUSER; 16 | -------------------------------------------------------------------------------- /test/unit/backends/layer-stats/torque-layer-stats-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var TorqueLayerStats = require('../../../../lib/backends/layer-stats/torque-layer-stats'); 5 | var MapConfig = require('windshaft').model.MapConfig; 6 | 7 | describe('torque-layer-stats', function () { 8 | beforeEach(function () { 9 | this.params = {}; 10 | }); 11 | 12 | var testMapConfigOneLayer = { 13 | version: '1.5.0', 14 | layers: [ 15 | { 16 | type: 'torque', 17 | options: { 18 | sql: 'select * from test_table limit 2', 19 | cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }', 20 | cartocss_version: '2.3.0' 21 | } 22 | } 23 | ] 24 | }; 25 | 26 | it('should return torque stats for one layer', function (done) { 27 | var mapConfig = MapConfig.create(testMapConfigOneLayer); 28 | var layerId = 0; 29 | var layer = mapConfig.getLayer(layerId); 30 | var testSubject = new TorqueLayerStats(); 31 | testSubject.getStats(layer, {}, function (err, result) { 32 | assert.ifError(err); 33 | assert.deepStrictEqual({}, result); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/backends/turbo-carto-postgres-datasource-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PostgresDatasource = require('../../../lib/backends/turbo-carto-postgres-datasource'); 4 | var PSQL = require('cartodb-psql'); 5 | var _ = require('underscore'); 6 | var assert = require('assert'); 7 | 8 | describe('turbo-carto-postgres-datasource', function () { 9 | beforeEach(function () { 10 | const dbname = _.template(global.environment.postgres_auth_user, { user_id: 1 }) + '_db'; 11 | const psql = new PSQL({ 12 | user: 'postgres', 13 | dbname: dbname, 14 | host: global.environment.postgres.host, 15 | port: global.environment.postgres.port 16 | }); 17 | const sql = [ 18 | 'SELECT', 19 | ' null::geometry the_geom_webmercator,', 20 | ' CASE', 21 | ' WHEN x % 4 = 0 THEN \'infinity\'::float', 22 | ' WHEN x % 4 = 1 THEN \'-infinity\'::float', 23 | ' WHEN x % 4 = 2 THEN \'NaN\'::float', 24 | ' ELSE x', 25 | ' END AS values', 26 | 'FROM generate_series(1, 1000) x' 27 | ].join('\n'); 28 | this.datasource = new PostgresDatasource(psql, sql); 29 | }); 30 | 31 | it('should ignore NaNs and Infinities when computing ramps', function (done) { 32 | var column = 'values'; 33 | var buckets = 4; 34 | var method = 'equal'; 35 | this.datasource.getRamp(column, buckets, method, function (err, result) { 36 | assert.ifError(err); 37 | var expectedResult = { 38 | ramp: [252, 501, 750, 999], 39 | stats: { min_val: 3, max_val: 999, avg_val: 501 }, 40 | strategy: undefined 41 | }; 42 | assert.deepStrictEqual(result, expectedResult); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/unit/cache/model/named-maps-entry-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | var _ = require('underscore'); 7 | var NamedMapsCacheEntry = require('../../../../lib/cache/model/named-maps-entry'); 8 | 9 | describe('cache named maps entry', function () { 10 | var namedMapOwner = 'foo'; 11 | var namedMapName = 'wadus_name'; 12 | var namedMapsCacheEntry = new NamedMapsCacheEntry(namedMapOwner, namedMapName); 13 | var entryKey = namedMapsCacheEntry.key(); 14 | 15 | it('key is a string', function () { 16 | assert.ok(_.isString(entryKey)); 17 | }); 18 | 19 | it('key is 8 chars length', function () { 20 | assert.strictEqual(entryKey.length, 8); 21 | var entryKeyParts = entryKey.split(':'); 22 | assert.strictEqual(entryKeyParts.length, 2); 23 | assert.strictEqual(entryKeyParts[0], 'n'); 24 | }); 25 | 26 | it('key is name spaced for named maps', function () { 27 | var entryKeyParts = entryKey.split(':'); 28 | assert.strictEqual(entryKeyParts.length, 2); 29 | assert.strictEqual(entryKeyParts[0], 'n'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/unit/error-messages-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | 7 | var errorMiddleware = require('../../lib/api/middlewares/error-middleware'); 8 | 9 | describe('error messages clean up', function () { 10 | // See https://github.com/CartoDB/Windshaft/issues/173 11 | it('#173 does not send db details in connection error response', function () { 12 | var inMessage = [ 13 | 'Postgis Plugin: Bad connection', 14 | "Connection string: 'host=127.0.0.1 port=5432 dbname=test_windshaft_cartodb_user_1_db " + 15 | "user=test_windshaft_cartodb_user_1 connect_timeout=4'", 16 | " encountered during parsing of layer 'layer0' in Layer" 17 | ].join('\n'); 18 | 19 | var outMessage = errorMiddleware.errorMessage(inMessage); 20 | 21 | assert.ok(outMessage.match('connect'), outMessage); 22 | assert.ok(!outMessage.match(/666/), outMessage); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/error-middleware-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | var errorMiddleware = require('../../lib/api/middlewares/error-middleware'); 7 | 8 | describe('error-middleware', function () { 9 | it('different formats for postgis plugin error returns 400 as status code', function () { 10 | var expectedStatusCode = 400; 11 | assert.strictEqual( 12 | errorMiddleware.findStatusCode('Postgis Plugin: ERROR: column "missing" does not exist\n'), 13 | expectedStatusCode, 14 | 'Error status code for single line does not match' 15 | ); 16 | 17 | assert.strictEqual( 18 | errorMiddleware.findStatusCode('Postgis Plugin: PSQL error:\nERROR: column "missing" does not exist\n'), 19 | expectedStatusCode, 20 | 'Error status code for multiline/PSQL does not match' 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/lzma-middleware-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var testHelper = require('../support/test-helper'); 5 | 6 | var lzmaMiddleware = require('../../lib/api/middlewares/lzma'); 7 | 8 | describe('lzma-middleware', function () { 9 | it('it should extend params with decoded lzma', function (done) { 10 | var qo = { 11 | config: { 12 | version: '1.3.0' 13 | } 14 | }; 15 | testHelper.lzma_compress_to_base64(JSON.stringify(qo), 1, function (err, data) { 16 | if (err) { 17 | return done(err); 18 | } 19 | 20 | const lzma = lzmaMiddleware(); 21 | var req = { 22 | headers: { 23 | host: 'localhost' 24 | }, 25 | query: { 26 | api_key: 'test', 27 | lzma: data 28 | }, 29 | profiler: { 30 | done: function () {} 31 | } 32 | }; 33 | 34 | lzma(req, {}, function (err) { 35 | if (err) { 36 | return done(err); 37 | } 38 | var query = req.query; 39 | assert.deepStrictEqual(qo.config, query.config); 40 | assert.strictEqual('test', query.api_key); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/unit/ported/profiler-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | var ProfilerProxy = require('../../../lib/stats/profiler-proxy'); 7 | 8 | describe('profiler', function () { 9 | it('Profiler is null in ProfilerProxy when profiling is not enabled', function () { 10 | var profilerProxy = new ProfilerProxy({ profile: false }); 11 | assert.strictEqual(profilerProxy.profiler, null); 12 | }); 13 | 14 | it('Profiler is NOT null in ProfilerProxy when profiling is enabled', function () { 15 | var profilerProxy = new ProfilerProxy({ profile: true }); 16 | assert.notStrictEqual(profilerProxy.profiler, null); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/unit/ported/stats-client-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | 7 | var StatsClient = require('../../../lib/stats/client'); 8 | 9 | describe('stats client', function () { 10 | var statsInstance; 11 | 12 | before(function () { 13 | statsInstance = StatsClient.instance; 14 | StatsClient.instance = null; 15 | }); 16 | 17 | after(function () { 18 | StatsClient.instance = statsInstance; 19 | }); 20 | 21 | it('reports errors when they repeat', function (done) { 22 | var WADUS_ERROR = 'wadus_error'; 23 | var statsClient = StatsClient.getInstance({ host: '127.0.0.1', port: 8033 }); 24 | 25 | statsClient.socket.emit('error', 'other_error'); 26 | assert.ok(statsClient.last_error); 27 | assert.strictEqual(statsClient.last_error.msg, 'other_error'); 28 | assert.ok(!statsClient.last_error.interval); 29 | 30 | statsClient.socket.emit('error', WADUS_ERROR); 31 | assert.ok(statsClient.last_error); 32 | assert.strictEqual(statsClient.last_error.msg, WADUS_ERROR); 33 | assert.ok(!statsClient.last_error.interval); 34 | 35 | statsClient.socket.emit('error', WADUS_ERROR); 36 | assert.ok(statsClient.last_error); 37 | assert.strictEqual(statsClient.last_error.msg, WADUS_ERROR); 38 | assert.ok(statsClient.last_error.interval); 39 | 40 | done(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/unit/ported/windshaft-server-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../support/test-helper'); 4 | 5 | var assert = require('assert'); 6 | var cartodbServer = require('../../../lib/server'); 7 | var serverOptions = require('../../../lib/server-options'); 8 | 9 | describe('windshaft', function () { 10 | it('should have valid global environment', function () { 11 | assert.strictEqual(global.environment.environment, 'test'); 12 | }); 13 | 14 | it('can instantiate a Windshaft object (configured express instance)', function () { 15 | var ws = cartodbServer(serverOptions); 16 | assert.ok(ws); 17 | }); 18 | 19 | it('throws exception if incorrect options passed in', function () { 20 | assert.throws( 21 | function () { 22 | var ws = cartodbServer({ unbuffered_logging: true }); 23 | ws.listen(); 24 | }, /Must initialise server with/ 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/stats/reporter/named-map-provider-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const NamedMapProviderReporter = require('../../../../lib/stats/reporter/named-map-provider-cache'); 5 | 6 | describe('named-map-provider-reporter', function () { 7 | it('should report metrics every 100 ms', function (done) { 8 | const oldStatsClient = global.statsClient; 9 | 10 | global.statsClient = { 11 | gauge: function (metric, value) { 12 | this[metric] = value; 13 | } 14 | }; 15 | 16 | const dummyCacheEntries = [ 17 | { 18 | k: 'foo:template_1', 19 | v: { instantiation_1: 1 } 20 | }, 21 | { 22 | k: 'bar:template_2', 23 | v: { instantiation_1: 1, instantiation_2: 2 } 24 | }, 25 | { 26 | k: 'buz:template_3', 27 | v: { instantiation_1: 1, instantiation_2: 2, instantiation_3: 3 } 28 | } 29 | ]; 30 | 31 | const reporter = new NamedMapProviderReporter({ 32 | namedMapProviderCache: { 33 | providerCache: { 34 | dump: () => dummyCacheEntries, 35 | length: dummyCacheEntries.length 36 | } 37 | }, 38 | intervalInMilliseconds: 100 39 | }); 40 | 41 | reporter.start(); 42 | 43 | setTimeout(() => { 44 | reporter.stop(); 45 | 46 | assert.strictEqual( 47 | global.statsClient['windshaft.named-map-provider-cache.named-map.count'], 48 | 3 49 | ); 50 | 51 | assert.strictEqual( 52 | global.statsClient['windshaft.named-map-provider-cache.named-map.instantiation.count'], 53 | 6 54 | ); 55 | 56 | global.statsClient = oldStatsClient; 57 | 58 | done(); 59 | }, 110); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/substitution-tokens-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var SubstitutionTokens = require('../../lib/utils/substitution-tokens'); 5 | 6 | describe('SubstitutionTokens', function () { 7 | var sql = [ 8 | 'WITH hgrid AS (', 9 | ' SELECT CDB_HexagonGrid(', 10 | ' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),', 11 | ' greatest(!pixel_width!,!pixel_height!) * 100', 12 | ' ) as cell', 13 | ')', 14 | 'SELECT', 15 | ' hgrid.cell as the_geom_webmercator,', 16 | ' count(1) as points_count,', 17 | ' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,', 18 | ' 1 as cartodb_id', 19 | 'FROM hgrid, (select * from table) i', 20 | 'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)', 21 | 'GROUP BY hgrid.cell' 22 | ].join('\n'); 23 | 24 | it('should return tokens present in sql', function () { 25 | assert.deepStrictEqual(SubstitutionTokens.tokens(sql), ['bbox', 'scale_denominator', 'pixel_width', 'pixel_height']); 26 | }); 27 | 28 | it('should return just one token', function () { 29 | assert.deepStrictEqual(SubstitutionTokens.tokens('select !bbox! from wadus'), ['bbox']); 30 | }); 31 | 32 | it('should not return other tokens', function () { 33 | assert.deepStrictEqual(SubstitutionTokens.tokens('select !wadus! from wadus'), []); 34 | }); 35 | 36 | it('should report sql has tokens', function () { 37 | assert.strictEqual(SubstitutionTokens.hasTokens(sql), true); 38 | assert.strictEqual(SubstitutionTokens.hasTokens('select !bbox! from wadus'), true); 39 | assert.strictEqual(SubstitutionTokens.hasTokens('select !wadus! from wadus'), false); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/utils/date-wrapper-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var dateWrapper = require('../../../lib/utils/date-wrapper'); 5 | 6 | describe('date-wrapper', function () { 7 | it('should wrap property fields with spaces', function () { 8 | const actual = dateWrapper.wrapDates( 9 | 'select * from table', 10 | [{ name: 'a' }, { name: 'b c' }] 11 | ); 12 | const expected = ` 13 | SELECT 14 | "a","b c" 15 | FROM 16 | (select * from table) _cdb_epoch_transformation `; 17 | assert.strictEqual(actual, expected); 18 | }); 19 | }); 20 | --------------------------------------------------------------------------------