├── .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 |
--------------------------------------------------------------------------------