├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cli.js ├── docs └── api.md ├── lib ├── auth.js ├── bbox.js ├── bounds.js ├── clone.js ├── deltas.js ├── feature.js ├── import.js ├── revert.js ├── schema.js ├── server.js ├── tiles.js ├── user.js └── webhooks.js ├── package.json ├── test ├── fixtures │ ├── invalid-geojson-schema.json │ ├── schema.json │ ├── valid-geojson.json │ └── valid-geojson.schema ├── lib.auth.test.js ├── lib.deltas.test.js ├── lib.feature.test.js ├── lib.import.test.js ├── lib.register.test.js ├── lib.revert.test.js ├── lib.test.js ├── util.getAuth.test.js ├── util.revert.test.js ├── util.validateBbox.test.js └── util.validateGeojson.test.js ├── util ├── eot.js ├── get_auth.js ├── revert.js ├── validateBbox.js └── validateGeojson.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.15 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 22 | 23 | # Download and cache dependencies 24 | - restore_cache: 25 | keys: 26 | - v1-dependencies-{{ checksum "package.json" }} 27 | # fallback to using the latest cache if no exact match is found 28 | - v1-dependencies- 29 | 30 | - run: yarn install 31 | 32 | - save_cache: 33 | paths: 34 | - node_modules 35 | key: v1-dependencies-{{ checksum "package.json" }} 36 | 37 | # run tests! 38 | - run: yarn coverage 39 | 40 | 41 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-scoped-var": 2, 4 | "comma-dangle": [2, "only-multiline"], 5 | "comma-style": 2, 6 | "indent": 2, 7 | "keyword-spacing": 2, 8 | "no-console": 0, 9 | "no-constant-condition": 0, 10 | "no-else-return": 0, 11 | "no-extra-parens": [2, "functions"], 12 | "no-lonely-if": 2, 13 | "no-new": 2, 14 | "no-proto": 2, 15 | "no-unused-vars": [2,{"args": "none"}], 16 | "no-use-before-define": [2,"nofunc"], 17 | "no-useless-escape": 0, 18 | "space-before-blocks": 2, 19 | "space-before-function-paren": [2, "never"], 20 | "space-in-parens": 2, 21 | "require-jsdoc": ["error", { 22 | "require": { 23 | "FunctionExpression": true, 24 | "ClassDeclaration": true, 25 | "MethodDefinition": true 26 | } 27 | }], 28 | "valid-jsdoc": ["error", { 29 | "requireReturn": false, 30 | "requireParamDescription": true, 31 | "requireParamType": true 32 | }] 33 | }, 34 | "env": { 35 | "node": "true", 36 | "es6": "true" 37 | }, 38 | "extends": ["@mapbox/eslint-config-geocoding"] 39 | } 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | coverage.lcov 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## Emoji Cheatsheet 4 | - :pencil2: doc updates 5 | - :bug: when fixing a bug 6 | - :rocket: when making general improvements 7 | - :white_check_mark: when adding tests 8 | - :arrow_up: when upgrading dependencies 9 | - :tada: when adding new features 10 | 11 | ## Version History 12 | 13 | ### v12.0.0 14 | 15 | - :rocket: import module now accepts a stream or a string filepath 16 | - :rocket: [breaking] Remove ArrayReader in favour of unified stream 17 | - :white_check_mark: Add first basic integration test for import module 18 | - :rocket: Have import module query server for upload limits 19 | 20 | ### v11.2.0 21 | 22 | - :pencil2: Add JSDoc comments to all functions, and update ESLint & DocumentationJS to enforce this 23 | 24 | ### v11.1.0 25 | 26 | - :rocket: 10x speed of delta reversion by running feature history retrieval in parallel 27 | 28 | ### v11.0.1 29 | 30 | - :rocket: Retire `node-mb-string-size` module in favour of now correct `Buffer.length` 31 | 32 | ### v11.0.0 33 | 34 | - :tada: Add substanitally faster and better support for reverting deltas 35 | 36 | ### v10.3.0 37 | 38 | - :tada: Add support for the features history API 39 | 40 | ### v10.2.0 41 | 42 | - :tada: Add duplicate ID check to import API 43 | 44 | ### v10.1.0 45 | 46 | - :tada: Add automatic schema to features to be imported via the import API 47 | 48 | ### v10.0.3 49 | 50 | - :arrow_up: Update to latest base dependencies 51 | 52 | ### v10.0.2 53 | 54 | - :rocket: Add support for `offset` parameter for list deltas endpoint 55 | 56 | ### v10.0.1 57 | 58 | - :rocket: Add helper functions for features endpoints 59 | 60 | ### v10.0.0 61 | 62 | - :tada: Add support for authenticating all requests to Hecate 63 | - :rocket: Remove support for `stack` parameter and ELB lookup 64 | - :rocket: Remove support for `port` parameter; should be included with `url` 65 | - :rocket: Add `url` validation 66 | 67 | ### v9.1.0 68 | 69 | - :tada: Add support for the new webhooks API 70 | 71 | ### v9.0.4 72 | 73 | - :arrow_up: Update to latest deps 74 | 75 | ### v9.0.3 76 | 77 | - :bug: Show help information if no subcommand is given 78 | 79 | ### v9.0.2 80 | 81 | - :rocket: Add additional null geometry checks 82 | 83 | ### v9.0.1 84 | 85 | - :bug: Fix `hecate.stack` call with 2nd argument 86 | 87 | ### v9.0.0 88 | 89 | - :rocket: Drop support for node 8, lock to node 10 90 | 91 | ### v8.0.4 92 | 93 | - :pencil2: Update API docs to be generated by `DocumentationJS` 94 | 95 | ### v8.0.3 96 | 97 | - :bug: Update `--stack` cli command expectations 98 | - :pencil2: Fix formatting of help text 99 | 100 | ### v8.0.1 & v8.0.2 101 | 102 | - :rocket: Uniform `options.geometry` 103 | 104 | ### v8.0.0 105 | 106 | - :rocket: Add ability to include username & pass via `stack `instantiation 107 | 108 | ### v7.3.2 109 | 110 | - :bug: Fix options name mismatch on bounds set 111 | 112 | ### v7.3.1 113 | 114 | - :rocket: Add `setBound` alias 115 | 116 | ### v7.3.0 117 | 118 | - :tada: Add hecate meta bounds API 119 | - :pencil2: Add missing bounds documentation 120 | 121 | ### v7.2.0 122 | 123 | - :tada: Add basic `mvt` tiles API support 124 | 125 | ### v7.1.1 126 | 127 | - :bug: Avoid memory overflow bug if all features in a large file are invalid 128 | 129 | ### v7.1.0 130 | 131 | - :tada: Add `get` & `list` support for deltas API 132 | 133 | ### v7.0.1 134 | 135 | - :bug: Ensure data chunk with EOT is written with EOT stripped 136 | 137 | ### v7.0.0 138 | 139 | - :tada: Introduce EOT checks on all streaming API endpoints 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mapbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

HecateJS

2 | 3 |

Javascript Library and CLI for Hecate, the persistent, mutable data store focused on GeoJSON first interchange

4 | 5 | ## General Usage 6 | 7 | ### Installation 8 | 9 | **JS Library** 10 | 11 | ```sh 12 | yarn add '@mapbox/hecatejs' 13 | ``` 14 | 15 | **CLI** 16 | 17 | ```sh 18 | yarn global add '@mapbox/hecatejs' 19 | ``` 20 | 21 |

Instantiation

22 | 23 | Note: if the username & password is not explicitly set, Hecate will fallback to checking for 24 | a `HECATE_USERNAME` & `HECATE_PASSWORD` environment variable. For the `url` parameter, be sure to include the protocol and (if necessary) port number. 25 | 26 | **JS Library** 27 | 28 | ```js 29 | const Hecate = require('@mapbox/hecatejs'); 30 | 31 | const hecate = new Hecate({ 32 | username: 'ingalls', 33 | password: 'yeaheh', 34 | url: 'https://example.com/hecate', 35 | }); 36 | ``` 37 | 38 | **CLI** 39 | 40 | The CLI tool must be provided the URL to connect to for each subcommand. 41 | This can be accomplished by providing the URL to a local or remote Hecate server. Be sure to include the protocol and, for local connections, the port number. 42 | 43 | The --url option must be provided for every subcommand but is omitted in this guide for clarity. 44 | 45 | ```sh 46 | # Connecting to a remote hecate server 47 | ./cli.js --url 'https://example.com' 48 | ``` 49 | 50 | ```sh 51 | # Connecting to a local hecate server 52 | ./cli.js --url 'http://localhost:8000' 53 | ``` 54 | 55 |

API Documentation

56 | 57 | The [API documentation](/docs/api.md) can be found in the `docs/API.md` file. This file is automatically 58 | generated from the internal JSDocs. 59 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const prompt = require('prompt'); 6 | const auth = require('./util/get_auth'); 7 | const settings = require('./package.json'); 8 | 9 | /** 10 | * @class Hecate 11 | */ 12 | class Hecate { 13 | /** 14 | * @param {Object} api Global API Settings Object 15 | * @param {string} api.url URL of Hecate instance to interact with 16 | * @param {string} api.username Hecate Username 17 | * @param {string} api.password Hecate Password 18 | * @param {Object} api.auth_rules [optional] If used as a library, an object containing the 19 | * authentication rules of the instance as retrieved from 20 | * /api/auth 21 | * The CLI will automatically attempt to populate this value 22 | */ 23 | constructor(api = {}) { 24 | this.url = api.url ? new URL(api.url).toString() : 'http://localhost:8000'; 25 | this.user = false; 26 | 27 | if ((api.username || process.env.HECATE_USERNAME) && (api.password || process.env.HECATE_PASSWORD)) { 28 | this.user = { 29 | username: api.username ? api.username : process.env.HECATE_USERNAME, 30 | password: api.password ? api.password : process.env.HECATE_PASSWORD 31 | }; 32 | } 33 | 34 | this.auth_rules = api.auth_rules ? api.auth_rules : null; 35 | 36 | // Instantiate New Library Instances 37 | this._ = { 38 | auth: new (require('./lib/auth'))(this), 39 | bbox: new (require('./lib/bbox'))(this), 40 | webhooks: new (require('./lib/webhooks'))(this), 41 | tiles: new (require('./lib/tiles'))(this), 42 | clone: new (require('./lib/clone'))(this), 43 | bounds: new (require('./lib/bounds'))(this), 44 | feature: new (require('./lib/feature'))(this), 45 | deltas: new (require('./lib/deltas'))(this), 46 | user: new (require('./lib/user'))(this), 47 | schema: new (require('./lib/schema'))(this), 48 | server: new (require('./lib/server'))(this), 49 | import: new (require('./lib/import'))(this), 50 | revert: new (require('./lib/revert'))(this) 51 | }; 52 | 53 | // Add Helper Functions 54 | this.auth = (...opts) => this._.auth.get(...opts); 55 | this.clone = (...opts) => this._.clone.get(...opts); 56 | this.server = (...opts) => this._.server.get(...opts); 57 | this.bbox = (...opts) => this._.bbox.get(...opts); 58 | this.listDeltas = (...opts) => this._.deltas.list(...opts); 59 | this.getDelta = (...opts) => this._.deltas.get(...opts); 60 | this.listBounds = (...opts) => this._.bounds.list(...opts); 61 | this.setBound = (...opts) => this._.bounds.set(...opts); 62 | this.getBound = (...opts) => this._.bounds.get(...opts); 63 | this.getBoundMeta = (...opts) => this._.bounds.meta(...opts); 64 | this.register = (...opts) => this._.user.register(...opts); 65 | this.schema = (...opts) => this._.schema.get(...opts); 66 | this.import = (...opts) => this._.import.multi(...opts); 67 | this.revert = (...opts) => this._.revert.revert(...opts); 68 | this.getFeatureHistory = (...opts) => this._.feature.history(...opts); 69 | this.getFeatureKey = (...opts) => this._.feature.key(...opts); 70 | this.getFeature = (...opts) => this._.feature.get(...opts); 71 | } 72 | } 73 | 74 | module.exports = Hecate; 75 | 76 | // Run in CLI mode 77 | if (require.main === module) { 78 | const argv = require('minimist')(process.argv, { 79 | boolean: ['help', 'version'], 80 | alias: { 81 | version: 'v', 82 | help: '?' 83 | } 84 | }); 85 | 86 | if (argv.version) { 87 | console.error('hecate-cli@' + settings.version); 88 | process.exit(0); 89 | } else if (!argv._[2] || (!argv._[2] && argv.help) || argv._[2] === 'help') { 90 | console.error(''); 91 | console.error('usage: cli.js [--version] [--help]'); 92 | console.error(''); 93 | console.error('note: the --script flag can be applied to any mode to disable prompts'); 94 | console.error(' when used the user is responsible for making sure they have all the'); 95 | console.error(' correct flags'); 96 | console.error(''); 97 | console.error(''); 98 | console.error(' help Displays this message'); 99 | console.error(' user [--help] User Management'); 100 | console.error(' import [--help] Import data into the server'); 101 | console.error(' feature [--help] Download individual features & their history'); 102 | console.error(' schema [--help] Obtain the JSON schema for a given server'); 103 | console.error(' auth [--help] Obtain the JSON Auth document'); 104 | console.error(' bbox [--help] Download data via bbox from a given server'); 105 | console.error(' clone [--help] Download the complete server dataset'); 106 | console.error(' revert [--help] Revert data from an specified delta'); 107 | console.error(''); 108 | console.error(''); 109 | console.error(' --version Print the current version of the CLI'); 110 | console.error(' --help Print a help message'); 111 | console.error(); 112 | process.exit(0); 113 | } 114 | 115 | const command = (err, hecate) => { 116 | if (err) throw err; 117 | const command = argv._[2]; 118 | const subcommand = argv._[3]; 119 | 120 | if (command && !hecate._[command]) { 121 | console.error(); 122 | console.error(`"${command}" command not found!`); 123 | console.error(); 124 | process.exit(1); 125 | } else if (command && subcommand && !hecate._[command][subcommand]) { 126 | console.error(); 127 | console.error(`"${command} ${subcommand}" command not found!`); 128 | console.error(); 129 | process.exit(1); 130 | } else if (argv.help || !subcommand) { 131 | return hecate._[command].help(); 132 | } 133 | 134 | if (!argv.script) { 135 | prompt.message = '$'; 136 | prompt.start({ 137 | stdout: process.stderr 138 | }); 139 | 140 | prompt.get([{ 141 | name: 'url', 142 | message: 'URL to connect to local or remote Hecate instance. Be sure to include the protocol and port number for local instances, e.g. \'http://localhost:8000\'', 143 | type: 'string', 144 | required: 'true', 145 | default: hecate.url 146 | }], (err, res) => { 147 | if (err) throw err; 148 | hecate.url = new URL(res.url).toString(); 149 | argv.cli = true; 150 | 151 | // if a custom auth policy hasn't been passed 152 | if (!hecate.auth_rules) { 153 | // fetch auth 154 | hecate.auth({}, (err, auth_rules) => { 155 | // if requesting auth returns a 401 156 | if (err && err.message === '401: Unauthorized') { 157 | // if username and password isn't set, prompt for it 158 | if (!hecate.user) { 159 | prompt.get(auth(hecate.user), (err, res) => { 160 | if (err) throw err; 161 | hecate.user = { 162 | username: res.hecate_username, 163 | password: res.hecate_password 164 | }; 165 | // request auth again 166 | hecate.auth({}, (err, auth_rules) => { 167 | if (err) throw err; 168 | hecate.auth_rules = auth_rules; 169 | return run(); 170 | }); 171 | }); 172 | } else { 173 | return run(); 174 | } 175 | } else { 176 | hecate.auth_rules = auth_rules; 177 | return run(); 178 | } 179 | }); 180 | } else return run(); 181 | }); 182 | } else { 183 | return run(); 184 | } 185 | 186 | /** 187 | * Once Hecate instance is instantiated, run the requested command 188 | * 189 | * @private 190 | * @returns {undefined} 191 | */ 192 | function run() { 193 | if (!subcommand) { 194 | hecate[command](argv); 195 | } else { 196 | hecate._[command][subcommand](argv); 197 | } 198 | } 199 | }; 200 | 201 | command(null, new Hecate(argv)); 202 | } 203 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | 7 | /** 8 | * @class Auth 9 | * @public 10 | * 11 | * @see {@link https://github.com/mapbox/hecate#authentication|Hecate Documentation} 12 | */ 13 | class Auth { 14 | /** 15 | * Create a new Auth Instance 16 | * 17 | * @param {Hecate} api parent hecate instance 18 | */ 19 | constructor(api) { 20 | this.api = api; 21 | } 22 | 23 | /** 24 | * Print help documentation about the subcommand to stderr 25 | */ 26 | help() { 27 | console.error(); 28 | console.error('Fetch authentication settings that the server uses to allow or deny specific api endpoints'); 29 | console.error(); 30 | console.error('Usage: cli.js auth '); 31 | console.error(); 32 | console.error(':'); 33 | console.error(' get Download the authentication settings'); 34 | console.error(); 35 | } 36 | 37 | /** 38 | * Return the auth settings for a given hecate instance 39 | * 40 | * @param {!Object} options Options for making a request to the auth API 41 | * @param {function} cb (err, res) style callback function 42 | */ 43 | get(options = {}, cb) { 44 | const self = this; 45 | 46 | if (!options) options = {}; 47 | 48 | if (options.script) { 49 | cb = cli; 50 | main(); 51 | } else if (options.cli) { 52 | cb = cli; 53 | main(); 54 | } else { 55 | main(); 56 | } 57 | 58 | /** 59 | * Once the options object is populated, make the API request 60 | * @private 61 | */ 62 | function main() { 63 | request.get({ 64 | json: true, 65 | url: new URL('/api/auth', self.api.url), 66 | auth: self.api.user 67 | }, (err, res) => { 68 | if (err) return cb(err); 69 | 70 | if (res.statusCode === 404) return cb(new Error('404: Could not obtain auth list')); 71 | if (res.statusCode === 401) return cb(new Error('401: Unauthorized')); 72 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 73 | 74 | cb(null, res.body); 75 | }); 76 | } 77 | 78 | /** 79 | * If in CLI mode, write results to stdout 80 | * or throw any errors incurred 81 | * 82 | * @private 83 | * 84 | * @param {Error} err [optional] API Error 85 | * @param {Object} auth Hecate Auth JSON 86 | */ 87 | function cli(err, auth) { 88 | if (err) throw err; 89 | 90 | console.log(JSON.stringify(auth, null, 4)); 91 | } 92 | } 93 | } 94 | 95 | module.exports = Auth; 96 | -------------------------------------------------------------------------------- /lib/bbox.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const validateBbox = require('../util/validateBbox'); 8 | const auth = require('../util/get_auth'); 9 | const EOT = require('../util/eot'); 10 | 11 | /** 12 | * @class BBox 13 | * @public 14 | * 15 | * @see {@link https://github.com/mapbox/hecate#downloading-multiple-features-via-bbox|Hecate Documentation} 16 | */ 17 | class BBox { 18 | /** 19 | * Create a new BBOX Instance 20 | * 21 | * @param {Hecate} api parent hecate instance 22 | */ 23 | constructor(api) { 24 | this.api = api; 25 | } 26 | 27 | /** 28 | * Print help documentation about the subcommand to stderr 29 | */ 30 | help() { 31 | console.error(); 32 | console.error('Fetch raw data from the server using the bbox API'); 33 | console.error(); 34 | console.error('Usage: cli.js bbox '); 35 | console.error(); 36 | console.error(':'); 37 | console.error(' get Stream LDgeoJSON within the given BBOX'); 38 | console.error(); 39 | } 40 | 41 | /** 42 | * Queries hecate /api/data/features endpoint 43 | * Currently supports downloading features by bbox 44 | * 45 | * @param {!Object} options Options for making a request to the hecate /api/data/features endpoint 46 | * @param {boolean} [options.history] [default: false] If true, return current and historic features 47 | * within the given bbox. 48 | * @param {Array|string} [options.bbox] Bounding box of features to download from hecate 49 | * @param {Stream} [options.output] Stream to write line-delimited GeoJSON to 50 | * 51 | * @param {function} cb (err, res) style callback function 52 | * 53 | * @return {function} (err, res) style callback 54 | */ 55 | get(options = {}, cb) { 56 | const self = this; 57 | 58 | if (!options) options = {}; 59 | 60 | if (options.script) { 61 | cb = cli; 62 | 63 | options.output = process.stdout; 64 | 65 | return main(); 66 | } else if (options.cli) { 67 | cb = cli; 68 | 69 | prompt.message = '$'; 70 | prompt.start({ 71 | stdout: process.stderr 72 | }); 73 | 74 | let args = [{ 75 | name: 'bbox', 76 | message: 'bbox to download', 77 | required: true, 78 | type: 'string', 79 | default: options.bbox 80 | },{ 81 | name: 'history', 82 | message: 'download current & historic data', 83 | required: true, 84 | type: 'boolean', 85 | default: options.history === undefined ? false : true 86 | }]; 87 | 88 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.feature.get !== 'public') { 89 | args = args.concat(auth(self.api.user)); 90 | } 91 | 92 | prompt.get(args, (err, argv) => { 93 | prompt.stop(); 94 | 95 | if (argv.hecate_username) { 96 | self.api.user = { 97 | username: argv.hecate_username, 98 | password: argv.hecate_password 99 | }; 100 | } 101 | 102 | options.bbox = argv.bbox; 103 | options.output = process.stdout; 104 | options.history = argv.history; 105 | 106 | return main(); 107 | }); 108 | } else { 109 | return main(); 110 | } 111 | 112 | /** 113 | * Once the options object is populated, make the API request 114 | * @private 115 | * 116 | * @returns {undefined} 117 | */ 118 | function main() { 119 | if (!options.bbox) return cb(new Error('options.bbox required')); 120 | if (!options.output) return cb(new Error('options.output required')); 121 | 122 | // Validate options.bbox. Will throw an error it bbox is invalid 123 | if (options.bbox) validateBbox(options.bbox); 124 | 125 | let url; 126 | 127 | if (options.history) { 128 | url = new URL(`/api/data/features/history?bbox=${options.bbox}`, self.api.url); 129 | } else { 130 | url = new URL(`/api/data/features?bbox=${options.bbox}`, self.api.url); 131 | } 132 | 133 | request({ 134 | method: 'GET', 135 | url: url, 136 | auth: self.api.user 137 | }).on('error', (err) => { 138 | return cb(err); 139 | }).on('response', (res) => { 140 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 141 | }).pipe(new EOT(cb)).pipe(options.output); 142 | } 143 | 144 | /** 145 | * If in CLI mode, write results to stdout 146 | * or throw any errors incurred 147 | * 148 | * @private 149 | * 150 | * @param {Error} err [optional] API Error 151 | * 152 | * @returns {undefined} 153 | */ 154 | function cli(err) { 155 | if (err) throw err; 156 | } 157 | } 158 | } 159 | 160 | module.exports = BBox; 161 | -------------------------------------------------------------------------------- /lib/bounds.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | const EOT = require('../util/eot'); 9 | 10 | /** 11 | * @class Bounds 12 | * @public 13 | * 14 | * @see {@link https://github.com/mapbox/hecate#boundaries|Hecate Documentation} 15 | */ 16 | class Bounds { 17 | /** 18 | * Create a new Bounds Instance 19 | * 20 | * @param {Hecate} api parent hecate instance 21 | */ 22 | constructor(api) { 23 | this.api = api; 24 | } 25 | 26 | /** 27 | * Print help documentation about the subcommand to stderr 28 | */ 29 | help() { 30 | console.error(); 31 | console.error('Fetch raw data from the server using the bounds API'); 32 | console.error(); 33 | console.error('Usage: cli.js bounds '); 34 | console.error(); 35 | console.error(''); 36 | console.error(' get Download geometries within a given bounds'); 37 | console.error(' stats Get stats for a given bounds'); 38 | console.error(' meta Get underlying boundary geojson'); 39 | console.error(' delete Delete a given bound'); 40 | console.error(' set Create a new bound'); 41 | console.error(' list List all potential bounds names'); 42 | console.error(); 43 | } 44 | 45 | /** 46 | * Return stats of geo data within a give bounds 47 | * 48 | * @param {!Object} options options for making a query to the bounds list endpoint 49 | * @param {function} cb (err, res) style callback function 50 | * 51 | * @return {function} (err, res) style callback 52 | */ 53 | stats(options = {}, cb) { 54 | const self = this; 55 | 56 | if (!options) options = {}; 57 | 58 | if (options.script) { 59 | cb = cli; 60 | return main(); 61 | } else if (options.cli) { 62 | cb = cli; 63 | 64 | prompt.message = '$'; 65 | prompt.start({ 66 | stdout: process.stderr 67 | }); 68 | 69 | let args = [{ 70 | name: 'bound', 71 | message: 'bound to download', 72 | required: true, 73 | type: 'string', 74 | default: options.bound 75 | }]; 76 | 77 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.stats.get !== 'public') { 78 | args = args.concat(auth(self.api.user)); 79 | } 80 | 81 | prompt.get(args, (err, argv) => { 82 | if (err) throw err; 83 | 84 | prompt.stop(); 85 | 86 | if (argv.hecate_username) { 87 | self.api.user = { 88 | username: argv.hecate_username, 89 | password: argv.hecate_password 90 | }; 91 | } 92 | 93 | options.bound = argv.bound; 94 | 95 | return main(); 96 | }); 97 | } else { 98 | return main(); 99 | } 100 | 101 | /** 102 | * Once the options object is populated, make the API request 103 | * @private 104 | * 105 | * @returns {undefined} 106 | */ 107 | function main() { 108 | if (!options.bound) return cb(new Error('options.bound required')); 109 | 110 | request({ 111 | method: 'GET', 112 | json: true, 113 | url: new URL(`/api/data/bounds/${options.bound}/stats`, self.api.url), 114 | auth: self.api.user 115 | }, (err, res) => { 116 | if (err) return cb(err); 117 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 118 | 119 | return cb(null, res.body); 120 | }); 121 | } 122 | 123 | /** 124 | * If in CLI mode, write results to stdout 125 | * or throw any errors incurred 126 | * 127 | * @private 128 | * 129 | * @param {Error} err [optional] API Error 130 | * @param {Object} stats stats object 131 | * 132 | * @returns {undefined} 133 | */ 134 | function cli(err, stats) { 135 | if (err) throw err; 136 | 137 | console.log(JSON.stringify(stats, null, 4)); 138 | } 139 | } 140 | 141 | /** 142 | * Return a list of the bounds that are currently loaded on the server 143 | * 144 | * @param {!Object} options options for making a query to the bounds list endpoint 145 | * @param {function} cb (err, res) style callback function 146 | * 147 | * @return {function} (err, res) style callback 148 | */ 149 | list(options = {}, cb) { 150 | const self = this; 151 | 152 | if (!options) options = {}; 153 | 154 | if (options.script) { 155 | cb = cli; 156 | return main(); 157 | } else if (options.cli) { 158 | cb = cli; 159 | 160 | prompt.message = '$'; 161 | prompt.start({ 162 | stdout: process.stderr 163 | }); 164 | 165 | let args = []; 166 | 167 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.bounds.list !== 'public') { 168 | args = args.concat(auth(self.api.user)); 169 | } 170 | 171 | prompt.get(args, (err, argv) => { 172 | if (err) throw err; 173 | 174 | prompt.stop(); 175 | 176 | if (argv.hecate_username) { 177 | self.api.user = { 178 | username: argv.hecate_username, 179 | password: argv.hecate_password 180 | }; 181 | } 182 | 183 | return main(); 184 | }); 185 | } else { 186 | return main(); 187 | } 188 | 189 | /** 190 | * Once the options object is populated, make the API request 191 | * @private 192 | * 193 | * @returns {undefined} 194 | */ 195 | function main() { 196 | request({ 197 | method: 'GET', 198 | json: true, 199 | url: new URL('/api/data/bounds', self.api.url), 200 | auth: self.api.user 201 | }, (err, res) => { 202 | if (err) return cb(err); 203 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 204 | 205 | return cb(null, res.body); 206 | }); 207 | } 208 | 209 | /** 210 | * If in CLI mode, write results to stdout 211 | * or throw any errors incurred 212 | * 213 | * @private 214 | * 215 | * @param {Error} err [optional] API Error 216 | * @param {Object} bounds list of bounds 217 | * 218 | * @returns {undefined} 219 | */ 220 | function cli(err, bounds) { 221 | if (err) throw err; 222 | 223 | console.log(JSON.stringify(bounds, null, 4)); 224 | } 225 | } 226 | 227 | /** 228 | * Queries the /api/data/bounds endpoints, returning a 229 | * line-delimited stream of GeoJSON Features 230 | * 231 | * @param {!Object} options Options for making a request to the bounds endpoint 232 | * @param {String} [options.bound] Name of the bound to download from 233 | * @param {Stream} [options.output] Stream to write line-delimited GeoJSON to 234 | * @param {function} cb (err, res) style callback function 235 | * 236 | * @return {function} (err, res) style callback 237 | */ 238 | get(options = {}, cb) { 239 | const self = this; 240 | 241 | if (!options) options = {}; 242 | 243 | if (options.script) { 244 | cb = cli; 245 | 246 | options.output = process.stdout; 247 | 248 | return main(); 249 | } else if (options.cli) { 250 | cb = cli; 251 | 252 | prompt.message = '$'; 253 | prompt.start({ 254 | stdout: process.stderr 255 | }); 256 | 257 | let args = [{ 258 | name: 'bound', 259 | message: 'bound to download', 260 | required: true, 261 | type: 'string', 262 | default: options.bound 263 | }]; 264 | 265 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.bounds.get !== 'public') { 266 | args = args.concat(auth(self.api.user)); 267 | } 268 | 269 | prompt.get(args, (err, argv) => { 270 | if (err) throw err; 271 | 272 | prompt.stop(); 273 | 274 | if (argv.hecate_username) { 275 | self.api.user = { 276 | username: argv.hecate_username, 277 | password: argv.hecate_password 278 | }; 279 | } 280 | 281 | options.output = process.stdout; 282 | options.bound = argv.bound; 283 | 284 | return main(); 285 | }); 286 | } else { 287 | return main(); 288 | } 289 | 290 | /** 291 | * Once the options object is populated, make the API request 292 | * @private 293 | * 294 | * @returns {undefined} 295 | */ 296 | function main() { 297 | if (!options.bound) return cb(new Error('options.bound required')); 298 | if (!options.output) return cb(new Error('options.output required')); 299 | 300 | request({ 301 | method: 'GET', 302 | url: new URL(`/api/data/bounds/${options.bound}`, self.api.url), 303 | auth: self.api.user 304 | }).on('error', (err) => { 305 | return cb(err); 306 | }).on('response', (res) => { 307 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 308 | }).pipe(new EOT(cb)).pipe(options.output); 309 | } 310 | 311 | /** 312 | * If in CLI mode, write results to stdout 313 | * or throw any errors incurred 314 | * 315 | * @private 316 | * 317 | * @param {Error} err [optional] API Error 318 | * 319 | * @returns {undefined} 320 | */ 321 | function cli(err) { 322 | if (err) throw err; 323 | } 324 | } 325 | 326 | /** 327 | * Returns underlying bounds geojson for a given bounds 328 | * 329 | * @param {!Object} options Options for making a request to the bounds endpoint 330 | * @param {String} [options.bound] Name of the bound to download from 331 | * @param {function} cb (err, res) style callback function 332 | * 333 | * @return {function} (err, res) style callback 334 | */ 335 | meta(options = {}, cb) { 336 | const self = this; 337 | 338 | if (!options) options = {}; 339 | 340 | if (options.script) { 341 | cb = cli; 342 | 343 | options.output = process.stdout; 344 | 345 | return main(); 346 | } else if (options.cli) { 347 | cb = cli; 348 | 349 | prompt.message = '$'; 350 | prompt.start({ 351 | stdout: process.stderr 352 | }); 353 | 354 | let args = [{ 355 | name: 'bound', 356 | message: 'bound to download', 357 | required: true, 358 | type: 'string', 359 | default: options.bound 360 | }]; 361 | 362 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.bounds.get !== 'public') { 363 | args = args.concat(auth(self.api.user)); 364 | } 365 | 366 | prompt.get(args, (err, argv) => { 367 | if (err) throw err; 368 | 369 | prompt.stop(); 370 | 371 | if (argv.hecate_username) { 372 | self.api.user = { 373 | username: argv.hecate_username, 374 | password: argv.hecate_password 375 | }; 376 | } 377 | 378 | options.bound = argv.bound; 379 | 380 | return main(); 381 | }); 382 | } else { 383 | return main(); 384 | } 385 | 386 | /** 387 | * Once the options object is populated, make the API request 388 | * @private 389 | * 390 | * @returns {undefined} 391 | */ 392 | function main() { 393 | if (!options.bound) return cb(new Error('options.bound required')); 394 | 395 | request({ 396 | method: 'GET', 397 | url: new URL(`/api/data/bounds/${options.bound}/meta`, self.api.url), 398 | auth: self.api.user 399 | }, (err, res) => { 400 | if (err) return cb(err); 401 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 402 | 403 | return cb(null, res.body); 404 | }); 405 | } 406 | 407 | /** 408 | * If in CLI mode, write results to stdout 409 | * or throw any errors incurred 410 | * 411 | * @private 412 | * 413 | * @param {Error} err [optional] API Error 414 | * @param {Object} meta meta data about a given bound 415 | * 416 | * @returns {undefined} 417 | */ 418 | function cli(err, meta) { 419 | if (err) throw err; 420 | 421 | console.log(JSON.stringify(meta, null, 4)); 422 | } 423 | } 424 | 425 | /** 426 | * Delete a boundary file 427 | * 428 | * @param {!Object} options Options for making a request to the bounds endpoint 429 | * @param {String} [options.bound] Name of the bound to download from 430 | * @param {function} cb (err, res) style callback function 431 | * 432 | * @return {function} (err, res) style callback 433 | */ 434 | delete(options = {}, cb) { 435 | const self = this; 436 | 437 | if (!options) options = {}; 438 | 439 | if (options.script) { 440 | cb = cli; 441 | return main(); 442 | } else if (options.cli) { 443 | cb = cli; 444 | 445 | prompt.message = '$'; 446 | prompt.start({ 447 | stdout: process.stderr 448 | }); 449 | 450 | let args = [{ 451 | name: 'bound', 452 | message: 'bound to delete', 453 | required: true, 454 | type: 'string', 455 | default: options.bound 456 | }]; 457 | 458 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.bounds.delete !== 'public') { 459 | args = args.concat(auth(self.api.user)); 460 | } 461 | 462 | prompt.get(args, (err, argv) => { 463 | if (err) throw err; 464 | 465 | prompt.stop(); 466 | 467 | if (argv.hecate_username) { 468 | self.api.user = { 469 | username: argv.hecate_username, 470 | password: argv.hecate_password 471 | }; 472 | } 473 | 474 | options.bound = argv.bound; 475 | 476 | return main(); 477 | }); 478 | } else { 479 | return main(); 480 | } 481 | 482 | /** 483 | * Once the options object is populated, make the API request 484 | * @private 485 | * 486 | * @returns {undefined} 487 | */ 488 | function main() { 489 | if (!options.bound) return cb(new Error('options.bound required')); 490 | 491 | request({ 492 | method: 'DELETE', 493 | url: new URL(`/api/data/bounds/${options.bound}`, self.api.url), 494 | auth: self.api.user 495 | }, (err, res) => { 496 | if (err) return cb(err); 497 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 498 | 499 | return cb(null, true); 500 | }); 501 | } 502 | 503 | /** 504 | * If in CLI mode, write results to stdout 505 | * or throw any errors incurred 506 | * 507 | * @private 508 | * 509 | * @param {Error} err [optional] API Error 510 | * 511 | * @returns {undefined} 512 | */ 513 | function cli(err) { 514 | if (err) throw err; 515 | 516 | console.error('ok - deleted bound'); 517 | } 518 | } 519 | 520 | /** 521 | * Create or update a boundary file 522 | * 523 | * @param {!Object} options Options for making a request to the bounds endpoint 524 | * @param {String} [options.bound] Name of the bound to download from 525 | * @param {String} [options.geom] JSON Geometry of bound 526 | * @param {function} cb (err, res) style callback function 527 | * 528 | * @return {function} (err, res) style callback 529 | */ 530 | set(options = {}, cb) { 531 | const self = this; 532 | 533 | if (!options) options = {}; 534 | 535 | if (options.script) { 536 | cb = cli; 537 | return main(); 538 | } else if (options.cli) { 539 | cb = cli; 540 | 541 | prompt.message = '$'; 542 | prompt.start({ 543 | stdout: process.stderr 544 | }); 545 | 546 | let args = [{ 547 | name: 'bound', 548 | message: 'name of bound to create', 549 | required: true, 550 | type: 'string', 551 | default: options.bound 552 | },{ 553 | name: 'geometry', 554 | message: 'GeoJSON geometry of bound to create', 555 | required: true, 556 | type: 'string', 557 | default: options.geometry 558 | }]; 559 | 560 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.bounds.create !== 'public') { 561 | args = args.concat(auth(self.api.user)); 562 | } 563 | 564 | prompt.get(args, (err, argv) => { 565 | if (err) throw err; 566 | 567 | prompt.stop(); 568 | 569 | if (argv.hecate_username) { 570 | self.api.user = { 571 | username: argv.hecate_username, 572 | password: argv.hecate_password 573 | }; 574 | } 575 | 576 | options.bound = argv.bound; 577 | options.geometry = argv.geometry; 578 | 579 | return main(); 580 | }); 581 | } else { 582 | return main(); 583 | } 584 | 585 | /** 586 | * Once the options object is populated, make the API request 587 | * @private 588 | * 589 | * @returns {undefined} 590 | */ 591 | function main() { 592 | if (!options.bound) return cb(new Error('options.bound required')); 593 | if (!options.geometry) return cb(new Error('options.geometry required')); 594 | 595 | request({ 596 | method: 'POST', 597 | json: true, 598 | url: new URL(`/api/data/bounds/${options.bound}`, self.api.url), 599 | body: { 600 | type: 'Feature', 601 | properties: { }, 602 | geometry: options.geometry 603 | }, 604 | auth: self.api.user 605 | }, (err, res) => { 606 | if (err) return cb(err); 607 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 608 | 609 | return cb(null, true); 610 | }); 611 | } 612 | 613 | /** 614 | * If in CLI mode, write results to stdout 615 | * or throw any errors incurred 616 | * 617 | * @private 618 | * 619 | * @param {Error} err [optional] API Error 620 | * 621 | * @returns {undefined} 622 | */ 623 | function cli(err) { 624 | if (err) throw err; 625 | 626 | console.error('ok - set bound'); 627 | } 628 | } 629 | } 630 | 631 | module.exports = Bounds; 632 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | const EOT = require('../util/eot'); 9 | 10 | /** 11 | * @class Clone 12 | * @public 13 | * 14 | * @see {@link https://github.com/mapbox/hecate#downloading-via-clone|Hecate Documentation} 15 | */ 16 | class Clone { 17 | /** 18 | * Create a new Clone Instance 19 | * 20 | * @param {Hecate} api parent hecate instance 21 | */ 22 | constructor(api) { 23 | this.api = api; 24 | } 25 | 26 | /** 27 | * Print help documentation about the subcommand to stderr 28 | */ 29 | help() { 30 | console.error(); 31 | console.error('Fetch a complete dataset from the server'); 32 | console.error(); 33 | console.error('Usage: cli.js clone '); 34 | console.error(); 35 | console.error(':'); 36 | console.error(' get Stream LDgeoJSON of all the data on the server'); 37 | console.error(); 38 | } 39 | 40 | /** 41 | * Clone all data on a given hecate server 42 | * 43 | * @param {!Object} options Options for making a request to the hecate /api/data/features endpoint 44 | * @param {Stream} [options.output] Stream to write line-delimited GeoJSON to 45 | * @param {function} cb (err, res) style callback function 46 | * 47 | * @return {function} (err, res) style callback 48 | */ 49 | get(options = {}, cb) { 50 | const self = this; 51 | 52 | if (!options) options = {}; 53 | 54 | if (options.script) { 55 | cb = cli; 56 | 57 | options.output = process.stdout; 58 | 59 | return main(); 60 | } else if (options.cli) { 61 | cb = cli; 62 | 63 | prompt.message = '$'; 64 | prompt.start({ 65 | stdout: process.stderr 66 | }); 67 | 68 | let args = []; 69 | 70 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.clone.get !== 'public') { 71 | args = args.concat(auth(self.api.user)); 72 | } 73 | 74 | prompt.get(args, (err, argv) => { 75 | prompt.stop(); 76 | 77 | if (argv.hecate_username) { 78 | self.api.user = { 79 | username: argv.hecate_username, 80 | password: argv.hecate_password 81 | }; 82 | } 83 | 84 | options.output = process.stdout; 85 | 86 | return main(); 87 | }); 88 | } else { 89 | return main(); 90 | } 91 | 92 | /** 93 | * Once the options object is populated, make the API request 94 | * @private 95 | * 96 | * @returns {undefined} 97 | */ 98 | function main() { 99 | if (!options.output) return cb(new Error('options.output required')); 100 | 101 | request({ 102 | method: 'GET', 103 | url: new URL('/api/data/clone', self.api.url), 104 | auth: self.api.user 105 | }).on('error', (err) => { 106 | return cb(err); 107 | }).on('response', (res) => { 108 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 109 | }).pipe(new EOT(cb)).pipe(options.output); 110 | } 111 | 112 | /** 113 | * If in CLI mode, write results to stdout 114 | * or throw any errors incurred 115 | * 116 | * @private 117 | * 118 | * @param {Error} err [optional] API Error 119 | * 120 | * @returns {undefined} 121 | */ 122 | function cli(err) { 123 | if (err) throw err; 124 | } 125 | } 126 | } 127 | 128 | module.exports = Clone; 129 | -------------------------------------------------------------------------------- /lib/deltas.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class Deltas 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/api-geocoder/pull/2634#issuecomment-481255528|Hecate Documentation} 14 | */ 15 | class Deltas { 16 | /** 17 | * Create a new Deltas Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('Fetch a list of deltas or information about a single delta'); 31 | console.error(); 32 | console.error('usage: cli.js delta '); 33 | console.error(); 34 | console.error(''); 35 | console.error(' list List recent deltas'); 36 | console.error(' get Get information about a particular delta'); 37 | } 38 | 39 | /** 40 | * Queries the recent deltas list, returning the most recent 100 deltas 41 | * 42 | * @param {!Object} options Options for making a request to the deltas endpoint 43 | * @param {String} [options.limit=100] Number of deltas to list by default 44 | * @param {String} [options.offset] delta id to start listing at 45 | * @param {function} cb (err, res) style callback function 46 | * 47 | * @return {function} (err, res) style callback 48 | */ 49 | list(options = {}, cb) { 50 | const self = this; 51 | 52 | if (!options.limit) options.limit = 100; 53 | 54 | if (!options) options = {}; 55 | 56 | if (options.script) { 57 | cb = cli; 58 | return main(); 59 | } else if (options.cli) { 60 | cb = cli; 61 | 62 | prompt.message = '$'; 63 | prompt.start({ 64 | stdout: process.stderr 65 | }); 66 | 67 | let args = [{ 68 | name: 'limit', 69 | message: 'number of deltas to retrieve', 70 | required: true, 71 | type: 'string', 72 | default: options.limit 73 | }, { 74 | name: 'offset', 75 | message: 'delta id to start listing at', 76 | required: false, 77 | type: 'string' 78 | }]; 79 | 80 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.delta.list !== 'public') { 81 | args = args.concat(auth(self.api.user)); 82 | } 83 | 84 | prompt.get(args, (err, argv) => { 85 | if (err) throw err; 86 | 87 | prompt.stop(); 88 | 89 | if (argv.hecate_username) { 90 | self.api.user = { 91 | username: argv.hecate_username, 92 | password: argv.hecate_password 93 | }; 94 | } 95 | 96 | options.limit = argv.limit; 97 | options.offset = argv.offset || null; 98 | 99 | return main(); 100 | }); 101 | } else { 102 | return main(); 103 | } 104 | 105 | /** 106 | * Once the options object is populated, make the API request 107 | * @private 108 | * 109 | * @returns {undefined} 110 | */ 111 | function main() { 112 | if (!options.limit) options.limit = 100; 113 | const query = options.offset ? 114 | `/api/deltas?limit=${options.limit}&offset=${options.offset}` 115 | : `/api/deltas?limit=${options.limit}`; 116 | request({ 117 | json: true, 118 | method: 'GET', 119 | url: new URL(query, self.api.url), 120 | auth: self.api.user 121 | }, (err, res) => { 122 | if (err) return cb(err); 123 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 124 | 125 | return cb(err, res.body); 126 | }); 127 | } 128 | 129 | /** 130 | * If in CLI mode, write results to stdout 131 | * or throw any errors incurred 132 | * 133 | * @private 134 | * 135 | * @param {Error} err [optional] API Error 136 | * @param {Object} deltas list of deltas 137 | * 138 | * @returns {undefined} 139 | */ 140 | function cli(err, deltas) { 141 | if (err) throw err; 142 | 143 | console.log(JSON.stringify(deltas, null, 4)); 144 | } 145 | } 146 | 147 | /** 148 | * Returns data about a specific delta 149 | * 150 | * @param {!Object} options Options for making a request to the deltas endpoint 151 | * @param {function} cb (err, res) style callback function 152 | * 153 | * @return {function} (err, res) style callback 154 | */ 155 | get(options = {}, cb) { 156 | const self = this; 157 | 158 | if (!options) options = {}; 159 | 160 | if (options.script) { 161 | cb = cli; 162 | return main(); 163 | } else if (options.cli) { 164 | cb = cli; 165 | 166 | prompt.message = '$'; 167 | prompt.start({ 168 | stdout: process.stderr 169 | }); 170 | 171 | let args = [{ 172 | name: 'delta', 173 | message: 'ID of delta to retrieve', 174 | required: true, 175 | type: 'string', 176 | default: options.delta 177 | }]; 178 | 179 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.delta.list !== 'public') { 180 | args = args.concat(auth(self.api.user)); 181 | } 182 | 183 | prompt.get(args, (err, argv) => { 184 | if (err) throw err; 185 | 186 | prompt.stop(); 187 | 188 | if (argv.hecate_username) { 189 | self.api.user = { 190 | username: argv.hecate_username, 191 | password: argv.hecate_password 192 | }; 193 | } 194 | 195 | options.delta = argv.delta; 196 | 197 | return main(); 198 | }); 199 | } else { 200 | return main(); 201 | } 202 | 203 | /** 204 | * Once the options object is populated, make the API request 205 | * @private 206 | * 207 | * @returns {undefined} 208 | */ 209 | function main() { 210 | if (!options.delta) return cb(new Error('options.delta required')); 211 | 212 | request({ 213 | json: true, 214 | method: 'GET', 215 | url: new URL(`/api/delta/${options.delta}`, self.api.url), 216 | auth: self.api.user 217 | }, (err, res) => { 218 | if (err) return cb(err); 219 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 220 | 221 | return cb(err, res.body); 222 | }); 223 | } 224 | 225 | /** 226 | * If in CLI mode, write results to stdout 227 | * or throw any errors incurred 228 | * 229 | * @private 230 | * 231 | * @param {Error} err [optional] API Error 232 | * @param {Object} delta delta object 233 | * 234 | * @returns {undefined} 235 | */ 236 | function cli(err, delta) { 237 | if (err) throw err; 238 | 239 | console.log(JSON.stringify(delta, null, 4)); 240 | } 241 | } 242 | } 243 | 244 | module.exports = Deltas; 245 | -------------------------------------------------------------------------------- /lib/feature.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class Feature 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/hecate#downloading-individual-features|Hecate Documentation} 14 | */ 15 | class Feature { 16 | /** 17 | * Create a new Feature Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('Fetch an individual feature and it\'s corresponding metadata'); 31 | console.error(); 32 | console.error('usage: cli.js feature '); 33 | console.error(); 34 | console.error(''); 35 | console.error(' get Download an individual feature by ID'); 36 | console.error(' key Download an individual feature by key'); 37 | console.error(' history Download the history of a feature by ID'); 38 | } 39 | 40 | /** 41 | * Queries the feature store endpoint, returning a history of a 42 | * GeoJSON Feature 43 | * 44 | * @param {!Object} options Options for making a request to the bounds endpoint 45 | * @param {String} [options.feature] ID of the feature to download from 46 | * @param {function} cb (err, res) style callback function 47 | * 48 | * @return {function} (err, res) style callback 49 | */ 50 | history(options = {}, cb) { 51 | const self = this; 52 | 53 | if (!options) options = {}; 54 | 55 | if (options.script) { 56 | cb = cli; 57 | return main(); 58 | } else if (options.cli) { 59 | cb = cli; 60 | 61 | prompt.message = '$'; 62 | prompt.start({ 63 | stdout: process.stderr 64 | }); 65 | 66 | let args = [{ 67 | name: 'feature', 68 | message: 'feature id to download', 69 | required: true, 70 | type: 'string', 71 | default: options.feature 72 | }]; 73 | 74 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.feature.get !== 'public') { 75 | args = args.concat(auth(self.api.user)); 76 | } 77 | 78 | prompt.get(args, (err, argv) => { 79 | if (err) throw err; 80 | 81 | prompt.stop(); 82 | 83 | if (argv.hecate_username) { 84 | self.api.user = { 85 | username: argv.hecate_username, 86 | password: argv.hecate_password 87 | }; 88 | } 89 | 90 | options.feature = argv.feature; 91 | 92 | return main(); 93 | }); 94 | } else { 95 | return main(); 96 | } 97 | 98 | /** 99 | * Once the options object is populated, make the API request 100 | * @private 101 | * 102 | * @returns {undefined} 103 | */ 104 | function main() { 105 | if (!options.feature) return cb(new Error('options.feature required')); 106 | 107 | request({ 108 | json: true, 109 | method: 'GET', 110 | url: new URL(`/api/data/feature/${options.feature}/history`, self.api.url), 111 | auth: self.api.user 112 | }, (err, res) => { 113 | if (err) return cb(err); 114 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 115 | 116 | return cb(err, res.body); 117 | }); 118 | } 119 | 120 | /** 121 | * If in CLI mode, write results to stdout 122 | * or throw any errors incurred 123 | * 124 | * @private 125 | * 126 | * @param {Error} err [optional] API Error 127 | * @param {Object} history history of the feature 128 | * 129 | * @returns {undefined} 130 | */ 131 | function cli(err, history) { 132 | if (err) throw err; 133 | 134 | console.log(JSON.stringify(history, null, 4)); 135 | } 136 | } 137 | 138 | /** 139 | * Queries the feature store endpoint by key, returning a 140 | * GeoJSON Feature 141 | * 142 | * @param {!Object} options Options for making a request to the bounds endpoint 143 | * @param {String} [options.feature] key of the feature to download from 144 | * @param {function} cb (err, res) style callback function 145 | * 146 | * @return {function} (err, res) style callback 147 | */ 148 | key(options = {}, cb) { 149 | const self = this; 150 | 151 | if (!options) options = {}; 152 | 153 | if (options.script) { 154 | cb = cli; 155 | return main(); 156 | } else if (options.cli) { 157 | cb = cli; 158 | 159 | prompt.message = '$'; 160 | prompt.start({ 161 | stdout: process.stderr 162 | }); 163 | 164 | let args = [{ 165 | name: 'feature', 166 | message: 'feature key to download', 167 | required: true, 168 | type: 'string', 169 | default: options.feature 170 | }]; 171 | 172 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.feature.get !== 'public') { 173 | args = args.concat(auth(self.api.user)); 174 | } 175 | 176 | prompt.get(args, (err, argv) => { 177 | if (err) throw err; 178 | 179 | prompt.stop(); 180 | 181 | if (argv.hecate_username) { 182 | self.api.user = { 183 | username: argv.hecate_username, 184 | password: argv.hecate_password 185 | }; 186 | } 187 | 188 | options.feature = argv.feature; 189 | 190 | return main(); 191 | }); 192 | } else { 193 | return main(); 194 | } 195 | 196 | /** 197 | * Once the options object is populated, make the API request 198 | * @private 199 | * 200 | * @returns {undefined} 201 | */ 202 | function main() { 203 | if (!options.feature) return cb(new Error('options.feature required')); 204 | 205 | request({ 206 | json: true, 207 | method: 'GET', 208 | url: new URL(`/api/data/feature?key=${encodeURIComponent(options.feature)}`, self.api.url), 209 | auth: self.api.user 210 | }, (err, res) => { 211 | if (err) return cb(err); 212 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 213 | 214 | return cb(err, res.body); 215 | }); 216 | } 217 | 218 | /** 219 | * If in CLI mode, write results to stdout 220 | * or throw any errors incurred 221 | * 222 | * @private 223 | * 224 | * @param {Error} err [optional] API Error 225 | * @param {Object} feature requested feature 226 | * 227 | * @returns {undefined} 228 | */ 229 | function cli(err, feature) { 230 | if (err) throw err; 231 | 232 | console.log(JSON.stringify(feature, null, 4)); 233 | } 234 | } 235 | 236 | /** 237 | * Queries the feature store endpoint, returning a 238 | * GeoJSON Feature 239 | * 240 | * @param {!Object} options Options for making a request to the bounds endpoint 241 | * @param {String} [options.feature] ID of the feature to download from 242 | * @param {function} cb (err, res) style callback function 243 | * 244 | * @return {function} (err, res) style callback 245 | */ 246 | get(options = {}, cb) { 247 | const self = this; 248 | 249 | if (!options) options = {}; 250 | 251 | if (options.script) { 252 | cb = cli; 253 | return main(); 254 | } else if (options.cli) { 255 | cb = cli; 256 | 257 | prompt.message = '$'; 258 | prompt.start({ 259 | stdout: process.stderr 260 | }); 261 | 262 | let args = [{ 263 | name: 'feature', 264 | message: 'feature id to download', 265 | required: true, 266 | type: 'string', 267 | default: options.feature 268 | }]; 269 | 270 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.feature.get !== 'public') { 271 | args = args.concat(auth(self.api.user)); 272 | } 273 | 274 | prompt.get(args, (err, argv) => { 275 | if (err) throw err; 276 | 277 | prompt.stop(); 278 | 279 | if (argv.hecate_username) { 280 | self.api.user = { 281 | username: argv.hecate_username, 282 | password: argv.hecate_password 283 | }; 284 | } 285 | 286 | options.feature = argv.feature; 287 | 288 | return main(); 289 | }); 290 | } else { 291 | return main(); 292 | } 293 | 294 | /** 295 | * Once the options object is populated, make the API request 296 | * @private 297 | * 298 | * @returns {undefined} 299 | */ 300 | function main() { 301 | if (!options.feature) return cb(new Error('options.feature required')); 302 | 303 | request({ 304 | json: true, 305 | method: 'GET', 306 | url: new URL(`/api/data/feature/${options.feature}`, self.api.url), 307 | auth: self.api.user 308 | }, (err, res) => { 309 | if (err) return cb(err); 310 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 311 | 312 | return cb(err, res.body); 313 | }); 314 | } 315 | 316 | /** 317 | * If in CLI mode, write results to stdout 318 | * or throw any errors incurred 319 | * 320 | * @private 321 | * 322 | * @param {Error} err [optional] API Error 323 | * @param {Object} feature requested feature 324 | * 325 | * @returns {undefined} 326 | */ 327 | function cli(err, feature) { 328 | if (err) throw err; 329 | 330 | console.log(JSON.stringify(feature, null, 4)); 331 | } 332 | } 333 | } 334 | 335 | module.exports = Feature; 336 | -------------------------------------------------------------------------------- /lib/import.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | // Maxish number of bytes to upload per delta (Default allows up to 20m) 6 | const DEFAULT_UPLOAD = 1048576; 7 | 8 | const os = require('os'); 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const split = require('split'); 12 | const pipeline = require('stream').pipeline; 13 | const rewind = require('geojson-rewind'); 14 | const prompt = require('prompt'); 15 | const config = require('../package.json'); 16 | const validateGeojson = require('../util/validateGeojson.js'); 17 | const auth = require('../util/get_auth'); 18 | const request = require('requestretry'); 19 | const readLineSync = require('n-readlines'); 20 | 21 | /** 22 | * @class Import 23 | * @public 24 | * 25 | * @see {@link https://github.com/mapbox/hecate#feature-creation|Hecate Documentation} 26 | */ 27 | class Import { 28 | /** 29 | * Create a new Import Instance 30 | * 31 | * @param {Hecate} api parent hecate instance 32 | */ 33 | constructor(api) { 34 | this.api = api; 35 | } 36 | 37 | /** 38 | * Print help documentation about the subcommand to stderr 39 | */ 40 | help() { 41 | console.error(); 42 | console.error('Upload Create/Modified/Deleted Data to the server'); 43 | console.error(); 44 | console.error('usage: cli.js import '); 45 | console.error(); 46 | console.error(''); 47 | console.error(' multi Import multiple features'); 48 | console.error(' [--ignore-rhr] Ignore Right Hand Rule Errors'); 49 | console.error(' [--ignore-dup] Disable duplicate ID check'); 50 | console.error(' [--dryrun] Perform pre-import validation checks'); 51 | console.error(' but do not perform import'); 52 | console.error(); 53 | } 54 | 55 | /** 56 | * Given a Stream of line-delimited features or an Array of features, validate and 57 | * import them 58 | * 59 | * @param {Object} options options object 60 | * @param {string} options.message Human readable description of changes 61 | * @param {Stream|string} options.input Stream of line-delimited geojson features to import or string of the file path to import 62 | * @param {boolean} options.ignoreRHR Ignore RHR winding errors 63 | * @param {boolean} options.ignoreDup Don't check duplicate IDs (will usually cause an import failure if they exist) 64 | * @param {boolean} options.dryrun Perform all validation but don't import 65 | * @param {function} cb (err, res) style callback 66 | * 67 | * @returns {function} (err, res) style callback 68 | */ 69 | multi(options = {}, cb) { 70 | const self = this; 71 | 72 | if (!options) options = {}; 73 | 74 | if (options.script) { 75 | if (options.input) { 76 | options.input = fs.createReadStream(path.resolve(options.input)); 77 | } else { 78 | options.input = process.stdin; 79 | } 80 | 81 | cb = cli; 82 | return main(); 83 | } else if (options.cli) { 84 | cb = cli; 85 | 86 | prompt.message = '$'; 87 | prompt.start({ 88 | stdout: process.stderr 89 | }); 90 | 91 | let args = [{ 92 | name: 'message', 93 | message: 'Description of changes in upload', 94 | required: true, 95 | type: 'string', 96 | default: options.message 97 | }, { 98 | name: 'input', 99 | message: 'File to upload (empty for stdin)', 100 | required: true, 101 | type: 'string', 102 | default: options.input 103 | }]; 104 | 105 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.feature.create !== 'public') { 106 | args = args.concat(auth(self.api.user)); 107 | } 108 | 109 | prompt.get(args, (err, argv) => { 110 | prompt.stop(); 111 | 112 | if (argv.hecate_username) { 113 | self.api.user = { 114 | username: argv.hecate_username, 115 | password: argv.hecate_password 116 | }; 117 | } 118 | 119 | options.message = argv.message; 120 | options.ignoreRHR = !!options['ignore-rhr']; 121 | options.ignoreDup = !!options['ignore-dup']; 122 | options.dryrun = !!options.dryrun; 123 | 124 | if (argv.input) { 125 | options.input = fs.createReadStream(path.resolve(argv.input)); 126 | } else { 127 | options.input = process.stdin; 128 | } 129 | 130 | return main(); 131 | }); 132 | } else { 133 | if (typeof options.input === 'string') { 134 | options.input = fs.createReadStream(path.resolve(options.input)); 135 | } 136 | 137 | return main(); 138 | } 139 | 140 | /** 141 | * Once the options object is populated, make the API request 142 | * @private 143 | * 144 | * @returns {undefined} 145 | */ 146 | function main() { 147 | if (!options.input) return cb(new Error('options.input required')); 148 | if (!options.message) return cb(new Error('options.message required')); 149 | 150 | let total = 0; // Total uploaded line delimited features - output in case of fatal error so the user can resume 151 | let rl; 152 | 153 | // This call is optional and won't terminate the upload if it fails, 154 | // It simply attempts to determine the max upload size constraints of the server 155 | self.api._.server.get({}, (err, server) => { 156 | if (err || !server || !server.constraints || !server.constraints.request || !server.constraints.request.max_size) { 157 | options.max = DEFAULT_UPLOAD; 158 | } else { 159 | options.max = server.constraints.request.max_size / 2; 160 | } 161 | 162 | // validate the geojson file before uploading starts 163 | self.api._.schema.get({}, (err, schema) => { 164 | if (err) return cb(err); 165 | 166 | if (schema) { 167 | console.error('ok - using fetched JSON Schema'); 168 | } else { 169 | console.error('warn - no JSON Schema fetched'); 170 | } 171 | 172 | const tmp = path.resolve(os.tmpdir(), `import.${Math.random().toString(36).substring(7)}.geojson`); 173 | 174 | pipeline( 175 | options.input, 176 | split(), 177 | validateGeojson({ 178 | ignoreRHR: options.ignoreRHR, 179 | schema: schema, 180 | ids: !options.ignoreDup 181 | }), 182 | // Since the library accepts a stream and does two complete passes over it, 183 | // one for validaton, and the second for import, the file is saved as a tmp 184 | // file in order to perform the second pass (import) 185 | fs.createWriteStream(tmp), 186 | (err) => { 187 | if (err) return cb(err); 188 | 189 | rl = new readLineSync(tmp); 190 | 191 | read(); 192 | } 193 | ); 194 | }); 195 | }); 196 | 197 | /** 198 | * Read features from the stream, chunking into an acceptable 199 | * import size, and passing to the import function 200 | * 201 | * @private 202 | * 203 | * @returns {undefined} 204 | */ 205 | function read() { 206 | if (options.dryrun) { 207 | console.error('ok - ok skipping import (dryrun)'); 208 | return cb(null, true); 209 | } 210 | 211 | let bytes = 0; 212 | const buffer = []; 213 | let line = true; 214 | 215 | while (line) { 216 | line = rl.next(); 217 | 218 | if (!line) break; 219 | line = String(line); 220 | if (!line.trim()) continue; 221 | 222 | bytes += Buffer.from(line).length; 223 | buffer.push(rewind(JSON.parse(line))); 224 | 225 | if (bytes > options.max) break; 226 | } 227 | 228 | if (buffer.length === 0) return cb(null, true); 229 | 230 | upload({ 231 | type: 'FeatureCollection', 232 | message: options.message, 233 | features: buffer 234 | }, () => { 235 | total += buffer.length; 236 | 237 | read(); 238 | }); 239 | } 240 | 241 | /** 242 | * Once validated, perform an import of the given features 243 | * 244 | * @private 245 | * 246 | * @param {Object} body GeoJSON Feature collection to upload 247 | * @param {function} upload_cb (err, res) style callback 248 | */ 249 | function upload(body, upload_cb) { 250 | console.error(`ok - beginning upload of ${body.features.length} features`); 251 | 252 | body.features = body.features.map((feat) => { 253 | // Ensure previously downloaded data can't be accidently uploaded 254 | // but do allow generic GeoJSON to be uploaded automatically 255 | if (!feat.id && !feat.version && !feat.action) feat.action = 'create'; 256 | return feat; 257 | }); 258 | 259 | request({ 260 | method: 'POST', 261 | url: new URL('/api/data/features', self.api.url), 262 | headers: { 263 | 'User-Agent': `Hecate-Internal v${config.version}`, 264 | 'Content-Type': 'application/json' 265 | }, 266 | auth: self.api.user, 267 | body: JSON.stringify(body) 268 | }, (err, res) => { 269 | if (err) { 270 | console.error(`Upload Error: Uploaded to Line ${total}`); 271 | return cb(err); 272 | } 273 | 274 | if (res.statusCode !== 200) { 275 | console.error(`${res.body}`); 276 | console.error(`Upload Error: Uploaded to Line ${total}`); 277 | return cb(new Error(JSON.stringify(res.body))); 278 | } 279 | return upload_cb(); 280 | }); 281 | } 282 | } 283 | 284 | /** 285 | * If in CLI mode, write results to stdout 286 | * or throw any errors incurred 287 | * 288 | * @private 289 | * 290 | * @param {Error} err [optional] API Error 291 | * 292 | * @returns {undefined} 293 | */ 294 | function cli(err) { 295 | if (err) throw err; 296 | 297 | console.log('ok - import complete'); 298 | } 299 | } 300 | } 301 | 302 | 303 | module.exports = Import; 304 | -------------------------------------------------------------------------------- /lib/revert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const prompt = require('prompt'); 3 | const auth = require('../util/get_auth'); 4 | const revert = require('../util/revert'); 5 | 6 | /** 7 | * @class Revert 8 | * @public 9 | */ 10 | class Revert { 11 | /** 12 | * Create a new Revert Instance 13 | * 14 | * @param {Hecate} api parent hecate instance 15 | */ 16 | constructor(api) { 17 | this.api = api; 18 | } 19 | 20 | /** 21 | * Print help documentation about the subcommand to stderr 22 | */ 23 | help() { 24 | console.error(); 25 | console.error('Revert data of an specified delta'); 26 | console.error(); 27 | console.error('Usage: cli.js revert '); 28 | console.error(); 29 | console.error(''); 30 | console.error(' deltas Revert a given set of deltas'); 31 | console.error(' [--start] Delta ID to revert from (inclusive)'); 32 | console.error(' [--end] Delta ID to revert to (inclusive)'); 33 | console.error(); 34 | } 35 | 36 | /** 37 | * Revert a given set of deltas 38 | * 39 | * @param {!Object} options Options for making reversion of a set of deltas 40 | * @param {number} options.start Inclusive start delta ID to revert 41 | * @param {number} options.end Inclusive end delta ID to revert 42 | * @param {Stream} [options.output] Stream to write line-delimited GeoJSON to 43 | * 44 | * @param {function} cb (err, res) style callback function 45 | * 46 | * @returns {function} (err, res) style callback 47 | */ 48 | deltas(options = {}, cb) { 49 | const self = this; 50 | 51 | if (!options) options = {}; 52 | 53 | if (options.script) { 54 | cb = cli; 55 | return main(options); 56 | } else if (options.cli) { 57 | cb = cli; 58 | 59 | prompt.message = '$'; 60 | prompt.start({ 61 | stdout: process.stderr 62 | }); 63 | 64 | let args = [{ 65 | name: 'start', 66 | message: 'inclusive delta ID to start reversion from', 67 | required: true, 68 | type: 'number', 69 | default: options.start 70 | }, { 71 | name: 'end', 72 | message: 'inclusive delta ID to end reversion at', 73 | required: false, 74 | type: 'number', 75 | default: options.end 76 | }]; 77 | 78 | if ( 79 | ( 80 | !self.api.user 81 | && self.api.auth_rules 82 | ) && ( 83 | self.api.auth_rules.feature.get !== 'public' 84 | || self.api.auth_rules.feature.history !== 'public' 85 | || self.api.auth_rules.delta.get !== 'public' 86 | || self.api.auth_rules.delta.list !== 'public' 87 | ) 88 | ) { 89 | args = args.concat(auth(self.api.user)); 90 | } 91 | 92 | prompt.get(args, (err, argv) => { 93 | if (argv.hecate_username) { 94 | self.api.user = { 95 | username: argv.hecate_username, 96 | password: argv.hecate_password 97 | }; 98 | } 99 | 100 | options.start = argv.start; 101 | options.end = argv.end; 102 | options.output = process.stdout; 103 | 104 | main(options); 105 | }); 106 | } else { 107 | return main(options); 108 | } 109 | 110 | /** 111 | * Once the options object is populated, make the API request 112 | * @private 113 | * 114 | * @param {Object} options option object 115 | * @param {number} options.start Inclusive start delta ID to revert 116 | * @param {number} options.end Inclusive end delta ID to revert 117 | * @param {Stream} [options.output] Stream to write line-delimited GeoJSON to 118 | * 119 | * @returns {undefined} 120 | */ 121 | async function main(options) { 122 | if (!options.start) return cb(new Error('start delta is required')); 123 | if (!options.end) return cb(new Error('start delta is required')); 124 | if (options.start > options.end) return cb(new Error('start delta must be less than end delta')); 125 | if (!options.output) return cb(new Error('output stream is required')); 126 | 127 | try { 128 | const db = await revert.cache(options, self.api); 129 | revert.iterate(db, options.output); 130 | 131 | revert.cleanCache(db); 132 | } catch (err) { 133 | return cb(err); 134 | } 135 | 136 | options.output.end(); 137 | 138 | return cb(); 139 | } 140 | 141 | /** 142 | * If in CLI mode, write results to stdout 143 | * or throw any errors incurred 144 | * 145 | * @private 146 | * 147 | * @param {Error} err [optional] API Error 148 | * 149 | * @returns {undefined} 150 | */ 151 | function cli(err) { 152 | if (err) throw err; 153 | 154 | console.log('Reversion Generated'); 155 | } 156 | } 157 | } 158 | 159 | module.exports = Revert; 160 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class Schema 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/hecate#schema|Hecate Documentation} 14 | */ 15 | class Schema { 16 | /** 17 | * Create a new Schema Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('Fetch Schema (if any) that the server uses to validate features'); 31 | console.error(); 32 | console.error('Usage: cli.js schema '); 33 | console.error(); 34 | console.error(''); 35 | console.error(' get Get the server schema'); 36 | console.error(); 37 | } 38 | 39 | /** 40 | * Retrieve a JSON schema that feature properties must conform to 41 | * 42 | * @param {Object} options options object 43 | * @param {function} cb (err, res) style callback 44 | * 45 | * @returns {function} (err, res) style callback 46 | */ 47 | get(options = {}, cb) { 48 | const self = this; 49 | if (!options) options = {}; 50 | 51 | if (options.script) { 52 | cb = cli; 53 | return main(); 54 | } else if (options.cli) { 55 | cb = cli; 56 | 57 | prompt.message = '$'; 58 | prompt.start({ 59 | stdout: process.stderr 60 | }); 61 | 62 | let args = []; 63 | 64 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.schema.get !== 'public') { 65 | args = args.concat(auth(self.api.user)); 66 | } 67 | 68 | prompt.get(args, (err, argv) => { 69 | prompt.stop(); 70 | 71 | if (argv.hecate_username) { 72 | self.api.user = { 73 | username: argv.hecate_username, 74 | password: argv.hecate_password 75 | }; 76 | } 77 | 78 | return main(); 79 | }); 80 | } else { 81 | return main(); 82 | } 83 | 84 | /** 85 | * Once the options object is populated, make the API request 86 | * @private 87 | * 88 | * @returns {undefined} 89 | */ 90 | function main() { 91 | request.get({ 92 | json: true, 93 | url: new URL('/api/schema', self.api.url), 94 | auth: self.api.user 95 | }, (err, res) => { 96 | if (err) return cb(err); 97 | 98 | // No schema validation on server 99 | if (res.statusCode === 404) return cb(); 100 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 101 | 102 | return cb(null, res.body); 103 | }); 104 | } 105 | 106 | /** 107 | * If in CLI mode, write results to stdout 108 | * or throw any errors incurred 109 | * 110 | * @private 111 | * 112 | * @param {Error} err [optional] API Error 113 | * @param {Object} schema JSON Schema 114 | * 115 | * @returns {undefined} 116 | */ 117 | function cli(err, schema) { 118 | if (err) throw err; 119 | 120 | if (schema) { 121 | console.log(JSON.stringify(schema, null, 4)); 122 | } else { 123 | console.error('No Schema Enforcement'); 124 | } 125 | } 126 | } 127 | } 128 | 129 | module.exports = Schema; 130 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class Server 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/hecate#meta|Hecate Documentation} 14 | */ 15 | class Server { 16 | /** 17 | * Create a new Server Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('Fetch a metadata about the server'); 31 | console.error(); 32 | console.error('Usage: cli.js server '); 33 | console.error(); 34 | console.error(':'); 35 | console.error(' get Get server meta'); 36 | console.error(' stats Get geo stats from server'); 37 | console.error(); 38 | } 39 | 40 | /** 41 | * Get server metadata 42 | * 43 | * @param {!Object} options Options for making a request to meta API 44 | * 45 | * @param {function} cb (err, res) style callback function 46 | * 47 | * @returns {function} (err, res) style callback 48 | */ 49 | get(options = {}, cb) { 50 | const self = this; 51 | 52 | if (!options) options = {}; 53 | 54 | if (options.script) { 55 | cb = cli; 56 | return main(); 57 | } else if (options.cli) { 58 | cb = cli; 59 | 60 | prompt.message = '$'; 61 | prompt.start({ 62 | stdout: process.stderr 63 | }); 64 | 65 | let args = []; 66 | 67 | if (self.api.auth_rules && self.api.auth_rules.server !== 'public') { 68 | args = args.concat(auth(self.api.user)); 69 | } 70 | 71 | prompt.get(args, (err, argv) => { 72 | prompt.stop(); 73 | 74 | if (argv.hecate_username) { 75 | self.api.user = { 76 | username: argv.hecate_username, 77 | password: argv.hecate_password 78 | }; 79 | } 80 | 81 | return main(); 82 | }); 83 | } else { 84 | return main(); 85 | } 86 | 87 | /** 88 | * Once the options object is populated, make the API request 89 | * @private 90 | * 91 | * @returns {undefined} 92 | */ 93 | function main() { 94 | request({ 95 | json: true, 96 | method: 'GET', 97 | url: new URL('/api', self.api.url), 98 | auth: self.api.user 99 | }, (err, res) => { 100 | if (err) return cb(err); 101 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 102 | 103 | return cb(null, res.body); 104 | }); 105 | } 106 | 107 | /** 108 | * If in CLI mode, write results to stdout 109 | * or throw any errors incurred 110 | * 111 | * @private 112 | * 113 | * @param {Error} err [optional] API Error 114 | * @param {Object} server server metadata 115 | * 116 | * @returns {undefined} 117 | */ 118 | function cli(err, server) { 119 | if (err) throw err; 120 | 121 | console.log(JSON.stringify(server, null, 4)); 122 | } 123 | } 124 | 125 | /** 126 | * Get server stats 127 | * 128 | * @param {!Object} options Options for making a request to meta API 129 | * 130 | * @param {function} cb (err, res) style callback function 131 | * 132 | * @returns {function} (err, res) style callback 133 | */ 134 | stats(options = {}, cb) { 135 | const self = this; 136 | 137 | if (!options) options = {}; 138 | 139 | if (options.script) { 140 | cb = cli; 141 | return main(); 142 | } else if (options.cli) { 143 | cb = cli; 144 | 145 | prompt.message = '$'; 146 | prompt.start({ 147 | stdout: process.stderr 148 | }); 149 | 150 | let args = []; 151 | 152 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.stats.get !== 'public') { 153 | args = args.concat(auth(self.api.user)); 154 | } 155 | 156 | prompt.get(args, (err, argv) => { 157 | prompt.stop(); 158 | 159 | if (argv.hecate_username) { 160 | self.api.user = { 161 | username: argv.hecate_username, 162 | password: argv.hecate_password 163 | }; 164 | } 165 | 166 | return main(); 167 | }); 168 | } else { 169 | return main(); 170 | } 171 | 172 | /** 173 | * Once the options object is populated, make the API request 174 | * @private 175 | * 176 | * @returns {undefined} 177 | */ 178 | function main() { 179 | request({ 180 | json: true, 181 | method: 'GET', 182 | url: new URL('/api/data/stats', self.api.url), 183 | auth: self.api.user 184 | }, (err, res) => { 185 | if (err) return cb(err); 186 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 187 | 188 | return cb(null, res.body); 189 | }); 190 | } 191 | 192 | /** 193 | * If in CLI mode, write results to stdout 194 | * or throw any errors incurred 195 | * 196 | * @private 197 | * 198 | * @param {Error} err [optional] API Error 199 | * @param {Object} stats Server data stats 200 | * 201 | * @returns {undefined} 202 | */ 203 | function cli(err, stats) { 204 | if (err) throw err; 205 | 206 | console.log(JSON.stringify(stats, null, 4)); 207 | } 208 | } 209 | } 210 | 211 | module.exports = Server; 212 | -------------------------------------------------------------------------------- /lib/tiles.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class Tiles 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/hecate#vector-tiles|Hecate Documentation} 14 | */ 15 | class Tiles { 16 | /** 17 | * Create a new Tiles Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('Fetch a Mapbox Vector Tile for the given zxy'); 31 | console.error(); 32 | console.error('usage: cli.js tiles '); 33 | console.error(); 34 | console.error(''); 35 | console.error(' get Get a specific mvt'); 36 | } 37 | 38 | /** 39 | * Fetch a Mapbox Vector Tile for the given zxy 40 | * 41 | * @param {!Object} options Options for making a request to the deltas endpoint 42 | * @param {String} [options.zxy] z/x/y coordinate to request 43 | * 44 | * @param {function} cb (err, res) style callback function 45 | * 46 | * @returns {function} (err, res) style callback 47 | */ 48 | get(options = {}, cb) { 49 | const self = this; 50 | 51 | if (!options) options = {}; 52 | 53 | if (options.script) { 54 | cb = cli; 55 | return main(); 56 | } else if (options.cli) { 57 | cb = cli; 58 | 59 | prompt.message = '$'; 60 | prompt.start({ 61 | stdout: process.stderr 62 | }); 63 | 64 | let args = [{ 65 | name: 'z/x/y', 66 | message: 'Z/X/Y of tile to retrieve', 67 | required: true, 68 | type: 'string', 69 | default: options.zxy 70 | }]; 71 | 72 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.mvt.get !== 'public') { 73 | args = args.concat(auth(self.api.user)); 74 | } 75 | 76 | prompt.get(args, (err, argv) => { 77 | if (err) throw err; 78 | 79 | prompt.stop(); 80 | 81 | if (argv.hecate_username) { 82 | self.api.user = { 83 | username: argv.hecate_username, 84 | password: argv.hecate_password 85 | }; 86 | } 87 | 88 | options.limit = argv.limit; 89 | 90 | return main(); 91 | }); 92 | } else { 93 | return main(); 94 | } 95 | 96 | /** 97 | * Once the options object is populated, make the API request 98 | * @private 99 | * 100 | * @returns {undefined} 101 | */ 102 | function main() { 103 | if (!options.zxy) return cb(new Error('options.zxy required')); 104 | 105 | request({ 106 | json: true, 107 | method: 'GET', 108 | url: new URL(`/api/tiles/${options.zxy}`, self.api.url), 109 | auth: self.api.user 110 | }, (err, res) => { 111 | if (err) return cb(err); 112 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 113 | 114 | return cb(err, res.body); 115 | }); 116 | } 117 | 118 | /** 119 | * If in CLI mode, write results to stdout 120 | * or throw any errors incurred 121 | * 122 | * @private 123 | * 124 | * @param {Error} err [optional] API Error 125 | * 126 | * @returns {undefined} 127 | */ 128 | function cli(err) { 129 | if (err) throw err; 130 | 131 | console.error('not ok - cannot output to console'); 132 | } 133 | } 134 | } 135 | 136 | module.exports = Tiles; 137 | -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class User 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/hecate#user-options|Hecate Documentation} 14 | */ 15 | class User { 16 | /** 17 | * Create a new User Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('Manage user information'); 31 | console.error(); 32 | console.error('usage: cli.js user '); 33 | console.error(); 34 | console.error(''); 35 | console.error(' register Register a new user account'); 36 | console.error(' info Get info about your own account'); 37 | console.error(' list List users wth an optional filter prefix'); 38 | console.error(); 39 | } 40 | 41 | /** 42 | * List users with optional filtering 43 | * 44 | * @param {Object} options Options object 45 | * @param {string} options.filter User prefix to filter by 46 | * @param {function} cb (err, res) style callback 47 | * 48 | * @returns {function} (err, res) style callback 49 | */ 50 | list(options = {}, cb) { 51 | const self = this; 52 | 53 | if (!options) options = {}; 54 | 55 | if (options.script) { 56 | cb = cli; 57 | return main(); 58 | } else if (options.cli) { 59 | cb = cli; 60 | 61 | prompt.message = '$'; 62 | prompt.start({ 63 | stdout: process.stderr 64 | }); 65 | 66 | let args = [{ 67 | name: 'filter', 68 | message: 'Optional filter prefix', 69 | type: 'string', 70 | required: false, 71 | default: options.filter 72 | }]; 73 | 74 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.user.list !== 'public') { 75 | args = args.concat(auth(self.api.user)); 76 | } 77 | 78 | prompt.get(args, (err, argv) => { 79 | prompt.stop(); 80 | 81 | if (argv.hecate_username) { 82 | self.api.user = { 83 | username: argv.hecate_username, 84 | password: argv.hecate_password 85 | }; 86 | } 87 | 88 | options.filter = argv.filter; 89 | 90 | return main(); 91 | }); 92 | } else { 93 | return main(); 94 | } 95 | 96 | /** 97 | * Once the options object is populated, make the API request 98 | * @private 99 | * 100 | * @returns {undefined} 101 | */ 102 | function main() { 103 | options.filter = options.filter ? encodeURIComponent(options.filter) : ''; 104 | 105 | request.get({ 106 | json: true, 107 | url: new URL(`/api/users?filter=${options.filter}`, self.api.url), 108 | auth: self.api.user 109 | }, (err, res) => { 110 | if (err) return cb(err); 111 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 112 | 113 | return cb(null, res.body); 114 | }); 115 | } 116 | 117 | /** 118 | * If in CLI mode, write results to stdout 119 | * or throw any errors incurred 120 | * 121 | * @private 122 | * 123 | * @param {Error} err [optional] API Error 124 | * @param {Object} users list of users 125 | * 126 | * @returns {undefined} 127 | */ 128 | function cli(err, users) { 129 | if (err) throw err; 130 | 131 | console.log(JSON.stringify(users, null, 4)); 132 | } 133 | } 134 | 135 | /** 136 | * Retrieve metadata about the user that makes the request 137 | * 138 | * @param {Object} options options object 139 | * @param {function} cb (err, res) style callback 140 | * 141 | * @returns {function} (err, res) style callback 142 | */ 143 | info(options = {}, cb) { 144 | const self = this; 145 | 146 | if (!options) options = {}; 147 | 148 | if (options.script) { 149 | cb = cli; 150 | return main(); 151 | } else if (options.cli) { 152 | cb = cli; 153 | 154 | prompt.message = '$'; 155 | prompt.start({ 156 | stdout: process.stderr 157 | }); 158 | 159 | let args = []; 160 | 161 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.user.info !== 'public') { 162 | args = args.concat(auth(self.api.user)); 163 | } 164 | 165 | prompt.get(args, (err, argv) => { 166 | prompt.stop(); 167 | 168 | if (argv.hecate_username) { 169 | self.api.user = { 170 | username: argv.hecate_username, 171 | password: argv.hecate_password 172 | }; 173 | } 174 | 175 | return main(); 176 | }); 177 | } else { 178 | return main(); 179 | } 180 | 181 | /** 182 | * Once the options object is populated, make the API request 183 | * @private 184 | * 185 | * @returns {undefined} 186 | */ 187 | function main() { 188 | request.get({ 189 | json: true, 190 | url: new URL('/api/user/info', self.api.url), 191 | auth: self.api.user 192 | }, (err, res) => { 193 | if (err) return cb(err); 194 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 195 | 196 | return cb(null, res.body); 197 | }); 198 | } 199 | 200 | /** 201 | * If in CLI mode, write results to stdout 202 | * or throw any errors incurred 203 | * 204 | * @private 205 | * 206 | * @param {Error} err [optional] API Error 207 | * @param {Object} info user info 208 | * 209 | * @returns {undefined} 210 | */ 211 | function cli(err, info) { 212 | if (err) throw err; 213 | 214 | console.log(JSON.stringify(info, null, 4)); 215 | } 216 | } 217 | 218 | /** 219 | * Register a new user account 220 | * 221 | * @param {Object} options options object 222 | * @param {string} options.username Username to register 223 | * @param {string} options.email Email of account to register 224 | * @param {string} options.password Password of account to register 225 | * @param {function} cb (err, res) style callback 226 | * 227 | * @returns {function} (err, res) style callback 228 | */ 229 | register(options = {}, cb) { 230 | const self = this; 231 | 232 | if (!options) options = {}; 233 | 234 | if (options.script) { 235 | cb = cli; 236 | return main(); 237 | } else if (options.cli) { 238 | cb = cli; 239 | 240 | prompt.message = '$'; 241 | prompt.start({ 242 | stdout: process.stderr 243 | }); 244 | 245 | let args = [{ 246 | name: 'username', 247 | message: 'Your Slack/Github Username', 248 | type: 'string', 249 | required: true, 250 | default: options.username 251 | }, { 252 | name: 'email', 253 | message: 'Your email address', 254 | type: 'string', 255 | required: true, 256 | default: options.email 257 | }, { 258 | name: 'password', 259 | message: 'secure password to be used at login', 260 | hidden: true, 261 | replace: '*', 262 | required: true, 263 | type: 'string' 264 | }]; 265 | 266 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.user.create !== 'public') { 267 | args = args.concat(auth(self.api.user)); 268 | } 269 | 270 | prompt.get(args, (err, argv) => { 271 | prompt.stop(); 272 | 273 | if (argv.hecate_username) { 274 | self.api.user = { 275 | username: argv.hecate_username, 276 | password: argv.hecate_password 277 | }; 278 | } 279 | 280 | options.username = argv.username; 281 | options.email = argv.email; 282 | options.password = argv.password; 283 | 284 | return main(); 285 | }); 286 | } else { 287 | return main(); 288 | } 289 | 290 | /** 291 | * Once the options object is populated, make the API request 292 | * @private 293 | * 294 | * @returns {undefined} 295 | */ 296 | function main() { 297 | if (!options.username) return cb(new Error('options.username required')); 298 | if (!options.password) return cb(new Error('options.password required')); 299 | if (!options.email) return cb(new Error('options.email required')); 300 | 301 | options.username = encodeURIComponent(options.username); 302 | options.password = encodeURIComponent(options.password); 303 | options.email = encodeURIComponent(options.email); 304 | 305 | request.get({ 306 | url: new URL(`/api/user/create?username=${options.username}&password=${options.password}&email=${options.email}`, self.api.url), 307 | auth: self.api.user 308 | }, (err, res) => { 309 | if (err) return cb(err); 310 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 311 | 312 | return cb(null, true); 313 | }); 314 | } 315 | 316 | /** 317 | * If in CLI mode, write results to stdout 318 | * or throw any errors incurred 319 | * 320 | * @private 321 | * 322 | * @param {Error} err [optional] API Error 323 | * 324 | * @returns {undefined} 325 | */ 326 | function cli(err) { 327 | if (err) throw err; 328 | 329 | console.log('ok - user registered'); 330 | } 331 | } 332 | } 333 | 334 | module.exports = User; 335 | -------------------------------------------------------------------------------- /lib/webhooks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const request = require('request'); 6 | const prompt = require('prompt'); 7 | const auth = require('../util/get_auth'); 8 | 9 | /** 10 | * @class Webhooks 11 | * @public 12 | * 13 | * @see {@link https://github.com/mapbox/hecate#webhooks|Hecate Documentation} 14 | */ 15 | class Webhooks { 16 | /** 17 | * Create a new Webhooks Instance 18 | * 19 | * @param {Hecate} api parent hecate instance 20 | */ 21 | constructor(api) { 22 | this.api = api; 23 | } 24 | 25 | /** 26 | * Print help documentation about the subcommand to stderr 27 | */ 28 | help() { 29 | console.error(); 30 | console.error('List, Create, Manage & Delete hecate webhooks'); 31 | console.error(); 32 | console.error('Usage: cli.js webhooks '); 33 | console.error(); 34 | console.error(':'); 35 | console.error(' list List webhooks currently active on the server'); 36 | console.error(' get Get a specific webhook'); 37 | console.error(' create Create a new webhook'); 38 | console.error(' update Update an existing webhook'); 39 | console.error(' delete Delete an existing webhook'); 40 | console.error(); 41 | } 42 | 43 | /** 44 | * Queries hecate /api/webhooks endpoint 45 | * 46 | * @param {!Object} options Options for making a request to the hecate /api/webhooks endpoint 47 | * 48 | * @param {function} cb (err, res) style callback function 49 | * 50 | * @returns {function} (err, res) style callback 51 | */ 52 | list(options = {}, cb) { 53 | const self = this; 54 | 55 | if (!options) options = {}; 56 | 57 | if (options.script) { 58 | cb = cli; 59 | 60 | options.output = process.stdout; 61 | 62 | return main(); 63 | } else if (options.cli) { 64 | cb = cli; 65 | 66 | prompt.message = '$'; 67 | prompt.start({ 68 | stdout: process.stderr 69 | }); 70 | 71 | let args = []; 72 | 73 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.webhooks.get !== 'public') { 74 | args = args.concat(auth(self.api.user)); 75 | } 76 | 77 | prompt.get(args, (err, argv) => { 78 | prompt.stop(); 79 | 80 | if (argv.hecate_username) { 81 | self.api.user = { 82 | username: argv.hecate_username, 83 | password: argv.hecate_password 84 | }; 85 | } 86 | 87 | return main(); 88 | }); 89 | } else { 90 | return main(); 91 | } 92 | 93 | /** 94 | * Once the options object is populated, make the API request 95 | * @private 96 | * 97 | * @returns {undefined} 98 | */ 99 | function main() { 100 | request.get({ 101 | json: true, 102 | url: new URL('/api/webhooks', self.api.url), 103 | auth: self.api.user 104 | }, (err, res) => { 105 | if (err) return cb(err); 106 | if (res.statusCode !== 200) return cb(new Error(JSON.stringify(res.body))); 107 | 108 | return cb(null, res.body); 109 | }); 110 | } 111 | 112 | /** 113 | * If in CLI mode, write results to stdout 114 | * or throw any errors incurred 115 | * 116 | * @private 117 | * 118 | * @param {Error} err [optional] API Error 119 | * @param {Object} hooks list of webhooks 120 | * 121 | * @returns {undefined} 122 | */ 123 | function cli(err, hooks) { 124 | if (err) throw err; 125 | 126 | console.log(JSON.stringify(hooks)); 127 | } 128 | } 129 | 130 | /** 131 | * Get a specific webhook given the ID 132 | * 133 | * @param {!Object} options Options for making a request to the hecate /api/webhooks endpoint 134 | * @param {number} options.id ID of the webhook to retreive 135 | * 136 | * @param {function} cb (err, res) style callback function 137 | * 138 | * @returns {function} (err, res) style callback function 139 | */ 140 | get(options = {}, cb) { 141 | const self = this; 142 | 143 | if (!options) options = {}; 144 | 145 | if (options.script) { 146 | cb = cli; 147 | 148 | options.output = process.stdout; 149 | 150 | return main(); 151 | } else if (options.cli) { 152 | cb = cli; 153 | 154 | prompt.message = '$'; 155 | prompt.start({ 156 | stdout: process.stderr 157 | }); 158 | 159 | let args = [{ 160 | name: 'id', 161 | message: 'Webhook ID', 162 | type: 'string', 163 | required: 'true', 164 | default: options.id 165 | }]; 166 | 167 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.webhooks.get !== 'public') { 168 | args = args.concat(auth(self.api.user)); 169 | } 170 | 171 | prompt.get(args, (err, argv) => { 172 | prompt.stop(); 173 | 174 | if (argv.hecate_username) { 175 | self.api.user = { 176 | username: argv.hecate_username, 177 | password: argv.hecate_password 178 | }; 179 | } 180 | 181 | options.id = argv.id; 182 | 183 | return main(); 184 | }); 185 | } else { 186 | return main(); 187 | } 188 | 189 | /** 190 | * Once the options object is populated, make the API request 191 | * @private 192 | * 193 | * @returns {undefined} 194 | */ 195 | function main() { 196 | if (!options.id) return cb(new Error('options.id required')); 197 | 198 | request({ 199 | method: 'GET', 200 | url: new URL(`/api/webhooks/${options.id}`, self.api.url), 201 | auth: self.api.user 202 | }, (err, res) => { 203 | if (err) return cb(err); 204 | if (res.statusCode !== 200) return cb(new Error(res.body)); 205 | 206 | return cb(null, JSON.parse(res.body)); 207 | }); 208 | } 209 | 210 | /** 211 | * If in CLI mode, write results to stdout 212 | * or throw any errors incurred 213 | * 214 | * @private 215 | * 216 | * @param {Error} err [optional] API Error 217 | * @param {Object} hook single webhook 218 | * 219 | * @returns {undefined} 220 | */ 221 | function cli(err, hook) { 222 | if (err) throw err; 223 | 224 | console.log(JSON.stringify(hook)); 225 | } 226 | } 227 | 228 | /** 229 | * Delete a specific webhook given the ID 230 | * 231 | * @param {!Object} options Options for making a request to the hecate /api/webhooks endpoint 232 | * @param {number} options.id ID of the webhook to delete 233 | * 234 | * @param {function} cb (err, res) style callback function 235 | * 236 | * @returns {function} (err, res) style callback function 237 | */ 238 | delete(options = {}, cb) { 239 | const self = this; 240 | 241 | if (!options) options = {}; 242 | 243 | if (options.script) { 244 | cb = cli; 245 | 246 | options.output = process.stdout; 247 | 248 | return main(); 249 | } else if (options.cli) { 250 | cb = cli; 251 | 252 | prompt.message = '$'; 253 | prompt.start({ 254 | stdout: process.stderr 255 | }); 256 | 257 | let args = [{ 258 | name: 'id', 259 | message: 'Webhook ID', 260 | type: 'string', 261 | required: 'true', 262 | default: options.id 263 | }]; 264 | 265 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.webhooks.set !== 'public') { 266 | args = args.concat(auth(self.api.user)); 267 | } 268 | 269 | prompt.get(args, (err, argv) => { 270 | prompt.stop(); 271 | 272 | if (argv.hecate_username) { 273 | self.api.user = { 274 | username: argv.hecate_username, 275 | password: argv.hecate_password 276 | }; 277 | } 278 | 279 | options.id = argv.id; 280 | 281 | return main(); 282 | }); 283 | } else { 284 | return main(); 285 | } 286 | 287 | /** 288 | * Once the options object is populated, make the API request 289 | * @private 290 | * 291 | * @returns {undefined} 292 | */ 293 | function main() { 294 | if (!options.id) return cb(new Error('options.id required')); 295 | 296 | request({ 297 | method: 'DELETE', 298 | url: new URL(`/api/webhooks/${options.id}`, self.api.url), 299 | auth: self.api.user 300 | }, (err, res) => { 301 | if (err) return cb(err); 302 | if (res.statusCode !== 200) return cb(new Error(res.body)); 303 | 304 | return cb(null, true); 305 | }); 306 | } 307 | 308 | /** 309 | * If in CLI mode, write results to stdout 310 | * or throw any errors incurred 311 | * 312 | * @private 313 | * 314 | * @param {Error} err [optional] API Error 315 | * 316 | * @returns {undefined} 317 | */ 318 | function cli(err) { 319 | if (err) throw err; 320 | 321 | console.log(true); 322 | } 323 | } 324 | 325 | /** 326 | * Update a given webhook ID 327 | * 328 | * @param {!Object} options Options for making a request to the hecate /api/webhooks endpoint 329 | * @param {number} options.id ID of the webhook to update 330 | * @param {string} options.name Name of the webhook 331 | * @param {string} options.url URL of the webhook 332 | * @param {Array} options.actions server actions the webhook should be fired on 333 | * 334 | * @param {function} cb (err, res) style callback function 335 | * 336 | * @returns {function} (err, res) style callback function 337 | */ 338 | update(options = {}, cb) { 339 | const self = this; 340 | 341 | if (!options) options = {}; 342 | 343 | if (options.script) { 344 | cb = cli; 345 | 346 | options.output = process.stdout; 347 | 348 | return main(); 349 | } else if (options.cli) { 350 | cb = cli; 351 | 352 | prompt.message = '$'; 353 | prompt.start({ 354 | stdout: process.stderr 355 | }); 356 | 357 | let args = [{ 358 | name: 'id', 359 | message: 'Webhook ID', 360 | type: 'string', 361 | required: 'true', 362 | default: options.id 363 | },{ 364 | name: 'name', 365 | message: 'Webhook Name', 366 | type: 'string', 367 | required: 'true', 368 | default: options.name 369 | },{ 370 | name: 'url', 371 | message: 'Webhook URL', 372 | type: 'string', 373 | required: 'true', 374 | default: options.url 375 | },{ 376 | name: 'actions', 377 | message: 'Webhook Actions (comma separated)', 378 | type: 'string', 379 | required: 'true', 380 | default: options.actions 381 | }]; 382 | 383 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.webhooks.set !== 'public') { 384 | args = args.concat(auth(self.api.user)); 385 | } 386 | 387 | prompt.get(args, (err, argv) => { 388 | prompt.stop(); 389 | 390 | if (argv.hecate_username) { 391 | self.api.user = { 392 | username: argv.hecate_username, 393 | password: argv.hecate_password 394 | }; 395 | } 396 | 397 | options.id = argv.id; 398 | options.name = argv.name; 399 | options.url = argv.url; 400 | options.actions = argv.actions.split(','); 401 | 402 | return main(); 403 | }); 404 | } else { 405 | return main(); 406 | } 407 | 408 | /** 409 | * Once the options object is populated, make the API request 410 | * @private 411 | * 412 | * @returns {undefined} 413 | */ 414 | function main() { 415 | if (!options.id) return cb(new Error('options.id required')); 416 | if (!options.name) return cb(new Error('options.name required')); 417 | if (!options.url) return cb(new Error('options.url required')); 418 | if (!options.actions) return cb(new Error('options.actions required')); 419 | 420 | request({ 421 | method: 'POST', 422 | json: true, 423 | url: new URL(`/api/webhooks/${options.id}`, self.api.url), 424 | auth: self.api.user, 425 | body: { 426 | name: options.name, 427 | url: options.url, 428 | actions: options.actions 429 | } 430 | }, (err, res) => { 431 | if (err) return cb(err); 432 | if (res.statusCode !== 200) return cb(new Error(res.body)); 433 | 434 | return cb(null, true); 435 | }); 436 | } 437 | 438 | /** 439 | * If in CLI mode, write results to stdout 440 | * or throw any errors incurred 441 | * 442 | * @private 443 | * 444 | * @param {Error} err [optional] API Error 445 | * 446 | * @returns {undefined} 447 | */ 448 | function cli(err) { 449 | if (err) throw err; 450 | 451 | console.log(true); 452 | } 453 | } 454 | 455 | /** 456 | * Create a new webhook 457 | * 458 | * @param {!Object} options Options for making a request to the hecate /api/webhooks endpoint 459 | * @param {string} options.name Name of the webhook 460 | * @param {string} options.url URL of the webhook 461 | * @param {Array} options.actions server actions the webhook should be fired on 462 | * 463 | * @param {function} cb (err, res) style callback function 464 | * 465 | * @returns {function} (err, res) style callback function 466 | */ 467 | create(options = {}, cb) { 468 | const self = this; 469 | 470 | if (!options) options = {}; 471 | 472 | if (options.script) { 473 | cb = cli; 474 | 475 | options.output = process.stdout; 476 | 477 | return main(); 478 | } else if (options.cli) { 479 | cb = cli; 480 | 481 | prompt.message = '$'; 482 | prompt.start({ 483 | stdout: process.stderr 484 | }); 485 | 486 | let args = [{ 487 | name: 'name', 488 | message: 'Webhook Name', 489 | type: 'string', 490 | required: 'true', 491 | default: options.name 492 | },{ 493 | name: 'url', 494 | message: 'Webhook URL', 495 | type: 'string', 496 | required: 'true', 497 | default: options.url 498 | },{ 499 | name: 'actions', 500 | message: 'Webhook Actions (comma separated)', 501 | type: 'string', 502 | required: 'true', 503 | default: options.actions 504 | }]; 505 | 506 | if (!self.api.user && self.api.auth_rules && self.api.auth_rules.webhooks.set !== 'public') { 507 | args = args.concat(auth(self.api.user)); 508 | } 509 | 510 | prompt.get(args, (err, argv) => { 511 | prompt.stop(); 512 | 513 | if (argv.hecate_username) { 514 | self.api.user = { 515 | username: argv.hecate_username, 516 | password: argv.hecate_password 517 | }; 518 | } 519 | 520 | options.name = argv.name; 521 | options.url = argv.url; 522 | options.actions = argv.actions.split(','); 523 | 524 | return main(); 525 | }); 526 | } else { 527 | return main(); 528 | } 529 | 530 | /** 531 | * Once the options object is populated, make the API request 532 | * @private 533 | * 534 | * @returns {undefined} 535 | */ 536 | function main() { 537 | if (!options.name) return cb(new Error('options.name required')); 538 | if (!options.url) return cb(new Error('options.url required')); 539 | if (!options.actions) return cb(new Error('options.actions required')); 540 | 541 | request({ 542 | method: 'POST', 543 | json: true, 544 | url: new URL('/api/webhooks', self.api.url), 545 | auth: self.api.user, 546 | body: { 547 | name: options.name, 548 | url: options.url, 549 | actions: options.actions 550 | } 551 | }, (err, res) => { 552 | if (err) return cb(err); 553 | if (res.statusCode !== 200) return cb(new Error(res.body)); 554 | 555 | return cb(null, true); 556 | }); 557 | } 558 | 559 | /** 560 | * If in CLI mode, write results to stdout 561 | * or throw any errors incurred 562 | * 563 | * @private 564 | * 565 | * @param {Error} err [optional] API Error 566 | * 567 | * @returns {undefined} 568 | */ 569 | function cli(err) { 570 | if (err) throw err; 571 | 572 | console.log(true); 573 | } 574 | } 575 | } 576 | 577 | module.exports = Webhooks; 578 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/hecatejs", 3 | "version": "12.0.0", 4 | "main": "cli.js", 5 | "repository": "git@github.com:mapcommons/HecateJS", 6 | "author": "ingalls ", 7 | "license": "MIT", 8 | "bin": { 9 | "hecatecli": "cli.js" 10 | }, 11 | "devDependencies": { 12 | "@mapbox/eslint-config-geocoding": "^2.0.0", 13 | "documentation": "^12.1.4", 14 | "eslint": "^6.6.0", 15 | "eslint-plugin-node": "10.0.0", 16 | "nyc": "^14.1.1", 17 | "tape": "^4.9.0" 18 | }, 19 | "dependencies": { 20 | "@mapbox/geojsonhint": "^3.0.0", 21 | "@turf/turf": "^5.1.6", 22 | "aws-sdk": "^2.224.1", 23 | "better-sqlite3": "^5.4.3", 24 | "d3-queue": "^3.0.7", 25 | "geojson-rewind": "^0.3.1", 26 | "geojson-validation": "^0.2.0", 27 | "minimist": "^1.2.0", 28 | "n-readlines": "^1.0.0 ", 29 | "nock": "^11.7.0", 30 | "parallel-transform": "^1.2.0", 31 | "prompt": "^1.0.0", 32 | "request": "^2.88.0", 33 | "requestretry": "^4.0.0", 34 | "split": "^1.0.1" 35 | }, 36 | "engines": { 37 | "node": ">=10" 38 | }, 39 | "scripts": { 40 | "doc": "documentation build --github --format md --output docs/api.md cli.js lib/**", 41 | "test": "yarn lint && tape test/*.test.js", 42 | "coverage": "TESTING=true nyc tape 'test/**/*.js' && nyc report --reporter=text-lcov > coverage.lcov", 43 | "lint": "documentation lint cli.js lib/** && eslint cli.js lib/*.js util/*.js test/*.js" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/invalid-geojson-schema.json: -------------------------------------------------------------------------------- 1 | {"type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"number":0,"street":[{"display":"\\N","priority":0}]}} 2 | {"type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"source":"mbx","number":0}} 3 | {"type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"source":"mbx","street":[{"display":"\\N","priority":0}]}} 4 | {"type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"source":"mbx","number":0, "street":[{"display":"\\N","priority":0}], "prop1":"line"}} 5 | {"type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"source":"mbx","number":0, "street":[{"display":"\\N","priority":0}], "postcode":true}} 6 | {"type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"source":"mbx","number":0, "street":"none"}} -------------------------------------------------------------------------------- /test/fixtures/schema.json: -------------------------------------------------------------------------------- 1 | { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Test", "properties": { "prop1": { "description": "test prop1", "enum": ["one", "two"], "type": "string" }, "number": { "description": "Number of building", "type": ["string", "number"] }, "postcode": { "description": "Postcode/Zipcode", "type": ["string", "number"] }, "source": { "description": "Name of the source where the data comes from", "type": "string" }, "street": { "description": "Name of the street", "type": "array" } }, "required": ["source", "number", "street"], "title": "Address", "type": "object" } -------------------------------------------------------------------------------- /test/fixtures/valid-geojson.json: -------------------------------------------------------------------------------- 1 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"venueid":"579470f1498edc3f60a84c9f","venuename":"Heilig-Kreuz-Kirche Kreuzberg","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 2 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"venueid":"57a6e59438fa90ec40af1198","venuename":"The Multi Hops of Malka Voodoo's Tuti Cult","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 3 | {"action": "create", "type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]},"properties":{"venueid":"4f9a53b1e4b0849acedde059","venuename":"\"@,@\"\t\"\\N\"","address":"\\N","address2":"52.468637","city":"Berlin","state":"","zip":"DE","countrycode":"N/A","formatted_phone":"Shops & Services","top_level_category":"Car Washes","category_primary":"\\N","category_secondary":"\\N","category_tertiary":"0.0","rating":"\\N","tip_count":"\\N","photo_count":"low","estimated_quality":"2017-01-11","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 4 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"venueid":"579470f1498edc3f60a84c9f","venuename":"Heilig-Kreuz-Kirche Kreuzberg","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 5 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"venueid":"579470f1498edc3f60a84c9f","venuename":"Heilig-Kreuz-Kirche Kreuzberg","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 6 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"venueid":"579470f1498edc3f60a84c9f","venuename":"Heilig-Kreuz-Kirche Kreuzberg","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 7 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"venueid":"579470f1498edc3f60a84c9f","venuename":"Heilig-Kreuz-Kirche Kreuzberg","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 8 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[23.5,12.34]},"properties":{"venueid":"579470f1498edc3f60a84c9f","venuename":"Heilig-Kreuz-Kirche Kreuzberg","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 9 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"venueid":"57a6e59438fa90ec40af1198","venuename":"The Multi Hops of Malka Voodoo's Tuti Cult","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 10 | {"action": "create", "type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]},"properties":{"venueid":"4f9a53b1e4b0849acedde059","venuename":"\"@,@\"\t\"\\N\"","address":"\\N","address2":"52.468637","city":"Berlin","state":"","zip":"DE","countrycode":"N/A","formatted_phone":"Shops & Services","top_level_category":"Car Washes","category_primary":"\\N","category_secondary":"\\N","category_tertiary":"0.0","rating":"\\N","tip_count":"\\N","photo_count":"low","estimated_quality":"2017-01-11","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 11 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"venueid":"57a6e59438fa90ec40af1198","venuename":"The Multi Hops of Malka Voodoo's Tuti Cult","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 12 | {"action": "create", "type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]},"properties":{"venueid":"4f9a53b1e4b0849acedde059","venuename":"\"@,@\"\t\"\\N\"","address":"\\N","address2":"52.468637","city":"Berlin","state":"","zip":"DE","countrycode":"N/A","formatted_phone":"Shops & Services","top_level_category":"Car Washes","category_primary":"\\N","category_secondary":"\\N","category_tertiary":"0.0","rating":"\\N","tip_count":"\\N","photo_count":"low","estimated_quality":"2017-01-11","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 13 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"venueid":"57a6e59438fa90ec40af1198","venuename":"The Multi Hops of Malka Voodoo's Tuti Cult","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 14 | {"action": "create", "type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]},"properties":{"venueid":"4f9a53b1e4b0849acedde059","venuename":"\"@,@\"\t\"\\N\"","address":"\\N","address2":"52.468637","city":"Berlin","state":"","zip":"DE","countrycode":"N/A","formatted_phone":"Shops & Services","top_level_category":"Car Washes","category_primary":"\\N","category_secondary":"\\N","category_tertiary":"0.0","rating":"\\N","tip_count":"\\N","photo_count":"low","estimated_quality":"2017-01-11","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 15 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"venueid":"57a6e59438fa90ec40af1198","venuename":"The Multi Hops of Malka Voodoo's Tuti Cult","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 16 | {"action": "create", "type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]},"properties":{"venueid":"4f9a53b1e4b0849acedde059","venuename":"\"@,@\"\t\"\\N\"","address":"\\N","address2":"52.468637","city":"Berlin","state":"","zip":"DE","countrycode":"N/A","formatted_phone":"Shops & Services","top_level_category":"Car Washes","category_primary":"\\N","category_secondary":"\\N","category_tertiary":"0.0","rating":"\\N","tip_count":"\\N","photo_count":"low","estimated_quality":"2017-01-11","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 17 | {"action": "create", "type":"Feature","geometry":{"type":"Point","coordinates":[0,-1]},"properties":{"venueid":"57a6e59438fa90ec40af1198","venuename":"The Multi Hops of Malka Voodoo's Tuti Cult","address":"\\N","address2":"\\N","city":"Berlin","state":"Berlin","zip":"\\N","countrycode":"DE","formatted_phone":"N/A","top_level_category":"Arts & Entertainment","category_primary":"Music Venues","category_secondary":"\\N","category_tertiary":"\\N","rating":"0.0","tip_count":"\\N","photo_count":"\\N","estimated_quality":"low","last_explicit_activity_dt":"\\N","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 18 | {"action": "create", "type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]},"properties":{"venueid":"4f9a53b1e4b0849acedde059","venuename":"\"@,@\"\t\"\\N\"","address":"\\N","address2":"52.468637","city":"Berlin","state":"","zip":"DE","countrycode":"N/A","formatted_phone":"Shops & Services","top_level_category":"Car Washes","category_primary":"\\N","category_secondary":"\\N","category_tertiary":"0.0","rating":"\\N","tip_count":"\\N","photo_count":"low","estimated_quality":"2017-01-11","source":"foursquare","number":0,"street":[{"display":"\\N","priority":0}]}} 19 | -------------------------------------------------------------------------------- /test/fixtures/valid-geojson.schema: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "description": "Validate addresses source", 4 | "properties": { 5 | "venueid": { 6 | "description": "ID Hash of Venue", 7 | "type": "string" 8 | } 9 | }, 10 | "required": [ 11 | "venueid" 12 | ], 13 | "title": "Address source", 14 | "type": "object" 15 | } 16 | -------------------------------------------------------------------------------- /test/lib.auth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hecate = require('../cli.js'); 4 | 5 | const test = require('tape').test; 6 | const nock = require('nock'); 7 | 8 | test('lib.auth.test.js', (t) => { 9 | nock('http://localhost:7777') 10 | .get('/api/auth') 11 | .reply(200, { 12 | custom: 'auth' 13 | }); 14 | 15 | const hecate = new Hecate({ 16 | url: 'http://localhost:7777' 17 | }); 18 | 19 | hecate.auth(null, (err, res) => { 20 | t.error(err, 'no error'); 21 | 22 | t.deepEquals(res, { custom: 'auth' }, 'returns custom auth json'); 23 | t.end(); 24 | }); 25 | }); 26 | 27 | test('lib.auth.test.js - Restore Nock', (t) => { 28 | nock.cleanAll(); 29 | t.end(); 30 | }); 31 | -------------------------------------------------------------------------------- /test/lib.deltas.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hecate = require('../cli.js'); 4 | 5 | const test = require('tape').test; 6 | const nock = require('nock'); 7 | 8 | test('lib.deltas.test.js', (t) => { 9 | const hecate = new Hecate({ 10 | url: 'http://localhost:7777' 11 | }); 12 | 13 | nock('http://localhost:7777') 14 | .get('/api/deltas?limit=100') 15 | .reply(200, true); 16 | 17 | t.test('lib.deltas.test.js - default', (q) => { 18 | hecate.listDeltas({}, (err, res) => { 19 | q.error(err, 'no errors'); 20 | q.equals(res, true); 21 | q.end(); 22 | }); 23 | }); 24 | 25 | nock('http://localhost:7777') 26 | .get('/api/deltas?limit=1') 27 | .reply(200, true); 28 | 29 | t.test('lib.deltas.test.js - custom limit', (q) => { 30 | hecate.listDeltas({ 31 | limit: 1 32 | }, (err, res) => { 33 | q.error(err, 'no errors'); 34 | q.equals(res, true); 35 | q.end(); 36 | }); 37 | }); 38 | 39 | nock('http://localhost:7777') 40 | .get('/api/deltas?limit=1&offset=1') 41 | .reply(200, true); 42 | 43 | t.test('lib.deltas.test.js - custom offset', (q) => { 44 | hecate.listDeltas({ 45 | limit: 1, 46 | offset: 1 47 | }, (err, res) => { 48 | q.error(err, 'no errors'); 49 | q.equals(res, true); 50 | q.end(); 51 | }); 52 | }); 53 | 54 | t.end(); 55 | }); 56 | 57 | test('Restore Nock', (t) => { 58 | nock.cleanAll(); 59 | t.end(); 60 | }); 61 | -------------------------------------------------------------------------------- /test/lib.feature.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hecate = require('../cli.js'); 4 | 5 | const test = require('tape').test; 6 | const nock = require('nock'); 7 | 8 | test('lib.feature.test.js', (t) => { 9 | const hecate = new Hecate({ 10 | url: 'http://localhost:7777' 11 | }); 12 | 13 | nock('http://localhost:7777') 14 | .get('/api/data/feature/7/history') 15 | .reply(200, true); 16 | 17 | hecate.getFeatureHistory({ 18 | feature: 7 19 | }, (err, res) => { 20 | t.error(err); 21 | t.equal(res, true); 22 | t.end(); 23 | }); 24 | }); 25 | 26 | test('Restore Nock', (t) => { 27 | nock.cleanAll(); 28 | t.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /test/lib.import.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hecate = require('../cli.js'); 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const test = require('tape'); 8 | const nock = require('nock'); 9 | 10 | test('Basic Import Test', (t) => { 11 | const hecate = new Hecate({ 12 | url: 'http://localhost:7777' 13 | }); 14 | 15 | nock('http://localhost:7777') 16 | .get('/api').reply(200, { 17 | constraints: { 18 | request: { 19 | max_size: 20971520 20 | } 21 | }, 22 | version: "0.82.1" 23 | }) 24 | .get('/api/schema').reply(200, 25 | JSON.parse(fs.readFileSync(path.resolve(__dirname, './fixtures/valid-geojson.schema'))) 26 | ); 27 | 28 | hecate._.import.multi({ 29 | input: fs.createReadStream(path.resolve(__dirname, './fixtures/valid-geojson.json')), 30 | message: 'Test Import', 31 | dryrun: true 32 | }, (err) => { 33 | t.error(err); 34 | t.end(); 35 | }); 36 | }); 37 | 38 | test('Restore Nock', (t) => { 39 | nock.cleanAll(); 40 | t.end(); 41 | }); 42 | -------------------------------------------------------------------------------- /test/lib.register.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hecate = require('../cli.js'); 4 | 5 | const test = require('tape').test; 6 | const nock = require('nock'); 7 | 8 | test('lib.register.test.js', (t) => { 9 | nock('http://localhost:7777') 10 | .get('/api/user/create?username=ingalls&password=yeaheh&email=nick%40mapbox.com') 11 | .reply(200, true); 12 | 13 | const hecate = new Hecate({ 14 | url: 'http://localhost:7777' 15 | }); 16 | 17 | t.test('lib.register.test.js - missing username', (q) => { 18 | hecate.register(null, (err) => { 19 | q.equals(err.message, 'options.username required'); 20 | q.end(); 21 | }); 22 | }); 23 | 24 | t.test('lib.register.test.js - missing password', (q) => { 25 | hecate.register({ 26 | username: 'ingalls' 27 | }, (err) => { 28 | q.equals(err.message, 'options.password required'); 29 | q.end(); 30 | }); 31 | }); 32 | 33 | t.test('lib.register.test.js - missing email', (q) => { 34 | hecate.register({ 35 | username: 'ingalls', 36 | password: 'yeaheh' 37 | }, (err) => { 38 | q.equals(err.message, 'options.email required'); 39 | q.end(); 40 | }); 41 | }); 42 | 43 | t.test('lib.register.test.js - missing email', (q) => { 44 | hecate.register({ 45 | username: 'ingalls', 46 | password: 'yeaheh', 47 | email: 'nick@mapbox.com' 48 | }, (err, res) => { 49 | q.error(err, 'no errors'); 50 | q.equals(res, true); 51 | q.end(); 52 | }); 53 | }); 54 | 55 | t.end(); 56 | }); 57 | 58 | test('Restore Nock', (t) => { 59 | nock.cleanAll(); 60 | t.end(); 61 | }); 62 | -------------------------------------------------------------------------------- /test/lib.revert.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hecate = require('../cli.js'); 4 | 5 | const test = require('tape'); 6 | const nock = require('nock'); 7 | const PassThrough = require('stream').PassThrough; 8 | 9 | test('Clean revert of single delta', (t) => { 10 | const hecate = new Hecate({ 11 | url: 'http://localhost:7777' 12 | }); 13 | 14 | nock('http://localhost:7777') 15 | .get('/api/delta/2') 16 | .reply(200, { 17 | features: { 18 | type: 'FeatureCollection', 19 | features: [{ 20 | id: 1, 21 | version: 2, 22 | action: 'modify', 23 | geometry:{ 24 | type: 'Point', 25 | coordinates: [-73.2055358886719,44.4822540283203] 26 | } 27 | }] 28 | } 29 | }) 30 | .get('/api/data/feature/1/history') 31 | .reply(200, [{ 32 | feat: { 33 | id: 1, 34 | type: 'Feature', 35 | action: 'modify', 36 | version: 2, 37 | properties: { 38 | modified: true 39 | }, 40 | geometry: { 41 | type: 'Point', 42 | coordinates: [0.0, 0.0] 43 | } 44 | } 45 | },{ 46 | feat: { 47 | id: 1, 48 | type: 'Feature', 49 | action: 'create', 50 | version: 1, 51 | properties: { 52 | created: true 53 | }, 54 | geometry: { 55 | type: 'Point', 56 | coordinates: [1.0, 1.0] 57 | } 58 | } 59 | }]); 60 | 61 | let buffer = ''; 62 | const results = new PassThrough().on('data', (data) => { 63 | buffer += String(data); 64 | }).on('end', () => { 65 | buffer = buffer.trim().split('\n').map((buff) => JSON.parse(buff)); 66 | 67 | t.deepEquals(buffer, [{ 68 | id: 1, 69 | type: 'Feature', 70 | action: 'modify', 71 | version: 2, 72 | properties: { 73 | created: true 74 | }, 75 | geometry: { 76 | type: 'Point', 77 | coordinates: [1, 1] 78 | } 79 | }]); 80 | 81 | t.end(); 82 | }); 83 | 84 | hecate._.revert.deltas({ 85 | output: results, 86 | start: 2, 87 | end: 2 88 | }, (err) => { 89 | t.error(err); 90 | }); 91 | }); 92 | 93 | test('Clean revert of multiple deltas', (t) => { 94 | const hecate = new Hecate({ 95 | url: 'http://localhost:7777' 96 | }); 97 | 98 | nock('http://localhost:7777') 99 | .get('/api/delta/2') 100 | .reply(200, { 101 | features: { 102 | type: 'FeatureCollection', 103 | features: [{ 104 | id: 1, 105 | version: 2, 106 | action: 'modify' 107 | }] 108 | } 109 | }) 110 | .get('/api/delta/3') 111 | .reply(200, { 112 | features: { 113 | type: 'FeatureCollection', 114 | features: [{ 115 | id: 2, 116 | version: 3, 117 | action: 'delete' 118 | }] 119 | } 120 | }) 121 | .get('/api/data/feature/1/history') 122 | .reply(200, [{ 123 | feat: { 124 | id: 1, 125 | type: 'Feature', 126 | action: 'modify', 127 | version: 2, 128 | properties: { 129 | modified: true 130 | }, 131 | geometry: { 132 | type: 'Point', 133 | coordinates: [0.0, 0.0] 134 | } 135 | } 136 | },{ 137 | feat: { 138 | id: 1, 139 | type: 'Feature', 140 | action: 'create', 141 | version: 1, 142 | properties: { 143 | created: true 144 | }, 145 | geometry: { 146 | type: 'Point', 147 | coordinates: [1.0, 1.0] 148 | } 149 | } 150 | }]) 151 | .get('/api/data/feature/2/history') 152 | .reply(200, [{ 153 | feat: { 154 | id: 2, 155 | type: 'Feature', 156 | action: 'delete', 157 | version: 3, 158 | properties: null, 159 | geometry: null 160 | } 161 | },{ 162 | feat: { 163 | id: 2, 164 | type: 'Feature', 165 | action: 'modify', 166 | version: 2, 167 | properties: { 168 | modified: true 169 | }, 170 | geometry: { 171 | type: 'Point', 172 | coordinates: [0.0, 0.0] 173 | } 174 | } 175 | },{ 176 | feat: { 177 | id: 2, 178 | type: 'Feature', 179 | action: 'create', 180 | version: 1, 181 | properties: { 182 | created: true 183 | }, 184 | geometry: { 185 | type: 'Point', 186 | coordinates: [1.0, 1.0] 187 | } 188 | } 189 | }]); 190 | 191 | let buffer = ''; 192 | const results = new PassThrough().on('data', (data) => { 193 | buffer += String(data); 194 | }).on('end', () => { 195 | buffer = buffer.trim().split('\n').map((buff) => JSON.parse(buff)); 196 | 197 | t.deepEquals(buffer, [{ 198 | id: 1, 199 | type: 'Feature', 200 | action: 'modify', 201 | version: 2, 202 | properties: { 203 | created: true 204 | }, 205 | geometry: { 206 | type: 'Point', 207 | coordinates: [1, 1] 208 | } 209 | },{ 210 | id: 2, 211 | type: 'Feature', 212 | action: 'restore', 213 | version: 3, 214 | properties: { 215 | modified: true 216 | }, 217 | geometry: { 218 | type: 'Point', 219 | coordinates: [0, 0] 220 | } 221 | }]); 222 | t.end(); 223 | }); 224 | 225 | hecate._.revert.deltas({ 226 | output: results, 227 | start: 2, 228 | end: 3 229 | }, (err) => { 230 | t.error(err); 231 | }); 232 | }); 233 | 234 | test('Failed revert as feature exists multiple times across detlas', (t) => { 235 | const hecate = new Hecate({ 236 | url: 'http://localhost:7777' 237 | }); 238 | 239 | nock('http://localhost:7777') 240 | .get('/api/delta/2') 241 | .reply(200, { 242 | features: { 243 | type: 'FeatureCollection', 244 | features: [{ 245 | id: 1, 246 | version: 1, 247 | action: 'create' 248 | }] 249 | } 250 | }) 251 | .get('/api/delta/3') 252 | .reply(200, { 253 | features: { 254 | type: 'FeatureCollection', 255 | features: [{ 256 | id: 1, 257 | version: 2, 258 | action: 'modify' 259 | }] 260 | } 261 | }) 262 | .get('/api/data/feature/1/history') 263 | .reply(200, [{ 264 | feat: { 265 | id: 1, 266 | type: 'Feature', 267 | action: 'modify', 268 | version: 2, 269 | properties: { 270 | modified: true 271 | }, 272 | geometry: { 273 | type: 'Point', 274 | coordinates: [0.0, 0.0] 275 | } 276 | } 277 | },{ 278 | feat: { 279 | id: 1, 280 | type: 'Feature', 281 | action: 'create', 282 | version: 1, 283 | properties: { 284 | created: true 285 | }, 286 | geometry: { 287 | type: 'Point', 288 | coordinates: [1.0, 1.0] 289 | } 290 | } 291 | }]) 292 | .get('/api/data/feature/1/history') 293 | .reply(200, [{ 294 | feat: { 295 | id: 1, 296 | type: 'Feature', 297 | action: 'modify', 298 | version: 2, 299 | properties: { 300 | modified: true 301 | }, 302 | geometry: { 303 | type: 'Point', 304 | coordinates: [0.0, 0.0] 305 | } 306 | } 307 | },{ 308 | feat: { 309 | id: 1, 310 | type: 'Feature', 311 | action: 'create', 312 | version: 1, 313 | properties: { 314 | created: true 315 | }, 316 | geometry: { 317 | type: 'Point', 318 | coordinates: [1.0, 1.0] 319 | } 320 | } 321 | }]); 322 | 323 | hecate._.revert.deltas({ 324 | output: new PassThrough(), 325 | start: 2, 326 | end: 3 327 | }, (err) => { 328 | t.equals(err.message, 'Feature: 1 exists multiple times across deltas to revert. reversion not supported'); 329 | t.end(); 330 | }); 331 | }); 332 | 333 | test('Failed revert as feature has been edited since desired revert', (t) => { 334 | const hecate = new Hecate({ 335 | url: 'http://localhost:7777' 336 | }); 337 | 338 | nock('http://localhost:7777') 339 | .get('/api/delta/2') 340 | .reply(200, { 341 | features: { 342 | type: 'FeatureCollection', 343 | features: [{ 344 | id: 1, 345 | version: 1, 346 | action: 'create' 347 | }] 348 | } 349 | }) 350 | .get('/api/data/feature/1/history') 351 | .reply(200, [{ 352 | feat: { 353 | id: 1, 354 | type: 'Feature', 355 | action: 'modify', 356 | version: 2, 357 | properties: { 358 | modified: true 359 | }, 360 | geometry: { 361 | type: 'Point', 362 | coordinates: [0.0, 0.0] 363 | } 364 | } 365 | },{ 366 | feat: { 367 | id: 1, 368 | type: 'Feature', 369 | action: 'create', 370 | version: 1, 371 | properties: { 372 | created: true 373 | }, 374 | geometry: { 375 | type: 'Point', 376 | coordinates: [1.0, 1.0] 377 | } 378 | } 379 | }]); 380 | 381 | hecate._.revert.deltas({ 382 | output: new PassThrough(), 383 | start: 2, 384 | end: 2 385 | }, (err) => { 386 | t.equals(err.message, 'Feature: 1 has been subsequenty edited. reversion not supported'); 387 | t.end(); 388 | }); 389 | }); 390 | 391 | test('Restore Nock', (t) => { 392 | nock.cleanAll(); 393 | t.end(); 394 | }); 395 | -------------------------------------------------------------------------------- /test/lib.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | for (const lib of fs.readdirSync(path.resolve(__dirname, '../lib'))) { 8 | test(`${lib}: Enforce lib API`, (t) => { 9 | const loaded = new (require(`../lib/${lib}`))({}); 10 | 11 | t.ok(loaded.help, 'Exposes help() function'); 12 | 13 | t.end(); 14 | }); 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/util.getAuth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getAuth = require('../util/get_auth.js'); 4 | const test = require('tape'); 5 | 6 | test('util/getAuth', (t) => { 7 | t.deepEquals(getAuth(), [{ 8 | name: 'hecate_username', 9 | message: 'Your Slack/Github Username', 10 | type: 'string', 11 | required: true, 12 | default: undefined 13 | },{ 14 | name: 'hecate_password', 15 | message: 'secure password to be used at login', 16 | hidden: true, 17 | replace: '*', 18 | required: true, 19 | type: 'string', 20 | default: undefined 21 | }], 'no auth provided'); 22 | 23 | t.deepEquals(getAuth({ 24 | username: 'ingalls' 25 | }), [{ 26 | name: 'hecate_username', 27 | message: 'Your Slack/Github Username', 28 | type: 'string', 29 | required: true, 30 | default: 'ingalls' 31 | },{ 32 | name: 'hecate_password', 33 | message: 'secure password to be used at login', 34 | hidden: true, 35 | replace: '*', 36 | required: true, 37 | type: 'string', 38 | default: undefined 39 | }], 'username provided'); 40 | 41 | t.deepEquals(getAuth({ 42 | password: 'yeaheh' 43 | }), [{ 44 | name: 'hecate_username', 45 | message: 'Your Slack/Github Username', 46 | type: 'string', 47 | required: true, 48 | default: undefined 49 | },{ 50 | name: 'hecate_password', 51 | message: 'secure password to be used at login', 52 | hidden: true, 53 | replace: '*', 54 | required: true, 55 | type: 'string', 56 | default: 'yeaheh' 57 | }], 'password provided'); 58 | 59 | t.deepEquals(getAuth({ 60 | username: 'ingalls', 61 | password: 'yeaheh' 62 | }), [{ 63 | name: 'hecate_username', 64 | message: 'Your Slack/Github Username', 65 | type: 'string', 66 | required: true, 67 | default: 'ingalls' 68 | },{ 69 | name: 'hecate_password', 70 | message: 'secure password to be used at login', 71 | hidden: true, 72 | replace: '*', 73 | required: true, 74 | type: 'string', 75 | default: 'yeaheh' 76 | }], 'username & password provided'); 77 | 78 | t.end(); 79 | }); 80 | -------------------------------------------------------------------------------- /test/util.revert.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const revert = require('../util/revert'); 5 | 6 | test('Revert#Inverse', (t) => { 7 | t.throws(() => { 8 | revert.inverse(); 9 | }, /Feature history cannot be empty/, 'Feature history cannot be empty'); 10 | 11 | t.throws(() => { 12 | revert.inverse(false); 13 | }, /Feature history cannot be empty/, 'Feature history cannot be empty'); 14 | 15 | t.throws(() => { 16 | revert.inverse({}); 17 | }, /Feature history cannot be empty/, 'Feature history cannot be empty'); 18 | 19 | t.throws(() => { 20 | revert.inverse([]); 21 | }, /Feature history cannot be empty/, 'Feature history cannot be empty'); 22 | 23 | t.throws(() => { 24 | revert.inverse([{ 25 | id: 1 26 | }], 1); 27 | }, /Feature: 1 missing initial create action/, 'Feature: 1 missing initial create action'); 28 | 29 | t.throws(() => { 30 | revert.inverse([{ 31 | id: 1, 32 | action: 'create' 33 | }, { 34 | id: 1, 35 | action: 'crazy' 36 | }], 2); 37 | }, /crazy not supported/, 'crazy not supported'); 38 | 39 | t.deepEquals(revert.inverse([{ 40 | id: 1, 41 | action: 'create', 42 | version: 1, 43 | properties: { 44 | desired: true 45 | }, 46 | geometry: { 47 | type: 'Point', 48 | coordinates: [1.0, 1.0] 49 | } 50 | }, { 51 | id: 1, 52 | action: 'modify', 53 | version: 2, 54 | properties: { 55 | undesired: true 56 | }, 57 | geometry: { 58 | type: 'Point', 59 | coordinates: [0.0, 0.0] 60 | } 61 | 62 | }], 2), { 63 | id: 1, 64 | type: 'Feature', 65 | action: 'modify', 66 | version: 2, 67 | properties: { 68 | desired: true 69 | }, 70 | geometry: { 71 | type: 'Point', 72 | coordinates: [1.0, 1.0] 73 | } 74 | }, 'modify->modify'); 75 | 76 | t.deepEquals(revert.inverse([{ 77 | id: 1, 78 | action: 'create', 79 | version: 1, 80 | properties: { 81 | desired: true 82 | }, 83 | geometry: { 84 | type: 'Point', 85 | coordinates: [1.0, 1.0] 86 | } 87 | }, { 88 | id: 1, 89 | action: 'delete', 90 | version: 2, 91 | properties: null, 92 | geometry: null 93 | }], 2), { 94 | id: 1, 95 | type: 'Feature', 96 | action: 'restore', 97 | version: 2, 98 | properties: { 99 | desired: true 100 | }, 101 | geometry: { 102 | type: 'Point', 103 | coordinates: [1.0, 1.0] 104 | } 105 | }, 'delete => restore'); 106 | 107 | t.deepEquals(revert.inverse([{ 108 | id: 1, 109 | action: 'create', 110 | version: 1, 111 | properties: { 112 | desired: true 113 | }, 114 | geometry: { 115 | type: 'Point', 116 | coordinates: [1.0, 1.0] 117 | } 118 | }, { 119 | id: 1, 120 | action: 'delete', 121 | version: 2, 122 | properties: null, 123 | geometry: null 124 | },{ 125 | id: 1, 126 | action: 'restore', 127 | version: 3, 128 | properties: { 129 | undesired: true 130 | }, 131 | geometry: { 132 | type: 'Point', 133 | coordinates: [0,0] 134 | } 135 | }], 3), { 136 | id: 1, 137 | type: 'Feature', 138 | action: 'delete', 139 | version: 3, 140 | properties: null, 141 | geometry: null 142 | }, 'restore => delete'); 143 | 144 | t.deepEquals(revert.inverse([{ 145 | id: 1, 146 | action: 'create', 147 | version: 1, 148 | properties: { 149 | desired: true 150 | }, 151 | geometry: { 152 | type: 'Point', 153 | coordinates: [1.0, 1.0] 154 | } 155 | }], 1), { 156 | id: 1, 157 | type: 'Feature', 158 | action: 'delete', 159 | version: 1, 160 | properties: null, 161 | geometry: null 162 | }, 'create => delete'); 163 | 164 | t.end(); 165 | }); 166 | 167 | test('Revert#createCache', (t) => { 168 | const db = revert.createCache(); 169 | 170 | t.equals(db.inTransaction, false); 171 | t.equals(db.open, true); 172 | t.equals(db.memory, false); 173 | t.equals(db.readonly, false); 174 | t.ok(/\/tmp\/revert\..*.\.sqlite/.test(db.name)); 175 | 176 | t.end(); 177 | }); 178 | -------------------------------------------------------------------------------- /test/util.validateBbox.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | const validateBbox = require('../util/validateBbox.js'); 5 | 6 | /** 7 | * From MDN guide to regexp: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions 8 | * @param {string} string regex string to escape 9 | * @return {string} escaped string 10 | */ 11 | function escapeRegExp(string) { 12 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 13 | } 14 | 15 | 16 | // Invalid bbox input test cases 17 | const invalidBboxes = [ 18 | // Input structure 19 | { 20 | bboxInput: '', 21 | expectedMsg: 'bbox must not be empty, must be minX,minY,maxX,maxY', 22 | description: 'Empty bbox input' 23 | }, { 24 | bboxInput: { 25 | 'bbox': [-101, 50, -100, 51] 26 | }, 27 | expectedMsg: 'bbox must be a string in the format minX,minY,maxX,maxY or Array in the format [minX,minY,maxX,maxY]', 28 | description: 'bbox input of a type other than array or string' 29 | }, { 30 | bboxInput: [-101, 50], 31 | expectedMsg: 'bbox must have four items in the format minX,minY,maxX,maxY or [minX,minY,maxX,maxY]', 32 | description: 'bbox input array with fewer than 4 items' 33 | }, { 34 | bboxInput: '-101, 50', 35 | expectedMsg: 'bbox must have four items in the format minX,minY,maxX,maxY or [minX,minY,maxX,maxY]', 36 | description: 'bbox input string with fewer than 4 items' 37 | }, 38 | // minX 39 | { 40 | bboxInput: ['test', 1, 2, 3], 41 | expectedMsg: 'bbox minX value must be a number between -180 and 180', 42 | description: 'Bbox minX that isnt a number' 43 | }, { 44 | bboxInput: [-190, 50, -100, 51], 45 | expectedMsg: 'bbox minX value must be a number between -180 and 180', 46 | description: 'Bbox minX that is less than -180' 47 | }, { 48 | bboxInput: [190, 50, 200, 51], 49 | expectedMsg: 'bbox minX value must be a number between -180 and 180', 50 | description: 'Bbox minX that is greater than 180' 51 | }, 52 | // minY 53 | { 54 | bboxInput: [-101, 'test', -100, 51], 55 | expectedMsg: 'bbox minY value must be a number between -90 and 90', 56 | description: 'Bbox minY that isnt a number' 57 | }, { 58 | bboxInput: [-101, -91, -100, 51], 59 | expectedMsg: 'bbox minY value must be a number between -90 and 90', 60 | description: 'Bbox minY that is less than -90' 61 | }, { 62 | bboxInput: [-101, 91, -100, 51], 63 | expectedMsg: 'bbox minY value must be a number between -90 and 90', 64 | description: 'Bbox minY that is greater than 90' 65 | }, 66 | // maxX 67 | { 68 | bboxInput: [-101, 50, 'test', 51], 69 | expectedMsg: 'bbox maxX value must be a number between -180 and 180', 70 | description: 'Bbox maxX that isnt a number' 71 | }, { 72 | bboxInput: [-101, 50, -190, 51], 73 | expectedMsg: 'bbox maxX value must be a number between -180 and 180', 74 | description: 'Bbox maxX that is less than -180' 75 | }, { 76 | bboxInput: [-101, 50, 190, 51], 77 | expectedMsg: 'bbox maxX value must be a number between -180 and 180', 78 | description: 'Bbox maxX that is greater than 180' 79 | }, 80 | // maxY 81 | { 82 | bboxInput: [-101, 50, -100, 'test'], 83 | expectedMsg: 'bbox maxY value must be a number between -90 and 90', 84 | description: 'Bbox maxY that isnt a number' 85 | }, { 86 | bboxInput: [-101, 50, -100, -91], 87 | expectedMsg: 'bbox maxY value must be a number between -90 and 90', 88 | description: 'Bbox maxY that is less than -90' 89 | }, { 90 | bboxInput: [-101, 50, -100, 91], 91 | expectedMsg: 'bbox maxY value must be a number between -90 and 90', 92 | description: 'Bbox maxY that is greater than 90' 93 | }, 94 | // Relative mins and maxes 95 | { 96 | bboxInput: [101, 50, 100, 51], 97 | expectedMsg: 'bbox minX value cannot be greater than maxX value', 98 | description: 'Bbox minX that is greater than the maxX' 99 | }, { 100 | bboxInput: [-101, 51, -100, 50], 101 | expectedMsg: 'bbox minY value cannot be greater than maxY value', 102 | description: 'Bbox minY that is greater than the maxY' 103 | } 104 | ]; 105 | 106 | const validBboxes = [{ 107 | bboxInput: '-101, 50, -100, 51', 108 | expected: '-101,50,-100,51', 109 | description: 'bbox input as a string' 110 | }, { 111 | bboxInput: [-101, 50, -100, 51], 112 | expected: '-101,50,-100,51', 113 | description: 'bbox input as an array of numbers' 114 | }, { 115 | bboxInput: ['-101', '50', '-100', '51'], 116 | expected: '-101,50,-100,51', 117 | description: 'bbox input as an array of strings' 118 | }]; 119 | 120 | // Assert the errors from invalid bbox inputs 121 | tape('Assert bbox fails', (t) => { 122 | 123 | invalidBboxes.forEach((test) => { 124 | const expected = new RegExp(escapeRegExp(test.expectedMsg)); 125 | t.throws(() => validateBbox(test.bboxInput), expected, test.description); 126 | }); 127 | t.end(); 128 | }); 129 | 130 | // Confirm valid bbox inputs 131 | tape('Assert valid bbox inputs', (t) => { 132 | 133 | validBboxes.forEach((test) => { 134 | t.deepEquals(validateBbox(test.bboxInput), test.expected, test.description); 135 | }); 136 | t.end(); 137 | }); 138 | -------------------------------------------------------------------------------- /test/util.validateGeojson.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Ajv = require('ajv'); 5 | const path = require('path'); 6 | const tape = require('tape'); 7 | const validateGeojson = require('../util/validateGeojson.js'); 8 | const pipeline = require('stream').pipeline; 9 | const split = require('split'); 10 | 11 | const ajv = new Ajv({ 12 | schemaId: 'auto' 13 | }); 14 | 15 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); 16 | 17 | // Assert the errors in the geojson file 18 | tape('Assert fails', (t) => { 19 | // Validate the corrupted sample data 20 | t.deepEquals(validateGeojson.validateFeature({ 21 | 'properties': { 22 | 'number':0, 23 | 'street':[{ 'display':'\\N','priority':0 }] 24 | }, 25 | 'geometry':{ 26 | 'type':'Point', 27 | 'coordinates': [23.6,23.5] 28 | } 29 | }), [{ 30 | message: 'Feature missing action', 31 | linenumber: 0 32 | },{ 33 | message: 'All GeoJSON must be type: Feature', 34 | linenumber: 0 35 | },{ 36 | message: '"type" member required', 37 | linenumber: 0 38 | }]); 39 | 40 | t.deepEquals(validateGeojson.validateFeature({ 41 | type: 'Feature', 42 | action: 'create', 43 | 'properties': { 44 | 'number':0, 45 | 'street':[{ 'display':'\\N','priority':0 }] 46 | }, 47 | 'geometry':{ 48 | 'type':'Point', 49 | 'coordinates': [null,23.5] 50 | } 51 | }), [{ 52 | message: 'each element in a position must be a number', 53 | linenumber: 0 54 | }]); 55 | 56 | t.deepEquals(validateGeojson.validateFeature({ 57 | type: 'Feature', 58 | action: 'create', 59 | properties: { 60 | number: 0, 61 | street: [{ 'display':'\\N','priority':0 }] 62 | }, 63 | geometry:{ 64 | coordinates: [null,23.5] 65 | } 66 | }), [{ 67 | message: '"type" member required', 68 | linenumber: 0 69 | }]); 70 | 71 | t.deepEquals(validateGeojson.validateFeature({ 72 | type: 'Feature', 73 | action: 'create', 74 | 'properties': { 75 | 'number':0, 76 | 'street':[{ 'display':'\\N','priority':0 }] 77 | } 78 | }), [{ 79 | message: 'Null or Invalid Geometry', 80 | linenumber: 0 81 | },{ 82 | message: '"geometry" member required', 83 | linenumber: 0 84 | }]); 85 | 86 | t.deepEquals(validateGeojson.validateFeature({ 87 | type: 'Feature', 88 | action: 'create', 89 | 'properties': { 90 | 'number':0, 91 | 'street':[{ 'display':'\\N','priority':0 }] 92 | }, 93 | geometry: { 94 | type: 'Point', 95 | coordinates: [0,0] 96 | } 97 | }), [{ 98 | message: 'coordinates must be other than [0,0]', 99 | linenumber: 0 100 | }]); 101 | 102 | t.deepEquals(validateGeojson.validateFeature({ 103 | type: 'Feature', 104 | action: 'delete', 105 | 'properties': { 106 | 'number':0, 107 | 'street': [{ 'display':'\\N','priority':0 }] 108 | }, 109 | geometry: { 110 | type: 'Point', 111 | coordinates: [0,0] 112 | } 113 | }), [{ 114 | message: 'Feature to delete must have id', 115 | linenumber: 0 116 | }]); 117 | 118 | t.deepEquals(validateGeojson.validateFeature({ 119 | id: 1, 120 | type: 'Feature', 121 | action: 'delete' 122 | }), [{ 123 | message: 'Feature to delete must have version', 124 | linenumber: 0 125 | },{ 126 | message: 'Feature to delete should have properties: null', 127 | linenumber: 0 128 | },{ 129 | message: 'Feature to delete should have geometry: null', 130 | linenumber: 0 131 | }]); 132 | 133 | t.deepEquals(validateGeojson.validateFeature({ 134 | type: 'Feature', 135 | action: 'modify', 136 | 'properties': { 137 | 'number':0, 138 | 'street': [{ 'display':'\\N','priority':0 }] 139 | }, 140 | geometry: { 141 | type: 'Point', 142 | coordinates: [1,1] 143 | } 144 | }), [{ 145 | message: 'Feature to modify must have id', 146 | linenumber: 0 147 | }]); 148 | 149 | t.deepEquals(validateGeojson.validateFeature({ 150 | type: 'Feature', 151 | action: 'create', 152 | 'properties': { 153 | 'number':0, 154 | 'street':[{ 'display':'\\N','priority':0 }] 155 | }, 156 | geometry: { 157 | type: 'Point' 158 | } 159 | }), [{ 160 | message: 'Null or Invalid Geometry', 161 | linenumber: 0 162 | },{ 163 | message: '"coordinates" member required', 164 | linenumber: 0 165 | }]); 166 | 167 | t.end(); 168 | }); 169 | 170 | // Confirm that the sample geojson file is a valid geojson file 171 | tape('Assert valid features', (t) => { 172 | pipeline( 173 | // By default parallel-transform has a high water mark of 16, 174 | // This file has > 16 features to ensure that a reader must be attached 175 | // to the transform stream so it finishes properly, otherwise this test will fail 176 | fs.createReadStream(path.resolve(__dirname, '.', './fixtures/valid-geojson.json')), 177 | split(), 178 | validateGeojson(), 179 | fs.createWriteStream('/dev/null'), 180 | (err) => { 181 | t.error(err); 182 | t.end(); 183 | } 184 | ); 185 | }); 186 | 187 | tape('Assert fails according to schema ', (t) => { 188 | const schemaJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, '.', './fixtures/schema.json'), 'utf8')); 189 | 190 | const schema = ajv.compile(schemaJson); 191 | 192 | t.deepEquals(validateGeojson.validateFeature({ 193 | action: 'create', 194 | 'properties': { 195 | 'number':0, 196 | 'street':[{ 'display':'\\N','priority':0 }] 197 | }, 198 | 'geometry':{ 199 | 'type':'Point', 200 | 'coordinates': [23.6,23.5] 201 | } 202 | }, { 203 | schema: schema 204 | }), [{ 205 | message: 'All GeoJSON must be type: Feature', 206 | linenumber: 0 207 | },{ 208 | message: 'should have required property \'source\'', 209 | linenumber: 0 210 | },{ 211 | message: '"type" member required', 212 | linenumber: 0 213 | }]); 214 | 215 | t.end(); 216 | }); 217 | 218 | tape('Duplicate ID Checks', (t) => { 219 | const ids = new Set(); 220 | 221 | t.deepEquals(validateGeojson.validateFeature({ 222 | id: 1, 223 | type: 'Feature', 224 | action: 'modify', 225 | version: 2, 226 | properties: { 227 | number: 0, 228 | street: [{ 'display':'\\N','priority':0 }] 229 | }, 230 | geometry: { 231 | type: 'Point', 232 | coordinates: [23.6,23.5] 233 | } 234 | }, { 235 | ids: ids 236 | }), []); 237 | 238 | t.deepEquals(validateGeojson.validateFeature({ 239 | id: 1, 240 | type: 'Feature', 241 | action: 'modify', 242 | version: 2, 243 | properties: { 244 | number: 0, 245 | street: [{ 'display':'\\N','priority':0 }] 246 | }, 247 | geometry: { 248 | type: 'Point', 249 | coordinates: [23.6,23.5] 250 | } 251 | }, { 252 | ids: ids 253 | }), [{ 254 | message: 'Feature ID: 1 exists more than once', 255 | linenumber: 0 256 | }]); 257 | 258 | t.end(); 259 | }); 260 | -------------------------------------------------------------------------------- /util/eot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Transform = require('stream').Transform; 4 | 5 | /** 6 | * Transform stream to passthrough linedelimited GeoJSON 7 | * and upon termination of the stream, ensure EOT 8 | * character was present to ensure all data was obtained 9 | * 10 | * @class 11 | * @private 12 | */ 13 | class EOT extends Transform { 14 | /** 15 | * Construct a new EOT Instance 16 | * 17 | * @param {function} cb (err, res) style callback 18 | */ 19 | constructor(cb) { 20 | if (!cb) throw new Error('callback required'); 21 | 22 | super(); 23 | 24 | this.cb = cb; 25 | this.eot = false; 26 | 27 | Transform.call(this); 28 | } 29 | 30 | /** 31 | * Internal transform function to passthrough data 32 | * and look for EOT character 33 | * 34 | * @param {Buffer} chunk chunk to passthrough 35 | * @param {string} encoding The encoding of the chunk 36 | * @param {function} done completion callback 37 | * 38 | * @returns {function} callback function 39 | */ 40 | _transform(chunk, encoding, done) { 41 | if (chunk.indexOf('\u0004') === chunk.length - 1) { 42 | this.eot = true; 43 | 44 | const str = String(chunk); 45 | chunk = new Buffer.from(str.slice(0, str.length - 1)); 46 | } 47 | 48 | this.push(chunk); 49 | 50 | return done(); 51 | } 52 | 53 | /** 54 | * Ensure the stream completeled successfully, checking for the EOT character 55 | * 56 | * @param {function} done completion callback 57 | * 58 | * @returns {function} (err, res) style callback 59 | */ 60 | _final(done) { 61 | if (!this.eot) return this.cb(new Error('Download Stream Terminated Abruptly - No EOT')); 62 | 63 | return done(); 64 | } 65 | } 66 | 67 | module.exports = EOT; 68 | -------------------------------------------------------------------------------- /util/get_auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (auth = {}) => { 4 | return [{ 5 | name: 'hecate_username', 6 | message: 'Your Slack/Github Username', 7 | type: 'string', 8 | required: true, 9 | default: auth.username 10 | }, { 11 | name: 'hecate_password', 12 | message: 'secure password to be used at login', 13 | hidden: true, 14 | replace: '*', 15 | required: true, 16 | type: 'string', 17 | default: auth.password 18 | }]; 19 | }; 20 | -------------------------------------------------------------------------------- /util/revert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { promisify } = require('util'); 4 | const Sqlite = require('better-sqlite3'); 5 | const Q = require('d3-queue').queue; 6 | const path = require('path'); 7 | const os = require('os'); 8 | const fs = require('fs'); 9 | 10 | /** 11 | * Given the feature history for a single feature and the version of the feature 12 | * to be reverted, calculate a feature that can be uploaded which will 13 | * restore the state of the feature to that of the version preceding 14 | * the version supplied 15 | * 16 | * Example: 17 | * 18 | * User wants to revert/rollback the changes made in v4 to be those in v3 19 | * 20 | * Current State: 21 | * Feature: 123 22 | * [ v1, v2, v3, v4 ] 23 | * 24 | * End State: 25 | * Feature:123 26 | * [ v1, v2, v3, v4, v5 ] 27 | * 28 | * Where v5 is the calculated inverse operation of v4 29 | * 30 | * Inverses: 31 | * 32 | * Below is the table of inverses, the action that must be applied 33 | * to "undo" a given feature action 34 | * 35 | * | Initial Action | Inverse | 36 | * | -------------- | ------- | 37 | * | Create | Delete | 38 | * | Modify | Modify | 39 | * | Delete | Restore | 40 | * | Restore | Delete | 41 | * 42 | * See the Hecate docs for more information about feature actions 43 | * and versioning 44 | * 45 | * @private 46 | * 47 | * @param {Object[]} history Array of features accross all verisons of the feature 48 | * @param {number} version feature version that should be rolled back 49 | * 50 | * @returns {Object} Returns calculated inverse feature 51 | */ 52 | function inverse(history, version) { 53 | if (!history || !Array.isArray(history) || history.length === 0) { 54 | throw new Error('Feature history cannot be empty'); 55 | } else { 56 | history = history.sort((a, b) => { 57 | return (a.version ? a.version : 1) - (b.version ? b.version : 1); 58 | }); 59 | } 60 | 61 | if (!version || isNaN(version)) { 62 | throw new Error('Feature version cannot be empty'); 63 | } else if (version > history.length) { 64 | throw new Error('version cannot be higher than feature history'); 65 | 66 | // If the history length is 1, the operation must be a 67 | // create operation, otherwise history is missing 68 | } else if (history.length >= 1 && history[0].action !== 'create') { 69 | throw new Error(`Feature: ${history[0].id} missing initial create action`); 70 | 71 | // If the version to be reverted isn't the last element in the array 72 | // it is a "dirty revert" or a revert that could potentially be in conflict 73 | // with subsequent changes, these are not currently supported 74 | } else if (version < history.length) { 75 | throw new Error(`Feature: ${history[0].id} has been subsequenty edited. reversion not supported`); 76 | 77 | // Feature has just been created and should be deleted 78 | } else if (history.length === 1) { 79 | const feat = history[0]; 80 | 81 | return { 82 | id: feat.id, 83 | action: 'delete', 84 | version: 1, 85 | type: 'Feature', 86 | properties: null, 87 | geometry: null 88 | }; 89 | } else { 90 | const desired = history[version - 2]; 91 | const latest = history[version - 1]; 92 | 93 | let action; 94 | if (latest.action === 'modify') { 95 | action = 'modify'; 96 | } else if (latest.action === 'delete') { 97 | action = 'restore'; 98 | } else if (latest.action === 'restore') { 99 | action = 'delete'; 100 | } else { 101 | throw new Error(`${latest.action} not supported`); 102 | } 103 | 104 | return { 105 | id: latest.id, 106 | type: 'Feature', 107 | action: action, 108 | version: latest.version, 109 | properties: desired.properties, 110 | geometry: desired.geometry 111 | }; 112 | } 113 | } 114 | 115 | /** 116 | * Iterate over Sqlite3 database containing features to revert to previous state 117 | * 118 | * Writes inversion to given writable stream 119 | * 120 | * @private 121 | * 122 | * @param {Object} db sqlite3 db to iterate over 123 | * @param {Stream} stream output stream to write inverted features to 124 | */ 125 | function iterate(db, stream) { 126 | const stmt = db.prepare(` 127 | SELECT 128 | version, 129 | history 130 | FROM 131 | features; 132 | `); 133 | 134 | for (const row of stmt.iterate()) { 135 | const history = JSON.parse(row.history).map((feat) => { 136 | return feat.feat; 137 | }); 138 | 139 | const inv = inverse(history, row.version); 140 | 141 | stream.write(JSON.stringify(inv) + '\n'); 142 | } 143 | } 144 | 145 | /** 146 | * Given a start/end range for a set of deltas, download 147 | * each of the deltas, then iterate through each feature, 148 | * retreiving it's history and writing it to disk 149 | * 150 | * @private 151 | * 152 | * @param {Object} options options object 153 | * @param {number} options.start Delta Start ID 154 | * @param {number} options.end Delta End ID 155 | * 156 | * @param {Hecate} api Hecate Instance for API calls 157 | * 158 | * @returns {Promise} Promise containing db instance or error 159 | */ 160 | function cache(options, api) { 161 | return new Promise((resolve, reject) => { 162 | const db = createCache(); 163 | 164 | const getDelta = promisify(api.getDelta); 165 | const getFeatureHistory = promisify(api.getFeatureHistory); 166 | 167 | const stmt = db.prepare(` 168 | INSERT INTO features (id, version, history) 169 | VALUES (?, ?, ?); 170 | `); 171 | 172 | deltai(options.start - 1); 173 | 174 | /** 175 | * Retrieve a single delta at a time, and then 176 | * each of it's component feature histories in parallem 177 | * commiting each to the database 178 | * 179 | * @private 180 | * 181 | * @param {number} i delta ID last retrieved 182 | * 183 | * @return {undefined} 184 | */ 185 | async function deltai(i) { 186 | if (i < options.end) { 187 | i++; 188 | } else { 189 | return resolve(db); 190 | } 191 | 192 | const delta = await getDelta({ 193 | delta: i 194 | }); 195 | 196 | const q = new Q(50); 197 | 198 | for (const feat of delta.features.features) { 199 | q.defer(async(feat, done) => { 200 | const history = await getFeatureHistory({ 201 | feature: feat.id 202 | }); 203 | 204 | try { 205 | stmt.run(feat.id, feat.version, JSON.stringify(history)); 206 | } catch (err) { 207 | if (err.message === 'UNIQUE constraint failed: features.id') { 208 | return done(new Error(`Feature: ${feat.id} exists multiple times across deltas to revert. reversion not supported`)); 209 | } else { 210 | return done(err); 211 | } 212 | } 213 | 214 | return done(); 215 | }, feat); 216 | } 217 | 218 | q.awaitAll((err) => { 219 | if (err) return reject(err); 220 | 221 | process.nextTick(() => { 222 | deltai(i); 223 | }); 224 | }); 225 | } 226 | }); 227 | } 228 | 229 | /** 230 | * Create a new reversion sqlite3 database, initialize it with table 231 | * definitions, and pass back db object to caller 232 | * 233 | * @private 234 | * 235 | * @returns {Object} Sqlite3 Database Handler 236 | */ 237 | function createCache() { 238 | const db = new Sqlite(path.resolve(os.tmpdir(), `revert.${Math.random().toString(36).substring(7)}.sqlite`)); 239 | 240 | db.exec(` 241 | CREATE TABLE features ( 242 | id INTEGER PRIMARY KEY, 243 | version INTEGER NOT NULL, 244 | history TEXT NOT NULL 245 | ); 246 | `); 247 | 248 | return db; 249 | } 250 | 251 | /** 252 | * Given a sqlite instance, close and delete it 253 | * 254 | * @private 255 | * 256 | * @param {Object} db sqlite3 database instance 257 | * 258 | * @returns {undefined} 259 | */ 260 | function cleanCache(db) { 261 | const name = db.name; 262 | 263 | db.close(); 264 | 265 | fs.unlinkSync(name); 266 | } 267 | 268 | module.exports.inverse = inverse; 269 | module.exports.iterate = iterate; 270 | module.exports.cache = cache; 271 | module.exports.createCache = createCache; 272 | module.exports.cleanCache = cleanCache; 273 | -------------------------------------------------------------------------------- /util/validateBbox.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Accepts a bbox as a string or array, and validates (naive with regards to the Antimeridian) 5 | * 6 | * @private 7 | * 8 | * @param {Array|string} bboxInput Array in the format [minX,minY,maxX,maxY] or string in the format minX,minY,maxX,maxY 9 | * @returns {string} String in the format minX,minY,maxX,maxY 10 | */ 11 | function validateBbox(bboxInput) { 12 | 13 | if (!bboxInput) throw new Error('bbox must not be empty, must be minX,minY,maxX,maxY'); 14 | if (!(typeof bboxInput == 'string' || Array.isArray(bboxInput))) { 15 | throw new Error('bbox must be a string in the format minX,minY,maxX,maxY or Array in the format [minX,minY,maxX,maxY]'); 16 | } 17 | 18 | let bbox = Array.isArray(bboxInput) ? bboxInput : bboxInput.split(','); 19 | 20 | if (bbox.length !== 4) throw new Error('bbox must have four items in the format minX,minY,maxX,maxY or [minX,minY,maxX,maxY]'); 21 | 22 | // Convert string coordinates to numbers 23 | bbox = bbox.map(Number); 24 | 25 | // check if valid bbox 26 | // TODO: handle crossing the Antimeridian 27 | if (isNaN(bbox[0]) || bbox[0] < -180 || bbox[0] > 180) 28 | throw new Error('bbox minX value must be a number between -180 and 180'); 29 | if (isNaN(bbox[1]) || bbox[1] < -90 || bbox[1] > 90) 30 | throw new Error('bbox minY value must be a number between -90 and 90'); 31 | if (isNaN(bbox[2]) || bbox[2] < -180 || bbox[2] > 180) 32 | throw new Error('bbox maxX value must be a number between -180 and 180'); 33 | if (isNaN(bbox[3]) || bbox[3] < -90 || bbox[3] > 90) 34 | throw new Error('bbox maxY value must be a number between -90 and 90'); 35 | if (bbox[0] > bbox[2]) 36 | throw new Error('bbox minX value cannot be greater than maxX value'); 37 | if (bbox[1] > bbox[3]) 38 | throw new Error('bbox minY value cannot be greater than maxY value'); 39 | 40 | return bbox.join(); 41 | } 42 | 43 | 44 | module.exports = validateBbox; 45 | -------------------------------------------------------------------------------- /util/validateGeojson.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const transform = require('parallel-transform'); 4 | const geojsonhint = require('@mapbox/geojsonhint').hint; 5 | const turf = require('@turf/turf'); 6 | const rewind = require('geojson-rewind'); 7 | const Ajv = require('ajv'); 8 | 9 | const ajv = new Ajv({ 10 | schemaId: 'auto' 11 | }); 12 | 13 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); 14 | 15 | /** 16 | * Ensure geometries are valid before import 17 | * 18 | * @private 19 | * 20 | * @param {Object} opts Options object 21 | * @param {boolean} opts.ignoreRHR=false Ignore Right Hand Rule errors 22 | * @param {Object} opts.schema JSON Schema to validate properties against 23 | * @param {boolean} opts.ids If false, disable duplicate ID checking 24 | * 25 | * @return {Stream} transform stream to validate GeoJSON 26 | */ 27 | function validateGeojson(opts = {}) { 28 | // Flag to track the feature line number 29 | let linenumber = 0; 30 | 31 | let schema = false; 32 | 33 | if (opts.schema) { 34 | schema = ajv.compile(opts.schema); 35 | } 36 | 37 | const ids = opts.ids === false ? false : new Set(); 38 | 39 | return transform(1, (feat, cb) => { 40 | if (!feat || !feat.trim()) return cb(null, ''); 41 | 42 | linenumber++; 43 | const errors = validateFeature(feat.toString('utf8'), { 44 | linenumber: linenumber, 45 | ignoreRFR: opts.ignoreRHR, 46 | schema: schema, 47 | ids: ids 48 | }); 49 | 50 | if (errors.length) { 51 | errors.map((error) => { 52 | console.error(error); 53 | }); 54 | 55 | throw new Error('Invalid Feature'); 56 | } 57 | 58 | return cb(null, feat+'\n'); 59 | }); 60 | 61 | } 62 | 63 | /** 64 | * Validate a single feature 65 | * 66 | * @private 67 | * 68 | * @param {Object|string} line Feature to validate 69 | * @param {Object} options Validation Options 70 | * @param {boolean} options.ignoreRHR Ignore winding order 71 | * @param {Function} options.schema AJV Function to validate feature properties against a JSON Schema 72 | * @param {number} options.linenumber Linenumber to output in error object 73 | * @param {Set} options.ids Set to keep track of feature id duplicates 74 | * 75 | * @returns {Array} Array of errors (empty array if none) 76 | */ 77 | function validateFeature(line, options) { 78 | if (!options) options = {}; 79 | if (!options.linenumber) options.linenumber = 0; 80 | 81 | // list of errors linked to the file name and line number 82 | const errors = []; 83 | 84 | let feature; 85 | if (typeof line === 'object') { 86 | feature = line; 87 | } else { 88 | feature = JSON.parse(line); 89 | } 90 | 91 | if (options.ids && feature.id && options.ids.has(feature.id)) { 92 | errors.push({ 93 | message: `Feature ID: ${feature.id} exists more than once`, 94 | linenumber: options.linenumber 95 | }); 96 | } else if (options.ids && feature.id) { 97 | options.ids.add(feature.id); 98 | } 99 | 100 | if (!feature.action) { 101 | errors.push({ 102 | message: 'Feature missing action', 103 | linenumber: options.linenumber 104 | }); 105 | } 106 | 107 | if (feature.action && !['create', 'modify', 'delete', 'restore'].includes(feature.action)) { 108 | errors.push({ 109 | message: 'Invalid action', 110 | linenumber: options.linenumber 111 | }); 112 | } 113 | 114 | if (!feature.type || feature.type !== 'Feature') { 115 | errors.push({ 116 | message: 'All GeoJSON must be type: Feature', 117 | linenumber: options.linenumber 118 | }); 119 | } 120 | 121 | if (['modify', 'delete', 'restore'].includes(feature.action)) { 122 | if (!feature.id) { 123 | errors.push({ 124 | message: `Feature to ${feature.action} must have id`, 125 | linenumber: options.linenumber 126 | }); 127 | } else if (!feature.version) { 128 | errors.push({ 129 | message: `Feature to ${feature.action} must have version`, 130 | linenumber: options.linenumber 131 | }); 132 | } 133 | } 134 | 135 | // Delete features are special in that they have null geometry && properties 136 | if (feature.action === 'delete') { 137 | if (feature.properties === undefined) { 138 | errors.push({ 139 | message: 'Feature to delete should have properties: null', 140 | linenumber: options.linenumber 141 | }); 142 | } 143 | 144 | if (feature.geometry === undefined) { 145 | errors.push({ 146 | message: 'Feature to delete should have geometry: null', 147 | linenumber: options.linenumber 148 | }); 149 | } 150 | // All other features are 100% standard GeoJSON 151 | } else { 152 | feature = rewind(feature); 153 | 154 | const geojsonErrs = geojsonhint(feature).filter((err) => { 155 | if (options.ignoreRHR && err.message === 'Polygons and MultiPolygons should follow the right-hand rule') { 156 | return false; 157 | } else { 158 | return true; 159 | } 160 | }); 161 | 162 | // Validate that the feature has the required properties by the schema 163 | if (options.schema) { 164 | options.schema(feature.properties); 165 | if (options.schema.errors) { 166 | options.schema.errors.forEach((e) => { 167 | errors.push({ 168 | message: e.message, 169 | linenumber: options.linenumber 170 | }); 171 | }); 172 | } 173 | } 174 | 175 | if ( 176 | !feature.geometry 177 | || !feature.geometry.coordinates 178 | || !feature.geometry.coordinates.length 179 | ) { 180 | errors.push({ 181 | 'message': 'Null or Invalid Geometry', 182 | linenumber: options.linenumber 183 | }); 184 | } 185 | 186 | if (geojsonErrs.length) { 187 | for (const err of geojsonErrs) { 188 | errors.push({ 189 | message: err.message, 190 | linenumber: options.linenumber 191 | }); 192 | } 193 | } else { // if the geojson is invalid, turf will err 194 | turf.coordEach(feature, (coords) => { 195 | if (coords[0] < -180 || coords[0] > 180) { 196 | errors.push({ 197 | message: 'longitude must be between -180 and 180', 198 | linenumber: options.linenumber 199 | }); 200 | } 201 | if (coords[1] < -90 || coords[1] > 90) { 202 | 203 | errors.push({ 204 | message: 'latitude must be between -90 and 90', 205 | linenumber: options.linenumber 206 | }); 207 | } 208 | if (coords[0] === 0 && coords[1] === 0) { 209 | errors.push({ 210 | message: 'coordinates must be other than [0,0]', 211 | linenumber: options.linenumber 212 | }); 213 | } 214 | }); 215 | } 216 | } 217 | 218 | return errors; 219 | } 220 | 221 | module.exports = validateGeojson; 222 | module.exports.validateFeature = validateFeature; 223 | --------------------------------------------------------------------------------