├── .gitignore ├── CHANGELOG.md ├── README.md ├── examples ├── geofence.js ├── search.js └── set_get.js ├── package-lock.json ├── package.json ├── src ├── live_geofence.js ├── tile38.js └── tile38_query.js └── test ├── test_hook_commands.js ├── test_keys_commands.js ├── test_tile38.js └── test_tile38_query.js /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /dist/ 3 | /data/ 4 | /node_modules/ 5 | .idea/ 6 | src/test.js 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Revision history: 2 | ====== 3 | ### 0.9.4 / October 4, 2022 4 | * Fixed issue #28 with WHEREEVAL command 5 | 6 | ### 0.9.3 / October 4, 2022 7 | * resolved another issue with package.json 8 | 9 | ### 0.9.2 / October 4, 2022 10 | * updated dependencies 11 | * fixed issue with incorrect version number in package-lock.json 12 | 13 | ### 0.9.1 / March 22, 2022 14 | * fixed an issue that caused the 0.9.0 version to not be properly published to npm 15 | 16 | ### 0.9.0 / November 29, 2021 17 | * fixed bug in chaining of options 18 | * fixed issues with new executeCommand function 19 | 20 | ### 0.8.0 / November, 2021 21 | * whenever a Promise is rejected, now passing back Error object instead of string #10 22 | * added NODWELL support for ROAM command #31 23 | * added support for TIMEOUT 24 | * added support for setting multiple fields using FSET 25 | * added support for RENAME and RENAMENX 26 | * updated package.json and cleaned up dependencies 27 | * fixed tests that broke against recent versions of Tile38 28 | 29 | ### 0.7.0 / August, 2019 30 | * Fixed #20, FENCE duplicate in query to server 31 | * Fixed #21, added support for custom logger 32 | * Fixed #22, failure parsing responses after server reconnect 33 | * Updated dependencies to fix security vulnerabilities 34 | 35 | ### 0.6.7 / March, 2019 36 | * Fixed broken test due to changed response on BOUNDS query 37 | * Added support for WHEREIN, WHEREEVAL, WHEREEVALSHA, CLIP, CIRCLE 38 | 39 | 40 | #### 0.6.6 / May 23, 2018 41 | * updated test & babel dependencies to fix security vulnerability 42 | * fix for issue #5 (nearby query without radius) 43 | 44 | #### 0.6.5 / May 22, 2018 45 | * fixed bugs in redis communication 46 | * added support for Tile38 server authentication 47 | * added option to use TILE38_PASSWD env variable for authentication 48 | * added option to use TILE38_PORT env variable for port config 49 | * added option to use TILE38_HOST env variable for host config 50 | * tests now run on default port 9851 unless TILE38_PORT is set 51 | * fixed auth() function (though there should be no need to call this directly) 52 | * added error handler with logging for redis client 53 | 54 | #### 0.6.4 / Feb 22, 2017 55 | * added convenience methods such as ids(), count() that can be used instead of output('ids'), output('count'), etc. 56 | * added support for AUTH command 57 | * onClose() for live fence now accepts callback method 58 | * fixed incorrect warning message 59 | 60 | #### 0.6.3 / Feb 21, 2017 61 | * Adding capability to create webhooks with roaming geofenced search 62 | 63 | #### 0.6.2 / Feb 7, 2017 64 | * Fixed issue with Redis command encoding for live fence queries 65 | 66 | #### 0.6.1 / Feb 6, 2017 67 | * Module packaging fix 68 | 69 | #### 0.6.0 / Feb 6, 2017 70 | * Added support for live geofences 71 | 72 | #### 0.5.0 / Feb 3, 2017 73 | * Implemented method chaining for search methods. Improved test coverage and improved README with search query examples 74 | 75 | 76 | Anything older than 0.5.0 was never used by anyone but the author. 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Tile38 Node driver 3 | 4 | This library can be used to access the Tile38 server from Node.js apps. 5 | 6 | 7 | # Links 8 | * [Project git repo](https://github.com/phulst/node-tile38) 9 | * [Tile38 website](http://tile38.com/) 10 | * [Tile38 Github](https://github.com/tidwall/tile38) 11 | 12 | # Installation 13 | 14 | ``` 15 | npm install tile38 16 | ``` 17 | 18 | # Overview 19 | 20 | While you can use your preferred Redis library to communicate with the Tile38 geolocation database, this node library 21 | offers a much more pleasant query interface for all Tile38 commands. It also supports live geofencing using 22 | persistent sockets to the server. 23 | 24 | In most cases, commands follow the [command documentation](http://tile38.com/commands/) on the Tile38 website, 25 | though the search/scan commands use method chaining. You can find some examples below as well as in the 26 | examples folder. 27 | 28 | This library has not been tested in the browser. You generally would not want to expose your database directly to the 29 | internet, and you also don't want to share Tile38 access credentials to a web client, so using this in the browser is not a good idea. 30 | 31 | 32 | ## Revision history 33 | 34 | See the [CHANGELOG](./CHANGELOG.md) 35 | 36 | ## Connection 37 | 38 | ```javascript 39 | var Tile38 = require('tile38'); 40 | var client = new Tile38(); 41 | // save a location 42 | client.set('fleet', 'truck1', [33.5123, -112.2693]); 43 | // save a location with additional fields 44 | client.set('fleet', 'truck2', [33.5123, -112.2693], { value: 10, othervalue: 20}); 45 | ``` 46 | 47 | You can pass any non-default connection settings into the Tile38 constructor, and you can also turn on 48 | optional debug logging as illustrated below. 49 | 50 | ``` 51 | var client = new Tile38({host: 'host.server.com', port: 9850, debug: true }); 52 | ``` 53 | 54 | You can also set the hostname, port and password using the environment vars TILE38_HOST, TILE38_PORT and TILE38_PASSWD. 55 | These environment variables will only be used if values are not passed into the constructor explicitly 56 | 57 | ## Custom logger 58 | 59 | This driver does not use a logging library, and logs messages to the console by default. 60 | However, you may pass a custom log implementation into the constructor. 61 | Your custom logger must implement the 'log', 'warn' and 'error' functions, all accepting strings. 62 | 63 | ``` 64 | const customLogger = { 65 | log: function(msg) { ... }, 66 | warn: function(msg) { ... }, 67 | error: function(msg) { ... } 68 | } 69 | const client = new Tile38({debug: true, logger: customLogger }); 70 | ``` 71 | 72 | ## Promises 73 | 74 | All of the implemented methods return promises (with the exception of executeFence(), see more info on that below). 75 | 76 | ```javascript 77 | client.get('fleet', 'truck1').then(data => { 78 | console.log(data); // prints coordinates in geoJSON format 79 | 80 | }).catch(err => 81 | console.log(err); // id not found 82 | }); 83 | 84 | // return the data as type POINT, and include FIELDS as well. 85 | client.get('fleet', 'truck2', {type: 'POINT', withfields: true}).then(data => { 86 | console.log(`truck2 is at ${data.point.lat},${data.point.lon}`); 87 | console.dir(data.fields); 88 | }); 89 | // There's also a getPoint(id,key) method that can be used as a shortcut instead of get(id,key,{type:'POINT'}) 90 | // as well as similar getBounds and getHash methods. 91 | ``` 92 | 93 | Many commands may not return values, but they still return promises, allowing you to wait until 94 | your changes have been persisted. 95 | 96 | ```javascript 97 | client.set('fleet', 'truck1', [33.5123, -112.2693]).then(() => { 98 | console.log('your changes have been persisted'); 99 | }); 100 | ``` 101 | 102 | You may, of course, prefer to use async/await instead of Promises, like so: 103 | 104 | ```javascript 105 | let data = await client.getPoint('fleet', 'truck2'); 106 | console.log(`truck2 is at ${data.point.lat},${data.point.lon}`); 107 | ``` 108 | 109 | # Command examples 110 | 111 | The command documentation for Tile38 server is followed as closely as possible. Command names become function names, 112 | mandatory properties become arguments, and optional properties become either optional arguments or are passed in 113 | through an options object argument. 114 | 115 | For example, the command 116 | 117 | ``` 118 | JGET key id path 119 | ``` 120 | 121 | is called as follows: 122 | 123 | ```javascript 124 | client.jget(key, id, path) 125 | ``` 126 | 127 | ## keys commands 128 | 129 | Some examples of keys commands: 130 | 131 | ```javascript 132 | client.bounds('fleet'); 133 | client.del('feet', 'truck2'); 134 | client.drop('fleet'); 135 | client.expire('fleet','truck', 10); 136 | client.fset('fleet', 'truck1', 'speed', 16); 137 | client.fset('fleet', 'truck1', {speed: 16, driver: 1224}); 138 | client.stats('fleet1', 'fleet2'); 139 | ...etc 140 | ``` 141 | 142 | ### get command 143 | The get command accepts an optional object that can be use to set the response data type: 144 | 145 | 146 | ```javascript 147 | // return truck1 location as a geoJSON object 148 | client.get('fleet', 'truck1'); 149 | client.get('fleet', 'truck1', { type: 'OBJECT' }); // equivalent 150 | // return as POINT (2 element array with lat/lon coordinates) 151 | client.get('fleet', 'truck1', { type: 'POINT' }); 152 | client.getPoint('fleet', 'truck1'); // equivalent of above 153 | // return bounding rectangle 154 | client.get('fleet', 'truck1', { type: 'BOUNDS' }); 155 | client.getBounds('fleet', 'truck1'); // equivalent of above 156 | // return a geohash with precision 6 (must be between 1 and 22) 157 | client.get('fleet', 'truck1', { type: 'HASH 6' }); 158 | client.getHash('fleet', 'truck1'); // equivalent of above 159 | client.getHash('fleet', 'truck1', { precision: 8}); // if you need different precision from default (6) 160 | 161 | // if you want the 'get' function to return fields as well, use the 'withfields' property 162 | client.get('fleet', 'truck1', { withfields: true }); 163 | ``` 164 | 165 | ### set command 166 | 167 | The set command has various forms. 168 | 169 | set(key, id, locationObject, fields, options) 170 | 171 | ```javascript 172 | // set a simple lat/lng coordinate 173 | client.set('fleet', 'truck1', [33.5123, -112.2693]) 174 | // set with additional fields 175 | client.set('fleet', 'truck1', [33.5123, -112.2693], { field1: 10, field2: 20}); 176 | // set lat/lon/alt coordinates, and expire in 120 secs 177 | client.set('fleet', 'truck1', [33.5123, -112.2693, 120.0], null, {expire: 120}) 178 | // set bounds 179 | set('props', 'house1', [33.7840, -112.1520, 33.7848, -112.1512]) 180 | // set an ID by geohash 181 | set('props', 'area1', '9tbnwg') // assumes HASH by default if only one extra parameter 182 | // set a String value 183 | set('props', 'area2', 'my string value', null, {type: 'string'}) # or force to String type 184 | // set with geoJson object 185 | set('cities', 'tempe', geoJsonObject) 186 | // only set truck1 if it doesn't exist yet 187 | client.set('fleet', 'truck1', [33.5123, -112.2693], null, {onlyIfNotExists: true}) 188 | ``` 189 | 190 | ### search commands 191 | 192 | The search commands use method chaining to deal with its many available options. See the query examples below, or look at 193 | tile38_query.js to see all available methods. 194 | 195 | 196 | #### One time results vs live geofence 197 | 198 | To execute the query and get the search results, use the execute() function, which will return a promise to the results. 199 | 200 | ```javascript 201 | let query = client.intersectsQuery('fleet').bounds(33.462, -112.268, 33.491, -112.245); 202 | query.execute().then(results => { 203 | console.dir(results); // results is an object. 204 | }).catch(err => { 205 | console.error("something went wrong! " + err); 206 | )}; 207 | ``` 208 | 209 | To set up a live geofence that will use a websocket to continuously send updates, you construct your query the exact same way. 210 | However, instead of execute() (which returns a Promise), use the executeFence() function while passing in a callback function. 211 | 212 | ```javascript 213 | let query = client.intersectsQuery('fleet').detect('enter','exit').bounds(33.462, -112.268, 33.491, -112.245); 214 | let fence = query.executeFence((err, results) => { 215 | // this callback will be called multiple times 216 | if (err) { 217 | console.error("something went wrong! " + err); 218 | } else { 219 | console.dir(results); 220 | } 221 | }); 222 | 223 | // if you want to be notified when the connection gets closed, register a callback function with onClose() 224 | fence.onClose(() => { 225 | console.log("geofence was closed"); 226 | }); 227 | 228 | // later on, when you want to close the socket and kill the live geofence: 229 | fence.close(); 230 | ``` 231 | 232 | Many of the chaining functions below can be used for all search commands. See the [Tile38 documentation](http://tile38.com/commands/#search) 233 | for more info on what query criteria are supported by what commands. 234 | 235 | Do not forget to call the execute() or executeFence() function after constructing your query chain. I've left this out in the 236 | examples below for brevity. 237 | 238 | #### INTERSECTS 239 | 240 | ```javascript 241 | // basic query that uses bounds 242 | client.intersectsQuery('fleet').bounds(33.462, -112.268, 33.491 -112.245) 243 | // intersect with a circle of 1000 meter radius 244 | client.intersectsQuery('fleet').circle(33.462, -112.268, 1000) 245 | // using cursor and limit for pagination 246 | client.intersectsQuery('fleet').cursor(100).limit(50).bounds(33.462, -112.268, 33.491 -112.245) 247 | // create a fence that triggeres when entering a polygon 248 | let polygon = {"type":"Polygon","coordinates": [[[-111.9787,33.4411],[-111.8902,33.4377],[-111.8950,33.2892],[-111.9739,33.2932],[-111.9787,33.4411]]]}; 249 | client.intersectsQuery('fleet').detect('enter','exit').object(polygon) 250 | 251 | ``` 252 | 253 | #### SEARCH 254 | 255 | ```javascript 256 | // basic search query 257 | client.searchQuery('names') 258 | // use matching patter and return results in descending order, without fields 259 | client.searchQuery('names').match('J*').nofields().desc() 260 | // return only IDs 261 | client.searchQuery('names').output('ids') 262 | // this does the same: 263 | client.searchQuery('names').ids() 264 | // return only count 265 | client.searchQuery('names').count() 266 | // use the where option 267 | client.searchQuery('names').where('age', 40, '+inf') 268 | ``` 269 | 270 | #### NEARBY 271 | 272 | ```javascript 273 | // basic nearby query, including distance for each returned object 274 | client.nearbyQuery('fleet').distance().point(33.462, -112.268, 6000) 275 | // return results as geohashes with precision 8 276 | client.nearbyQuery('fleet').point(33.462, -112.268, 6000).hashes(8) 277 | // use the roam option 278 | client.nearbyQuery('fleet').roam('truck', 'ptn', 3000) 279 | client.nearbyQuery('fleet').nodwell().roam('truck', 'ptn', 3000) // with NODWELL option 280 | // use the whereeval option. (unlike the Tile38 CLI no need to specify the number of arguments). 281 | // You may also call whereEval (or whereEvalSha) multiple times in your command chain. 282 | client.nearbyQuery('fleet').whereEval("return FIELDS.wheels > ARGV[1] or (FIELDS.length * FIELDS.width) > ARGV[2]", 8, 120); 283 | ``` 284 | 285 | #### SCAN 286 | 287 | ```javascript 288 | // basic scan query, returning all results in geojson 289 | client.scanQuery('fleet').output('objects'); 290 | // this does the same 291 | client.scanQuery('fleet').objects() 292 | // return simple coordinates, and do not include fields 293 | client.scanQuery('fleet').nofields().points() 294 | ``` 295 | 296 | #### WITHIN 297 | 298 | The withinQuery has the same query options as intersects. 299 | 300 | ```javascript 301 | // basic within query, returning all results in geojson 302 | client.withinQuery('fleet').bounds(33.462, -112.268, 33.491, -112.245) 303 | // check within an area that's already defined in the database 304 | client.withinQuery('fleet').get('cities', 'tempe') 305 | // return objects within a given tile x, y, z 306 | client.withinQuery('fleet').tile(x, y, z); 307 | // return objects within a given circle of 1000 meter radius 308 | client.withinQuery('fleet').circle(33.462, -112.268, 1000) 309 | ``` 310 | 311 | ### using timeouts 312 | 313 | The scan/search commands above can be used with a timeout as well, so the command will be aborted 314 | if a given timeout is exceeded. The timeout function can be called after the initial 315 | query is created (with searchQuery/nearbyQuery etc.) as follows: 316 | 317 | ```javascript 318 | client.scanQuery('mykey').where('foo', 1, 2).count().timeout(0.1); 319 | ``` 320 | 321 | ### Unsupported commands 322 | 323 | It's hard to keep up with development and new features in Tile38. The following commands are not 324 | yet supported in this driver. Pull requests welcome: 325 | 326 | All channel commands (CHANS, DELCHAN, PDELCHAN, PSUBSCRIBE, SETCHAN, SUBSCRIBE) \ 327 | HEALTHZ \ 328 | JGET does not support the RAW option \ 329 | JSET does not support the RAW or STR options \ 330 | All replication commands (AOF, AOFMD5, AOFSHRINK, FOLLOW) \ 331 | All scripting commands (EVAL, EVALNA, EVALNASHA, EVALRO, EVALROSHA, EVALSHA, SCRIPT EXISTS, SCRIPT FLUSH, SCRIPT LOAD) \ 332 | #TODO need to review what else is incomplete. 333 | 334 | Note that even unsupported commands can be called through this library as follows: 335 | 336 | ```javascript 337 | let success = await client.executeCommand('SCRIPT LOAD MYSCRIPT'); 338 | ``` 339 | Using executeCommand you can send any command exactly as documented in the Tile38 command docs. 340 | Most Tile38 commands return a json response with an ok=true property. By default, executeCommand will parse and return (a promise to) the value of this 'ok' property and thus return a boolean true. 341 | 342 | To parse and return a different object from the Tile38 JSON response: 343 | ```javascript 344 | let keys = await client.executeCommand('KEYS *', { returnProp: 'keys'}) 345 | ``` 346 | 347 | or to skip JSON parsing altogether and return the raw server response 348 | ```javascript 349 | let rawResponse = client.executeCommand('KEYS *', { parseJson: false }) 350 | ``` 351 | Note that if the server returns ok=false in any response, this library will reject the Promise. The only exception to this is if you set the parseJson=false property as illustrated above, in which case it will not try to interpret the response in any way. 352 | 353 | 354 | # Running tests 355 | 356 | WARNING: THIS WILL WIPE OUT YOUR DATA! 357 | The test suite currently depends on having a local instance of Tile38 running on the default port 9851. 358 | It tests all supported commands, including FLUSHDB, so you'll LOSE ALL EXISTING DATA in your Tile38 instance. 359 | 360 | To change the Tile38 host name, port number, or configure authentication password, set the 361 | TILE38_HOST, TILE38_PORT and/or TILE38_PASSWD environment variables before running the tests. 362 | 363 | If you have nothing critical in your local db, you can run the tests with: 364 | 365 | ``` 366 | npm test 367 | ``` 368 | 369 | # Project roadmap 370 | 371 | Other potential shortcomings and/or future improvements beyond adding above mentioned commands: 372 | 373 | - The executeFence method / live geofences has had limited testing. Please submit bugs if you run into issues. 374 | - The SETHOOK command has some similarities to the search commands. It's not currently using method chaining but I may rewrite 375 | it so it can be used in a similar way to the other search functions. 376 | 377 | Testing TODO: 378 | - webhooks needs test coverage 379 | - test coverage for live geofences 380 | 381 | # Missing something? Did it break? 382 | 383 | For bugs or feature requests, please open an issue. 384 | -------------------------------------------------------------------------------- /examples/geofence.js: -------------------------------------------------------------------------------- 1 | 2 | const Tile38 = require('../src/tile38'); 3 | const client = new Tile38(); 4 | 5 | 6 | // construct the geofence query 7 | let query = client.intersectsQuery('fleet').detect('enter','exit').bounds(33.462, -112.268, 33.491, -112.245); 8 | // start the live geofence 9 | let fence = query.executeFence((err, results) => { 10 | // this callback will be called multiple times 11 | if (err) { 12 | console.log("error: " + err); 13 | } else { 14 | console.dir(results); 15 | } 16 | }); 17 | 18 | // if you want to be notified when the connection gets closed 19 | fence.onClose(() => { 20 | console.log("geofence was closed"); 21 | }); 22 | 23 | // after 20 seconds, close the geofence again. 24 | setTimeout(() => { 25 | fence.close(); 26 | }, 20000); 27 | -------------------------------------------------------------------------------- /examples/search.js: -------------------------------------------------------------------------------- 1 | 2 | const Tile38 = require('../src/tile38'); 3 | const client = new Tile38({debug: true}); 4 | 5 | 6 | /* 7 | * have 10 users move around randomly inside the map_bounds 8 | */ 9 | map_bounds = [37.6930029, -122.33026355, 37.844208, -122.153541]; 10 | 11 | // return a random coordinate inside given bounds 12 | function randomPointInBounds(swLat, swLon, neLat, neLon) { 13 | let randLat = Math.random() * (neLat - swLat) + swLat; 14 | let randLon = Math.random() * (neLon - swLon) + swLon; 15 | return [randLat, randLon]; 16 | } 17 | let user = 1; 18 | function sendCoords() { 19 | client.set('eastbay', "user" + user, randomPointInBounds(...map_bounds)); 20 | user = user + 1; 21 | if (user == 11) user = 1; 22 | } 23 | setInterval(() => { 24 | sendCoords(); 25 | }, 1000); 26 | 27 | 28 | 29 | 30 | /* 31 | * create a live geofence to receive updates when object enter/exit an area within 2000m radius of a point. 32 | */ 33 | nearPiedmont = client.nearbyQuery('eastbay').detect('enter','exit').output('points').point(37.822357, -122.232007, 2000); 34 | nearPiedmont.executeFence((err, results) => { 35 | if (err) { 36 | console.error(err); 37 | } else { 38 | console.log(results.id + ": " + results.detect + " Piedmond 2k radius"); 39 | // console.dir(results); 40 | } 41 | }); 42 | 43 | 44 | /* 45 | * create a live geofence to find objects that fall within given bounds 46 | */ 47 | withinSomeBounds = client.withinQuery('eastbay').detect('enter','exit').bounds(37.759704,-122.218763, 37.810648, -122.181591); 48 | withinSomeBounds.executeFence((err, results) => { 49 | if (err) { 50 | console.error(err); 51 | } else { 52 | console.log(results.id + ": " + results.detect + " inner bounds area"); 53 | // console.dir(results); 54 | } 55 | }); 56 | 57 | 58 | /* 59 | * create a live geofence to find objects that fall within a given Polygon 60 | */ 61 | let polygon = { type: "Polygon", coordinates: [[ 62 | [-122.3300231682738,37.78158260714663], 63 | [-122.2349811979788,37.75009376613725], 64 | [-122.2246718616854,37.75530988561864], 65 | [-122.2253442927083,37.76667594878754], 66 | [-122.2794064696383,37.79227940644699], 67 | [-122.3308765320844,37.79628967707779], 68 | [-122.3300231682738,37.78158260714663] 69 | ]]}; 70 | withinAlameda = client.withinQuery('eastbay').detect('enter','exit').object(polygon); 71 | withinAlameda.executeFence((err, results) => { 72 | if (err) { 73 | console.error(err); 74 | } else { 75 | console.log(results.id + ": " + results.detect + " Alameda polygon area"); 76 | // console.dir(results); 77 | } 78 | }); 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/set_get.js: -------------------------------------------------------------------------------- 1 | 2 | const Tile38 = require('../src/tile38'); 3 | const client = new Tile38(); 4 | 5 | // set a coordinate 6 | client.set('fleet', 'truck1', [33.5123, -112.2693]); 7 | 8 | // set a coordinate, with completion handler and error handler 9 | client.set('fleet', 'truck2', [33.5211, -112.2710]).then(() => { 10 | console.log("done"); 11 | }).catch(err => { 12 | console.error(err); 13 | }); 14 | 15 | 16 | // retrieve it as geojson object: 17 | client.get('fleet', 'truck1').then(obj => { 18 | // will print: { object: { type: 'Point', coordinates: [ -112.2693, 33.5123 ] } } 19 | console.dir(obj); 20 | }); 21 | 22 | // retrieve it as a simple point 23 | client.getPoint('fleet', 'truck2').then(obj => { 24 | // will print: { point: { lat: 33.5211, lon: -112.271 } } 25 | console.dir(obj); 26 | }); 27 | 28 | 29 | // same as above but adds error handling: 30 | client.getPoint('fleet', 'truck2').then(obj => { 31 | // will print: { point: { lat: 33.5211, lon: -112.271 } } 32 | console.dir(obj); 33 | }).catch(err => { 34 | console.error(err); 35 | }); 36 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tile38", 3 | "version": "0.9.4", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tile38", 9 | "version": "0.9.4", 10 | "license": "ISC", 11 | "dependencies": { 12 | "redis": "^3.0.0", 13 | "redis-parser": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "chai": "^4.3.4", 17 | "mocha": "^9.1.3" 18 | } 19 | }, 20 | "node_modules/@ungap/promise-all-settled": { 21 | "version": "1.1.2", 22 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 23 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 24 | "dev": true 25 | }, 26 | "node_modules/ansi-colors": { 27 | "version": "4.1.1", 28 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 29 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 30 | "dev": true, 31 | "engines": { 32 | "node": ">=6" 33 | } 34 | }, 35 | "node_modules/ansi-regex": { 36 | "version": "5.0.1", 37 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 38 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 39 | "dev": true, 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | }, 44 | "node_modules/ansi-styles": { 45 | "version": "4.3.0", 46 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 47 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 48 | "dev": true, 49 | "dependencies": { 50 | "color-convert": "^2.0.1" 51 | }, 52 | "engines": { 53 | "node": ">=8" 54 | }, 55 | "funding": { 56 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 57 | } 58 | }, 59 | "node_modules/anymatch": { 60 | "version": "3.1.2", 61 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 62 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 63 | "dev": true, 64 | "dependencies": { 65 | "normalize-path": "^3.0.0", 66 | "picomatch": "^2.0.4" 67 | }, 68 | "engines": { 69 | "node": ">= 8" 70 | } 71 | }, 72 | "node_modules/argparse": { 73 | "version": "2.0.1", 74 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 75 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 76 | "dev": true 77 | }, 78 | "node_modules/assertion-error": { 79 | "version": "1.1.0", 80 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 81 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 82 | "dev": true, 83 | "engines": { 84 | "node": "*" 85 | } 86 | }, 87 | "node_modules/balanced-match": { 88 | "version": "1.0.2", 89 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 90 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 91 | "dev": true 92 | }, 93 | "node_modules/binary-extensions": { 94 | "version": "2.2.0", 95 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 96 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 97 | "dev": true, 98 | "engines": { 99 | "node": ">=8" 100 | } 101 | }, 102 | "node_modules/brace-expansion": { 103 | "version": "1.1.11", 104 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 105 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 106 | "dev": true, 107 | "dependencies": { 108 | "balanced-match": "^1.0.0", 109 | "concat-map": "0.0.1" 110 | } 111 | }, 112 | "node_modules/braces": { 113 | "version": "3.0.2", 114 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 115 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 116 | "dev": true, 117 | "dependencies": { 118 | "fill-range": "^7.0.1" 119 | }, 120 | "engines": { 121 | "node": ">=8" 122 | } 123 | }, 124 | "node_modules/browser-stdout": { 125 | "version": "1.3.1", 126 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 127 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 128 | "dev": true 129 | }, 130 | "node_modules/camelcase": { 131 | "version": "6.3.0", 132 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 133 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 134 | "dev": true, 135 | "engines": { 136 | "node": ">=10" 137 | }, 138 | "funding": { 139 | "url": "https://github.com/sponsors/sindresorhus" 140 | } 141 | }, 142 | "node_modules/chai": { 143 | "version": "4.3.6", 144 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", 145 | "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", 146 | "dev": true, 147 | "dependencies": { 148 | "assertion-error": "^1.1.0", 149 | "check-error": "^1.0.2", 150 | "deep-eql": "^3.0.1", 151 | "get-func-name": "^2.0.0", 152 | "loupe": "^2.3.1", 153 | "pathval": "^1.1.1", 154 | "type-detect": "^4.0.5" 155 | }, 156 | "engines": { 157 | "node": ">=4" 158 | } 159 | }, 160 | "node_modules/chalk": { 161 | "version": "4.1.2", 162 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 163 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 164 | "dev": true, 165 | "dependencies": { 166 | "ansi-styles": "^4.1.0", 167 | "supports-color": "^7.1.0" 168 | }, 169 | "engines": { 170 | "node": ">=10" 171 | }, 172 | "funding": { 173 | "url": "https://github.com/chalk/chalk?sponsor=1" 174 | } 175 | }, 176 | "node_modules/chalk/node_modules/supports-color": { 177 | "version": "7.2.0", 178 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 179 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 180 | "dev": true, 181 | "dependencies": { 182 | "has-flag": "^4.0.0" 183 | }, 184 | "engines": { 185 | "node": ">=8" 186 | } 187 | }, 188 | "node_modules/check-error": { 189 | "version": "1.0.2", 190 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 191 | "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", 192 | "dev": true, 193 | "engines": { 194 | "node": "*" 195 | } 196 | }, 197 | "node_modules/chokidar": { 198 | "version": "3.5.3", 199 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 200 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 201 | "dev": true, 202 | "funding": [ 203 | { 204 | "type": "individual", 205 | "url": "https://paulmillr.com/funding/" 206 | } 207 | ], 208 | "dependencies": { 209 | "anymatch": "~3.1.2", 210 | "braces": "~3.0.2", 211 | "glob-parent": "~5.1.2", 212 | "is-binary-path": "~2.1.0", 213 | "is-glob": "~4.0.1", 214 | "normalize-path": "~3.0.0", 215 | "readdirp": "~3.6.0" 216 | }, 217 | "engines": { 218 | "node": ">= 8.10.0" 219 | }, 220 | "optionalDependencies": { 221 | "fsevents": "~2.3.2" 222 | } 223 | }, 224 | "node_modules/cliui": { 225 | "version": "7.0.4", 226 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 227 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 228 | "dev": true, 229 | "dependencies": { 230 | "string-width": "^4.2.0", 231 | "strip-ansi": "^6.0.0", 232 | "wrap-ansi": "^7.0.0" 233 | } 234 | }, 235 | "node_modules/color-convert": { 236 | "version": "2.0.1", 237 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 238 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 239 | "dev": true, 240 | "dependencies": { 241 | "color-name": "~1.1.4" 242 | }, 243 | "engines": { 244 | "node": ">=7.0.0" 245 | } 246 | }, 247 | "node_modules/color-name": { 248 | "version": "1.1.4", 249 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 250 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 251 | "dev": true 252 | }, 253 | "node_modules/concat-map": { 254 | "version": "0.0.1", 255 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 256 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 257 | "dev": true 258 | }, 259 | "node_modules/debug": { 260 | "version": "4.3.3", 261 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 262 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 263 | "dev": true, 264 | "dependencies": { 265 | "ms": "2.1.2" 266 | }, 267 | "engines": { 268 | "node": ">=6.0" 269 | }, 270 | "peerDependenciesMeta": { 271 | "supports-color": { 272 | "optional": true 273 | } 274 | } 275 | }, 276 | "node_modules/debug/node_modules/ms": { 277 | "version": "2.1.2", 278 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 279 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 280 | "dev": true 281 | }, 282 | "node_modules/decamelize": { 283 | "version": "4.0.0", 284 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 285 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 286 | "dev": true, 287 | "engines": { 288 | "node": ">=10" 289 | }, 290 | "funding": { 291 | "url": "https://github.com/sponsors/sindresorhus" 292 | } 293 | }, 294 | "node_modules/deep-eql": { 295 | "version": "3.0.1", 296 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 297 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 298 | "dev": true, 299 | "dependencies": { 300 | "type-detect": "^4.0.0" 301 | }, 302 | "engines": { 303 | "node": ">=0.12" 304 | } 305 | }, 306 | "node_modules/denque": { 307 | "version": "1.5.1", 308 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", 309 | "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", 310 | "engines": { 311 | "node": ">=0.10" 312 | } 313 | }, 314 | "node_modules/diff": { 315 | "version": "5.0.0", 316 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 317 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 318 | "dev": true, 319 | "engines": { 320 | "node": ">=0.3.1" 321 | } 322 | }, 323 | "node_modules/emoji-regex": { 324 | "version": "8.0.0", 325 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 326 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 327 | "dev": true 328 | }, 329 | "node_modules/escalade": { 330 | "version": "3.1.1", 331 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 332 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 333 | "dev": true, 334 | "engines": { 335 | "node": ">=6" 336 | } 337 | }, 338 | "node_modules/escape-string-regexp": { 339 | "version": "4.0.0", 340 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 341 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 342 | "dev": true, 343 | "engines": { 344 | "node": ">=10" 345 | }, 346 | "funding": { 347 | "url": "https://github.com/sponsors/sindresorhus" 348 | } 349 | }, 350 | "node_modules/fill-range": { 351 | "version": "7.0.1", 352 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 353 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 354 | "dev": true, 355 | "dependencies": { 356 | "to-regex-range": "^5.0.1" 357 | }, 358 | "engines": { 359 | "node": ">=8" 360 | } 361 | }, 362 | "node_modules/find-up": { 363 | "version": "5.0.0", 364 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 365 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 366 | "dev": true, 367 | "dependencies": { 368 | "locate-path": "^6.0.0", 369 | "path-exists": "^4.0.0" 370 | }, 371 | "engines": { 372 | "node": ">=10" 373 | }, 374 | "funding": { 375 | "url": "https://github.com/sponsors/sindresorhus" 376 | } 377 | }, 378 | "node_modules/flat": { 379 | "version": "5.0.2", 380 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 381 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 382 | "dev": true, 383 | "bin": { 384 | "flat": "cli.js" 385 | } 386 | }, 387 | "node_modules/fs.realpath": { 388 | "version": "1.0.0", 389 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 390 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 391 | "dev": true 392 | }, 393 | "node_modules/fsevents": { 394 | "version": "2.3.2", 395 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 396 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 397 | "dev": true, 398 | "hasInstallScript": true, 399 | "optional": true, 400 | "os": [ 401 | "darwin" 402 | ], 403 | "engines": { 404 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 405 | } 406 | }, 407 | "node_modules/get-caller-file": { 408 | "version": "2.0.5", 409 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 410 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 411 | "dev": true, 412 | "engines": { 413 | "node": "6.* || 8.* || >= 10.*" 414 | } 415 | }, 416 | "node_modules/get-func-name": { 417 | "version": "2.0.0", 418 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 419 | "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", 420 | "dev": true, 421 | "engines": { 422 | "node": "*" 423 | } 424 | }, 425 | "node_modules/glob": { 426 | "version": "7.2.0", 427 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 428 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 429 | "dev": true, 430 | "dependencies": { 431 | "fs.realpath": "^1.0.0", 432 | "inflight": "^1.0.4", 433 | "inherits": "2", 434 | "minimatch": "^3.0.4", 435 | "once": "^1.3.0", 436 | "path-is-absolute": "^1.0.0" 437 | }, 438 | "engines": { 439 | "node": "*" 440 | }, 441 | "funding": { 442 | "url": "https://github.com/sponsors/isaacs" 443 | } 444 | }, 445 | "node_modules/glob-parent": { 446 | "version": "5.1.2", 447 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 448 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 449 | "dev": true, 450 | "dependencies": { 451 | "is-glob": "^4.0.1" 452 | }, 453 | "engines": { 454 | "node": ">= 6" 455 | } 456 | }, 457 | "node_modules/glob/node_modules/minimatch": { 458 | "version": "3.1.2", 459 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 460 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 461 | "dev": true, 462 | "dependencies": { 463 | "brace-expansion": "^1.1.7" 464 | }, 465 | "engines": { 466 | "node": "*" 467 | } 468 | }, 469 | "node_modules/growl": { 470 | "version": "1.10.5", 471 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 472 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 473 | "dev": true, 474 | "engines": { 475 | "node": ">=4.x" 476 | } 477 | }, 478 | "node_modules/has-flag": { 479 | "version": "4.0.0", 480 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 481 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 482 | "dev": true, 483 | "engines": { 484 | "node": ">=8" 485 | } 486 | }, 487 | "node_modules/he": { 488 | "version": "1.2.0", 489 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 490 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 491 | "dev": true, 492 | "bin": { 493 | "he": "bin/he" 494 | } 495 | }, 496 | "node_modules/inflight": { 497 | "version": "1.0.6", 498 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 499 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 500 | "dev": true, 501 | "dependencies": { 502 | "once": "^1.3.0", 503 | "wrappy": "1" 504 | } 505 | }, 506 | "node_modules/inherits": { 507 | "version": "2.0.4", 508 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 509 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 510 | "dev": true 511 | }, 512 | "node_modules/is-binary-path": { 513 | "version": "2.1.0", 514 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 515 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 516 | "dev": true, 517 | "dependencies": { 518 | "binary-extensions": "^2.0.0" 519 | }, 520 | "engines": { 521 | "node": ">=8" 522 | } 523 | }, 524 | "node_modules/is-extglob": { 525 | "version": "2.1.1", 526 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 527 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 528 | "dev": true, 529 | "engines": { 530 | "node": ">=0.10.0" 531 | } 532 | }, 533 | "node_modules/is-fullwidth-code-point": { 534 | "version": "3.0.0", 535 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 536 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 537 | "dev": true, 538 | "engines": { 539 | "node": ">=8" 540 | } 541 | }, 542 | "node_modules/is-glob": { 543 | "version": "4.0.3", 544 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 545 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 546 | "dev": true, 547 | "dependencies": { 548 | "is-extglob": "^2.1.1" 549 | }, 550 | "engines": { 551 | "node": ">=0.10.0" 552 | } 553 | }, 554 | "node_modules/is-number": { 555 | "version": "7.0.0", 556 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 557 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 558 | "dev": true, 559 | "engines": { 560 | "node": ">=0.12.0" 561 | } 562 | }, 563 | "node_modules/is-plain-obj": { 564 | "version": "2.1.0", 565 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 566 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 567 | "dev": true, 568 | "engines": { 569 | "node": ">=8" 570 | } 571 | }, 572 | "node_modules/is-unicode-supported": { 573 | "version": "0.1.0", 574 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 575 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 576 | "dev": true, 577 | "engines": { 578 | "node": ">=10" 579 | }, 580 | "funding": { 581 | "url": "https://github.com/sponsors/sindresorhus" 582 | } 583 | }, 584 | "node_modules/isexe": { 585 | "version": "2.0.0", 586 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 587 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 588 | "dev": true 589 | }, 590 | "node_modules/js-yaml": { 591 | "version": "4.1.0", 592 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 593 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 594 | "dev": true, 595 | "dependencies": { 596 | "argparse": "^2.0.1" 597 | }, 598 | "bin": { 599 | "js-yaml": "bin/js-yaml.js" 600 | } 601 | }, 602 | "node_modules/locate-path": { 603 | "version": "6.0.0", 604 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 605 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 606 | "dev": true, 607 | "dependencies": { 608 | "p-locate": "^5.0.0" 609 | }, 610 | "engines": { 611 | "node": ">=10" 612 | }, 613 | "funding": { 614 | "url": "https://github.com/sponsors/sindresorhus" 615 | } 616 | }, 617 | "node_modules/log-symbols": { 618 | "version": "4.1.0", 619 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 620 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 621 | "dev": true, 622 | "dependencies": { 623 | "chalk": "^4.1.0", 624 | "is-unicode-supported": "^0.1.0" 625 | }, 626 | "engines": { 627 | "node": ">=10" 628 | }, 629 | "funding": { 630 | "url": "https://github.com/sponsors/sindresorhus" 631 | } 632 | }, 633 | "node_modules/loupe": { 634 | "version": "2.3.4", 635 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", 636 | "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", 637 | "dev": true, 638 | "dependencies": { 639 | "get-func-name": "^2.0.0" 640 | } 641 | }, 642 | "node_modules/minimatch": { 643 | "version": "4.2.1", 644 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", 645 | "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", 646 | "dev": true, 647 | "dependencies": { 648 | "brace-expansion": "^1.1.7" 649 | }, 650 | "engines": { 651 | "node": ">=10" 652 | } 653 | }, 654 | "node_modules/mocha": { 655 | "version": "9.2.2", 656 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", 657 | "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", 658 | "dev": true, 659 | "dependencies": { 660 | "@ungap/promise-all-settled": "1.1.2", 661 | "ansi-colors": "4.1.1", 662 | "browser-stdout": "1.3.1", 663 | "chokidar": "3.5.3", 664 | "debug": "4.3.3", 665 | "diff": "5.0.0", 666 | "escape-string-regexp": "4.0.0", 667 | "find-up": "5.0.0", 668 | "glob": "7.2.0", 669 | "growl": "1.10.5", 670 | "he": "1.2.0", 671 | "js-yaml": "4.1.0", 672 | "log-symbols": "4.1.0", 673 | "minimatch": "4.2.1", 674 | "ms": "2.1.3", 675 | "nanoid": "3.3.1", 676 | "serialize-javascript": "6.0.0", 677 | "strip-json-comments": "3.1.1", 678 | "supports-color": "8.1.1", 679 | "which": "2.0.2", 680 | "workerpool": "6.2.0", 681 | "yargs": "16.2.0", 682 | "yargs-parser": "20.2.4", 683 | "yargs-unparser": "2.0.0" 684 | }, 685 | "bin": { 686 | "_mocha": "bin/_mocha", 687 | "mocha": "bin/mocha" 688 | }, 689 | "engines": { 690 | "node": ">= 12.0.0" 691 | }, 692 | "funding": { 693 | "type": "opencollective", 694 | "url": "https://opencollective.com/mochajs" 695 | } 696 | }, 697 | "node_modules/ms": { 698 | "version": "2.1.3", 699 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 700 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 701 | "dev": true 702 | }, 703 | "node_modules/nanoid": { 704 | "version": "3.3.1", 705 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", 706 | "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", 707 | "dev": true, 708 | "bin": { 709 | "nanoid": "bin/nanoid.cjs" 710 | }, 711 | "engines": { 712 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 713 | } 714 | }, 715 | "node_modules/normalize-path": { 716 | "version": "3.0.0", 717 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 718 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 719 | "dev": true, 720 | "engines": { 721 | "node": ">=0.10.0" 722 | } 723 | }, 724 | "node_modules/once": { 725 | "version": "1.4.0", 726 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 727 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 728 | "dev": true, 729 | "dependencies": { 730 | "wrappy": "1" 731 | } 732 | }, 733 | "node_modules/p-limit": { 734 | "version": "3.1.0", 735 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 736 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 737 | "dev": true, 738 | "dependencies": { 739 | "yocto-queue": "^0.1.0" 740 | }, 741 | "engines": { 742 | "node": ">=10" 743 | }, 744 | "funding": { 745 | "url": "https://github.com/sponsors/sindresorhus" 746 | } 747 | }, 748 | "node_modules/p-locate": { 749 | "version": "5.0.0", 750 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 751 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 752 | "dev": true, 753 | "dependencies": { 754 | "p-limit": "^3.0.2" 755 | }, 756 | "engines": { 757 | "node": ">=10" 758 | }, 759 | "funding": { 760 | "url": "https://github.com/sponsors/sindresorhus" 761 | } 762 | }, 763 | "node_modules/path-exists": { 764 | "version": "4.0.0", 765 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 766 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 767 | "dev": true, 768 | "engines": { 769 | "node": ">=8" 770 | } 771 | }, 772 | "node_modules/path-is-absolute": { 773 | "version": "1.0.1", 774 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 775 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 776 | "dev": true, 777 | "engines": { 778 | "node": ">=0.10.0" 779 | } 780 | }, 781 | "node_modules/pathval": { 782 | "version": "1.1.1", 783 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 784 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 785 | "dev": true, 786 | "engines": { 787 | "node": "*" 788 | } 789 | }, 790 | "node_modules/picomatch": { 791 | "version": "2.3.1", 792 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 793 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 794 | "dev": true, 795 | "engines": { 796 | "node": ">=8.6" 797 | }, 798 | "funding": { 799 | "url": "https://github.com/sponsors/jonschlinkert" 800 | } 801 | }, 802 | "node_modules/randombytes": { 803 | "version": "2.1.0", 804 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 805 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 806 | "dev": true, 807 | "dependencies": { 808 | "safe-buffer": "^5.1.0" 809 | } 810 | }, 811 | "node_modules/readdirp": { 812 | "version": "3.6.0", 813 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 814 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 815 | "dev": true, 816 | "dependencies": { 817 | "picomatch": "^2.2.1" 818 | }, 819 | "engines": { 820 | "node": ">=8.10.0" 821 | } 822 | }, 823 | "node_modules/redis": { 824 | "version": "3.1.2", 825 | "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", 826 | "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", 827 | "dependencies": { 828 | "denque": "^1.5.0", 829 | "redis-commands": "^1.7.0", 830 | "redis-errors": "^1.2.0", 831 | "redis-parser": "^3.0.0" 832 | }, 833 | "engines": { 834 | "node": ">=10" 835 | }, 836 | "funding": { 837 | "type": "opencollective", 838 | "url": "https://opencollective.com/node-redis" 839 | } 840 | }, 841 | "node_modules/redis-commands": { 842 | "version": "1.7.0", 843 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", 844 | "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" 845 | }, 846 | "node_modules/redis-errors": { 847 | "version": "1.2.0", 848 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 849 | "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", 850 | "engines": { 851 | "node": ">=4" 852 | } 853 | }, 854 | "node_modules/redis-parser": { 855 | "version": "3.0.0", 856 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 857 | "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", 858 | "dependencies": { 859 | "redis-errors": "^1.0.0" 860 | }, 861 | "engines": { 862 | "node": ">=4" 863 | } 864 | }, 865 | "node_modules/require-directory": { 866 | "version": "2.1.1", 867 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 868 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 869 | "dev": true, 870 | "engines": { 871 | "node": ">=0.10.0" 872 | } 873 | }, 874 | "node_modules/safe-buffer": { 875 | "version": "5.2.1", 876 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 877 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 878 | "dev": true, 879 | "funding": [ 880 | { 881 | "type": "github", 882 | "url": "https://github.com/sponsors/feross" 883 | }, 884 | { 885 | "type": "patreon", 886 | "url": "https://www.patreon.com/feross" 887 | }, 888 | { 889 | "type": "consulting", 890 | "url": "https://feross.org/support" 891 | } 892 | ] 893 | }, 894 | "node_modules/serialize-javascript": { 895 | "version": "6.0.0", 896 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 897 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 898 | "dev": true, 899 | "dependencies": { 900 | "randombytes": "^2.1.0" 901 | } 902 | }, 903 | "node_modules/string-width": { 904 | "version": "4.2.3", 905 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 906 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 907 | "dev": true, 908 | "dependencies": { 909 | "emoji-regex": "^8.0.0", 910 | "is-fullwidth-code-point": "^3.0.0", 911 | "strip-ansi": "^6.0.1" 912 | }, 913 | "engines": { 914 | "node": ">=8" 915 | } 916 | }, 917 | "node_modules/strip-ansi": { 918 | "version": "6.0.1", 919 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 920 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 921 | "dev": true, 922 | "dependencies": { 923 | "ansi-regex": "^5.0.1" 924 | }, 925 | "engines": { 926 | "node": ">=8" 927 | } 928 | }, 929 | "node_modules/strip-json-comments": { 930 | "version": "3.1.1", 931 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 932 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 933 | "dev": true, 934 | "engines": { 935 | "node": ">=8" 936 | }, 937 | "funding": { 938 | "url": "https://github.com/sponsors/sindresorhus" 939 | } 940 | }, 941 | "node_modules/supports-color": { 942 | "version": "8.1.1", 943 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 944 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 945 | "dev": true, 946 | "dependencies": { 947 | "has-flag": "^4.0.0" 948 | }, 949 | "engines": { 950 | "node": ">=10" 951 | }, 952 | "funding": { 953 | "url": "https://github.com/chalk/supports-color?sponsor=1" 954 | } 955 | }, 956 | "node_modules/to-regex-range": { 957 | "version": "5.0.1", 958 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 959 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 960 | "dev": true, 961 | "dependencies": { 962 | "is-number": "^7.0.0" 963 | }, 964 | "engines": { 965 | "node": ">=8.0" 966 | } 967 | }, 968 | "node_modules/type-detect": { 969 | "version": "4.0.8", 970 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 971 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 972 | "dev": true, 973 | "engines": { 974 | "node": ">=4" 975 | } 976 | }, 977 | "node_modules/which": { 978 | "version": "2.0.2", 979 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 980 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 981 | "dev": true, 982 | "dependencies": { 983 | "isexe": "^2.0.0" 984 | }, 985 | "bin": { 986 | "node-which": "bin/node-which" 987 | }, 988 | "engines": { 989 | "node": ">= 8" 990 | } 991 | }, 992 | "node_modules/workerpool": { 993 | "version": "6.2.0", 994 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", 995 | "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", 996 | "dev": true 997 | }, 998 | "node_modules/wrap-ansi": { 999 | "version": "7.0.0", 1000 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1001 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1002 | "dev": true, 1003 | "dependencies": { 1004 | "ansi-styles": "^4.0.0", 1005 | "string-width": "^4.1.0", 1006 | "strip-ansi": "^6.0.0" 1007 | }, 1008 | "engines": { 1009 | "node": ">=10" 1010 | }, 1011 | "funding": { 1012 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1013 | } 1014 | }, 1015 | "node_modules/wrappy": { 1016 | "version": "1.0.2", 1017 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1018 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1019 | "dev": true 1020 | }, 1021 | "node_modules/y18n": { 1022 | "version": "5.0.8", 1023 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1024 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1025 | "dev": true, 1026 | "engines": { 1027 | "node": ">=10" 1028 | } 1029 | }, 1030 | "node_modules/yargs": { 1031 | "version": "16.2.0", 1032 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1033 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1034 | "dev": true, 1035 | "dependencies": { 1036 | "cliui": "^7.0.2", 1037 | "escalade": "^3.1.1", 1038 | "get-caller-file": "^2.0.5", 1039 | "require-directory": "^2.1.1", 1040 | "string-width": "^4.2.0", 1041 | "y18n": "^5.0.5", 1042 | "yargs-parser": "^20.2.2" 1043 | }, 1044 | "engines": { 1045 | "node": ">=10" 1046 | } 1047 | }, 1048 | "node_modules/yargs-parser": { 1049 | "version": "20.2.4", 1050 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1051 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1052 | "dev": true, 1053 | "engines": { 1054 | "node": ">=10" 1055 | } 1056 | }, 1057 | "node_modules/yargs-unparser": { 1058 | "version": "2.0.0", 1059 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1060 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1061 | "dev": true, 1062 | "dependencies": { 1063 | "camelcase": "^6.0.0", 1064 | "decamelize": "^4.0.0", 1065 | "flat": "^5.0.2", 1066 | "is-plain-obj": "^2.1.0" 1067 | }, 1068 | "engines": { 1069 | "node": ">=10" 1070 | } 1071 | }, 1072 | "node_modules/yocto-queue": { 1073 | "version": "0.1.0", 1074 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1075 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1076 | "dev": true, 1077 | "engines": { 1078 | "node": ">=10" 1079 | }, 1080 | "funding": { 1081 | "url": "https://github.com/sponsors/sindresorhus" 1082 | } 1083 | } 1084 | }, 1085 | "dependencies": { 1086 | "@ungap/promise-all-settled": { 1087 | "version": "1.1.2", 1088 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 1089 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 1090 | "dev": true 1091 | }, 1092 | "ansi-colors": { 1093 | "version": "4.1.1", 1094 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 1095 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 1096 | "dev": true 1097 | }, 1098 | "ansi-regex": { 1099 | "version": "5.0.1", 1100 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1101 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1102 | "dev": true 1103 | }, 1104 | "ansi-styles": { 1105 | "version": "4.3.0", 1106 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1107 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1108 | "dev": true, 1109 | "requires": { 1110 | "color-convert": "^2.0.1" 1111 | } 1112 | }, 1113 | "anymatch": { 1114 | "version": "3.1.2", 1115 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 1116 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 1117 | "dev": true, 1118 | "requires": { 1119 | "normalize-path": "^3.0.0", 1120 | "picomatch": "^2.0.4" 1121 | } 1122 | }, 1123 | "argparse": { 1124 | "version": "2.0.1", 1125 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1126 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1127 | "dev": true 1128 | }, 1129 | "assertion-error": { 1130 | "version": "1.1.0", 1131 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 1132 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 1133 | "dev": true 1134 | }, 1135 | "balanced-match": { 1136 | "version": "1.0.2", 1137 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1138 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1139 | "dev": true 1140 | }, 1141 | "binary-extensions": { 1142 | "version": "2.2.0", 1143 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 1144 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 1145 | "dev": true 1146 | }, 1147 | "brace-expansion": { 1148 | "version": "1.1.11", 1149 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1150 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1151 | "dev": true, 1152 | "requires": { 1153 | "balanced-match": "^1.0.0", 1154 | "concat-map": "0.0.1" 1155 | } 1156 | }, 1157 | "braces": { 1158 | "version": "3.0.2", 1159 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 1160 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 1161 | "dev": true, 1162 | "requires": { 1163 | "fill-range": "^7.0.1" 1164 | } 1165 | }, 1166 | "browser-stdout": { 1167 | "version": "1.3.1", 1168 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 1169 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 1170 | "dev": true 1171 | }, 1172 | "camelcase": { 1173 | "version": "6.3.0", 1174 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 1175 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 1176 | "dev": true 1177 | }, 1178 | "chai": { 1179 | "version": "4.3.6", 1180 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", 1181 | "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", 1182 | "dev": true, 1183 | "requires": { 1184 | "assertion-error": "^1.1.0", 1185 | "check-error": "^1.0.2", 1186 | "deep-eql": "^3.0.1", 1187 | "get-func-name": "^2.0.0", 1188 | "loupe": "^2.3.1", 1189 | "pathval": "^1.1.1", 1190 | "type-detect": "^4.0.5" 1191 | } 1192 | }, 1193 | "chalk": { 1194 | "version": "4.1.2", 1195 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1196 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1197 | "dev": true, 1198 | "requires": { 1199 | "ansi-styles": "^4.1.0", 1200 | "supports-color": "^7.1.0" 1201 | }, 1202 | "dependencies": { 1203 | "supports-color": { 1204 | "version": "7.2.0", 1205 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1206 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1207 | "dev": true, 1208 | "requires": { 1209 | "has-flag": "^4.0.0" 1210 | } 1211 | } 1212 | } 1213 | }, 1214 | "check-error": { 1215 | "version": "1.0.2", 1216 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 1217 | "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", 1218 | "dev": true 1219 | }, 1220 | "chokidar": { 1221 | "version": "3.5.3", 1222 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 1223 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 1224 | "dev": true, 1225 | "requires": { 1226 | "anymatch": "~3.1.2", 1227 | "braces": "~3.0.2", 1228 | "fsevents": "~2.3.2", 1229 | "glob-parent": "~5.1.2", 1230 | "is-binary-path": "~2.1.0", 1231 | "is-glob": "~4.0.1", 1232 | "normalize-path": "~3.0.0", 1233 | "readdirp": "~3.6.0" 1234 | } 1235 | }, 1236 | "cliui": { 1237 | "version": "7.0.4", 1238 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 1239 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 1240 | "dev": true, 1241 | "requires": { 1242 | "string-width": "^4.2.0", 1243 | "strip-ansi": "^6.0.0", 1244 | "wrap-ansi": "^7.0.0" 1245 | } 1246 | }, 1247 | "color-convert": { 1248 | "version": "2.0.1", 1249 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1250 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1251 | "dev": true, 1252 | "requires": { 1253 | "color-name": "~1.1.4" 1254 | } 1255 | }, 1256 | "color-name": { 1257 | "version": "1.1.4", 1258 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1259 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1260 | "dev": true 1261 | }, 1262 | "concat-map": { 1263 | "version": "0.0.1", 1264 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1265 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1266 | "dev": true 1267 | }, 1268 | "debug": { 1269 | "version": "4.3.3", 1270 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 1271 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 1272 | "dev": true, 1273 | "requires": { 1274 | "ms": "2.1.2" 1275 | }, 1276 | "dependencies": { 1277 | "ms": { 1278 | "version": "2.1.2", 1279 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1280 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1281 | "dev": true 1282 | } 1283 | } 1284 | }, 1285 | "decamelize": { 1286 | "version": "4.0.0", 1287 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 1288 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 1289 | "dev": true 1290 | }, 1291 | "deep-eql": { 1292 | "version": "3.0.1", 1293 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 1294 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 1295 | "dev": true, 1296 | "requires": { 1297 | "type-detect": "^4.0.0" 1298 | } 1299 | }, 1300 | "denque": { 1301 | "version": "1.5.1", 1302 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", 1303 | "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" 1304 | }, 1305 | "diff": { 1306 | "version": "5.0.0", 1307 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 1308 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 1309 | "dev": true 1310 | }, 1311 | "emoji-regex": { 1312 | "version": "8.0.0", 1313 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1314 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1315 | "dev": true 1316 | }, 1317 | "escalade": { 1318 | "version": "3.1.1", 1319 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1320 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 1321 | "dev": true 1322 | }, 1323 | "escape-string-regexp": { 1324 | "version": "4.0.0", 1325 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1326 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1327 | "dev": true 1328 | }, 1329 | "fill-range": { 1330 | "version": "7.0.1", 1331 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 1332 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 1333 | "dev": true, 1334 | "requires": { 1335 | "to-regex-range": "^5.0.1" 1336 | } 1337 | }, 1338 | "find-up": { 1339 | "version": "5.0.0", 1340 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1341 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1342 | "dev": true, 1343 | "requires": { 1344 | "locate-path": "^6.0.0", 1345 | "path-exists": "^4.0.0" 1346 | } 1347 | }, 1348 | "flat": { 1349 | "version": "5.0.2", 1350 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 1351 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 1352 | "dev": true 1353 | }, 1354 | "fs.realpath": { 1355 | "version": "1.0.0", 1356 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1357 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 1358 | "dev": true 1359 | }, 1360 | "fsevents": { 1361 | "version": "2.3.2", 1362 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1363 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1364 | "dev": true, 1365 | "optional": true 1366 | }, 1367 | "get-caller-file": { 1368 | "version": "2.0.5", 1369 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1370 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 1371 | "dev": true 1372 | }, 1373 | "get-func-name": { 1374 | "version": "2.0.0", 1375 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 1376 | "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", 1377 | "dev": true 1378 | }, 1379 | "glob": { 1380 | "version": "7.2.0", 1381 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 1382 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 1383 | "dev": true, 1384 | "requires": { 1385 | "fs.realpath": "^1.0.0", 1386 | "inflight": "^1.0.4", 1387 | "inherits": "2", 1388 | "minimatch": "^3.0.4", 1389 | "once": "^1.3.0", 1390 | "path-is-absolute": "^1.0.0" 1391 | }, 1392 | "dependencies": { 1393 | "minimatch": { 1394 | "version": "3.1.2", 1395 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1396 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1397 | "dev": true, 1398 | "requires": { 1399 | "brace-expansion": "^1.1.7" 1400 | } 1401 | } 1402 | } 1403 | }, 1404 | "glob-parent": { 1405 | "version": "5.1.2", 1406 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1407 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1408 | "dev": true, 1409 | "requires": { 1410 | "is-glob": "^4.0.1" 1411 | } 1412 | }, 1413 | "growl": { 1414 | "version": "1.10.5", 1415 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 1416 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 1417 | "dev": true 1418 | }, 1419 | "has-flag": { 1420 | "version": "4.0.0", 1421 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1422 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1423 | "dev": true 1424 | }, 1425 | "he": { 1426 | "version": "1.2.0", 1427 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1428 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1429 | "dev": true 1430 | }, 1431 | "inflight": { 1432 | "version": "1.0.6", 1433 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1434 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1435 | "dev": true, 1436 | "requires": { 1437 | "once": "^1.3.0", 1438 | "wrappy": "1" 1439 | } 1440 | }, 1441 | "inherits": { 1442 | "version": "2.0.4", 1443 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1444 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1445 | "dev": true 1446 | }, 1447 | "is-binary-path": { 1448 | "version": "2.1.0", 1449 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1450 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1451 | "dev": true, 1452 | "requires": { 1453 | "binary-extensions": "^2.0.0" 1454 | } 1455 | }, 1456 | "is-extglob": { 1457 | "version": "2.1.1", 1458 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1459 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1460 | "dev": true 1461 | }, 1462 | "is-fullwidth-code-point": { 1463 | "version": "3.0.0", 1464 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1465 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1466 | "dev": true 1467 | }, 1468 | "is-glob": { 1469 | "version": "4.0.3", 1470 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1471 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1472 | "dev": true, 1473 | "requires": { 1474 | "is-extglob": "^2.1.1" 1475 | } 1476 | }, 1477 | "is-number": { 1478 | "version": "7.0.0", 1479 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1480 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1481 | "dev": true 1482 | }, 1483 | "is-plain-obj": { 1484 | "version": "2.1.0", 1485 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 1486 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 1487 | "dev": true 1488 | }, 1489 | "is-unicode-supported": { 1490 | "version": "0.1.0", 1491 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1492 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1493 | "dev": true 1494 | }, 1495 | "isexe": { 1496 | "version": "2.0.0", 1497 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1498 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1499 | "dev": true 1500 | }, 1501 | "js-yaml": { 1502 | "version": "4.1.0", 1503 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1504 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1505 | "dev": true, 1506 | "requires": { 1507 | "argparse": "^2.0.1" 1508 | } 1509 | }, 1510 | "locate-path": { 1511 | "version": "6.0.0", 1512 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1513 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1514 | "dev": true, 1515 | "requires": { 1516 | "p-locate": "^5.0.0" 1517 | } 1518 | }, 1519 | "log-symbols": { 1520 | "version": "4.1.0", 1521 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1522 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1523 | "dev": true, 1524 | "requires": { 1525 | "chalk": "^4.1.0", 1526 | "is-unicode-supported": "^0.1.0" 1527 | } 1528 | }, 1529 | "loupe": { 1530 | "version": "2.3.4", 1531 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", 1532 | "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", 1533 | "dev": true, 1534 | "requires": { 1535 | "get-func-name": "^2.0.0" 1536 | } 1537 | }, 1538 | "minimatch": { 1539 | "version": "4.2.1", 1540 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", 1541 | "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", 1542 | "dev": true, 1543 | "requires": { 1544 | "brace-expansion": "^1.1.7" 1545 | } 1546 | }, 1547 | "mocha": { 1548 | "version": "9.2.2", 1549 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", 1550 | "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", 1551 | "dev": true, 1552 | "requires": { 1553 | "@ungap/promise-all-settled": "1.1.2", 1554 | "ansi-colors": "4.1.1", 1555 | "browser-stdout": "1.3.1", 1556 | "chokidar": "3.5.3", 1557 | "debug": "4.3.3", 1558 | "diff": "5.0.0", 1559 | "escape-string-regexp": "4.0.0", 1560 | "find-up": "5.0.0", 1561 | "glob": "7.2.0", 1562 | "growl": "1.10.5", 1563 | "he": "1.2.0", 1564 | "js-yaml": "4.1.0", 1565 | "log-symbols": "4.1.0", 1566 | "minimatch": "4.2.1", 1567 | "ms": "2.1.3", 1568 | "nanoid": "3.3.1", 1569 | "serialize-javascript": "6.0.0", 1570 | "strip-json-comments": "3.1.1", 1571 | "supports-color": "8.1.1", 1572 | "which": "2.0.2", 1573 | "workerpool": "6.2.0", 1574 | "yargs": "16.2.0", 1575 | "yargs-parser": "20.2.4", 1576 | "yargs-unparser": "2.0.0" 1577 | } 1578 | }, 1579 | "ms": { 1580 | "version": "2.1.3", 1581 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1582 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1583 | "dev": true 1584 | }, 1585 | "nanoid": { 1586 | "version": "3.3.1", 1587 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", 1588 | "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", 1589 | "dev": true 1590 | }, 1591 | "normalize-path": { 1592 | "version": "3.0.0", 1593 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1594 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1595 | "dev": true 1596 | }, 1597 | "once": { 1598 | "version": "1.4.0", 1599 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1600 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1601 | "dev": true, 1602 | "requires": { 1603 | "wrappy": "1" 1604 | } 1605 | }, 1606 | "p-limit": { 1607 | "version": "3.1.0", 1608 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1609 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1610 | "dev": true, 1611 | "requires": { 1612 | "yocto-queue": "^0.1.0" 1613 | } 1614 | }, 1615 | "p-locate": { 1616 | "version": "5.0.0", 1617 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1618 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1619 | "dev": true, 1620 | "requires": { 1621 | "p-limit": "^3.0.2" 1622 | } 1623 | }, 1624 | "path-exists": { 1625 | "version": "4.0.0", 1626 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1627 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1628 | "dev": true 1629 | }, 1630 | "path-is-absolute": { 1631 | "version": "1.0.1", 1632 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1633 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1634 | "dev": true 1635 | }, 1636 | "pathval": { 1637 | "version": "1.1.1", 1638 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 1639 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 1640 | "dev": true 1641 | }, 1642 | "picomatch": { 1643 | "version": "2.3.1", 1644 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1645 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1646 | "dev": true 1647 | }, 1648 | "randombytes": { 1649 | "version": "2.1.0", 1650 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1651 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1652 | "dev": true, 1653 | "requires": { 1654 | "safe-buffer": "^5.1.0" 1655 | } 1656 | }, 1657 | "readdirp": { 1658 | "version": "3.6.0", 1659 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1660 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1661 | "dev": true, 1662 | "requires": { 1663 | "picomatch": "^2.2.1" 1664 | } 1665 | }, 1666 | "redis": { 1667 | "version": "3.1.2", 1668 | "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", 1669 | "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", 1670 | "requires": { 1671 | "denque": "^1.5.0", 1672 | "redis-commands": "^1.7.0", 1673 | "redis-errors": "^1.2.0", 1674 | "redis-parser": "^3.0.0" 1675 | } 1676 | }, 1677 | "redis-commands": { 1678 | "version": "1.7.0", 1679 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", 1680 | "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" 1681 | }, 1682 | "redis-errors": { 1683 | "version": "1.2.0", 1684 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 1685 | "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" 1686 | }, 1687 | "redis-parser": { 1688 | "version": "3.0.0", 1689 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 1690 | "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", 1691 | "requires": { 1692 | "redis-errors": "^1.0.0" 1693 | } 1694 | }, 1695 | "require-directory": { 1696 | "version": "2.1.1", 1697 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1698 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1699 | "dev": true 1700 | }, 1701 | "safe-buffer": { 1702 | "version": "5.2.1", 1703 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1704 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1705 | "dev": true 1706 | }, 1707 | "serialize-javascript": { 1708 | "version": "6.0.0", 1709 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 1710 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 1711 | "dev": true, 1712 | "requires": { 1713 | "randombytes": "^2.1.0" 1714 | } 1715 | }, 1716 | "string-width": { 1717 | "version": "4.2.3", 1718 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1719 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1720 | "dev": true, 1721 | "requires": { 1722 | "emoji-regex": "^8.0.0", 1723 | "is-fullwidth-code-point": "^3.0.0", 1724 | "strip-ansi": "^6.0.1" 1725 | } 1726 | }, 1727 | "strip-ansi": { 1728 | "version": "6.0.1", 1729 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1730 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1731 | "dev": true, 1732 | "requires": { 1733 | "ansi-regex": "^5.0.1" 1734 | } 1735 | }, 1736 | "strip-json-comments": { 1737 | "version": "3.1.1", 1738 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1739 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1740 | "dev": true 1741 | }, 1742 | "supports-color": { 1743 | "version": "8.1.1", 1744 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1745 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1746 | "dev": true, 1747 | "requires": { 1748 | "has-flag": "^4.0.0" 1749 | } 1750 | }, 1751 | "to-regex-range": { 1752 | "version": "5.0.1", 1753 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1754 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1755 | "dev": true, 1756 | "requires": { 1757 | "is-number": "^7.0.0" 1758 | } 1759 | }, 1760 | "type-detect": { 1761 | "version": "4.0.8", 1762 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1763 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1764 | "dev": true 1765 | }, 1766 | "which": { 1767 | "version": "2.0.2", 1768 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1769 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1770 | "dev": true, 1771 | "requires": { 1772 | "isexe": "^2.0.0" 1773 | } 1774 | }, 1775 | "workerpool": { 1776 | "version": "6.2.0", 1777 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", 1778 | "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", 1779 | "dev": true 1780 | }, 1781 | "wrap-ansi": { 1782 | "version": "7.0.0", 1783 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1784 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1785 | "dev": true, 1786 | "requires": { 1787 | "ansi-styles": "^4.0.0", 1788 | "string-width": "^4.1.0", 1789 | "strip-ansi": "^6.0.0" 1790 | } 1791 | }, 1792 | "wrappy": { 1793 | "version": "1.0.2", 1794 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1795 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1796 | "dev": true 1797 | }, 1798 | "y18n": { 1799 | "version": "5.0.8", 1800 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1801 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1802 | "dev": true 1803 | }, 1804 | "yargs": { 1805 | "version": "16.2.0", 1806 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1807 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1808 | "dev": true, 1809 | "requires": { 1810 | "cliui": "^7.0.2", 1811 | "escalade": "^3.1.1", 1812 | "get-caller-file": "^2.0.5", 1813 | "require-directory": "^2.1.1", 1814 | "string-width": "^4.2.0", 1815 | "y18n": "^5.0.5", 1816 | "yargs-parser": "^20.2.2" 1817 | } 1818 | }, 1819 | "yargs-parser": { 1820 | "version": "20.2.4", 1821 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1822 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1823 | "dev": true 1824 | }, 1825 | "yargs-unparser": { 1826 | "version": "2.0.0", 1827 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1828 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1829 | "dev": true, 1830 | "requires": { 1831 | "camelcase": "^6.0.0", 1832 | "decamelize": "^4.0.0", 1833 | "flat": "^5.0.2", 1834 | "is-plain-obj": "^2.1.0" 1835 | } 1836 | }, 1837 | "yocto-queue": { 1838 | "version": "0.1.0", 1839 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1840 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1841 | "dev": true 1842 | } 1843 | } 1844 | } 1845 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tile38", 3 | "version": "0.9.4", 4 | "description": "Node library for accessing Tile38 service", 5 | "main": "./src/tile38.js", 6 | "directories": { 7 | "example": "examples", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "mocha" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/phulst/node-tile38.git" 16 | }, 17 | "author": "Peter Hulst ", 18 | "license": "ISC", 19 | "dependencies": { 20 | "redis": "^3.0.0", 21 | "redis-parser": "^3.0.0" 22 | }, 23 | "devDependencies": { 24 | "chai": "^4.3.4", 25 | "mocha": "^9.1.3" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/phulst/node-tile38/issues" 29 | }, 30 | "homepage": "https://github.com/phulst/node-tile38#readme" 31 | } 32 | -------------------------------------------------------------------------------- /src/live_geofence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const Parser = require("redis-parser"); 5 | 6 | /** 7 | * establishes an open socket to the Tile38 server for live geofences 8 | */ 9 | class LiveGeofence { 10 | 11 | constructor(debug = false, logger = console) { 12 | this.debug = debug; 13 | this.logger = logger; 14 | } 15 | 16 | /** 17 | * opens a socket to the server, submits a command, then continuously processes data that is returned 18 | * from the Tile38 server 19 | * @param host 20 | * @param port 21 | * @param password 22 | * @param command Command string to send to Tile38 23 | * @param callback callback method with parameters (err, results) 24 | */ 25 | open(host, port, password, command, callback) { 26 | const socket = new net.Socket(); 27 | this.socket = socket; 28 | socket.connect(port, host, () => { 29 | if (password) { 30 | // authenticate if necessary 31 | socket.write(`AUTH ${password}\r\n`); 32 | } 33 | socket.write(command + "\r\n"); 34 | }); 35 | 36 | let self = this; 37 | socket.on('close', () => { 38 | //this.logger.log("Socket is being closed!"); 39 | if (this.onCloseCb) this.onCloseCb(); 40 | }); 41 | 42 | const parser = new Parser({ 43 | returnReply: function(reply) { 44 | if (self.debug) self.logger.log(reply); 45 | if (reply == 'OK') return; // we're not invoking a callback for the 'OK' response that comes first 46 | 47 | let response = reply; 48 | let f = reply.charAt(0); 49 | if (f == '{' || f == '[') { 50 | // this smells like json, so try to parse it 51 | try { 52 | response = JSON.parse(reply); 53 | } catch (err) { 54 | self.logger.warn("Unable to parse server response: " + reply); 55 | // we'll return the reply as-is. 56 | } 57 | } 58 | callback(null, response); 59 | }, 60 | returnError: function(err) { 61 | self.logger.error('live socket error: ' + err); 62 | callback(err, null); 63 | }, 64 | returnFatalError: function(err) { 65 | self.logger.error('fatal live socket error: ' + err); 66 | self.socket.destroy(); 67 | callback(err, null); 68 | } 69 | }); 70 | 71 | socket.on('data', buffer => { 72 | //self.logger.log(JSON.stringify(buffer.toString())); 73 | parser.execute(buffer); 74 | }); 75 | return this; 76 | } 77 | 78 | // allows clients to register an 'on closed' handler to be notified if the socket unexpectedly gets closed 79 | onClose(callback) { 80 | this.onCloseCb = callback; 81 | } 82 | 83 | // Forces the geofence to be closed 84 | close() { 85 | if (this.socket) this.socket.destroy(); 86 | } 87 | } 88 | 89 | module.exports = LiveGeofence; 90 | -------------------------------------------------------------------------------- /src/tile38.js: -------------------------------------------------------------------------------- 1 | // const Redis = require('ioredis'); 2 | const redis = require('redis'); 3 | const Query = require('./tile38_query'); 4 | const LiveGeofence = require('./live_geofence'); 5 | 6 | const DEFAULT_HASH_PRECISION = 6; 7 | 8 | class Tile38 { 9 | 10 | constructor({port, host, password, debug = false, logger = console} = {}) { 11 | this.port = port ? port : (process.env.TILE38_PORT || 9851); 12 | this.host = host ? host : (process.env.TILE38_HOST || 'localhost'); 13 | this.password = password ? password : process.env.TILE38_PASSWD; 14 | this.logger = logger; 15 | 16 | let conn = { port: this.port, host: this.host}; 17 | if (this.password) { 18 | conn.password = this.password; 19 | } 20 | this.client = redis.createClient(conn); 21 | this.client.on('error', (err) => { 22 | this.logger.error('Tile38 connection error: ' + err); 23 | }); 24 | // put the OUTPUT in json mode, whenever connection is (re)established 25 | this.sendCommand('OUTPUT', null, 'json'); 26 | // after a reconnect, ensure that the OUTPUT mode is still json 27 | this.client.on('connect', () => { 28 | this.sendCommand('OUTPUT', null, 'json').catch((err) => { 29 | // unable to set output mode to json. Is the connection already closed? 30 | this.logger.warn(`unable to set output mode to json: ${err.message}`); 31 | }); 32 | }); 33 | this.debug = debug; 34 | } 35 | 36 | 37 | /** 38 | * convenience user function for sending commands that are not yet supported by this library. 39 | * Example use: 40 | * client.executeCommand('SCRIPT LOAD MYSCRIPT'); 41 | * 42 | * Most Tile38 return a json response with an ok=true property. By default, executeCommand will 43 | * return the value of this 'ok' property and thus return a boolean true. 44 | * To only return a different object from the Tile38 response: 45 | * client.executeCommand('KEYS *', { returnProp: 'keys'}) 46 | * 47 | * or to skip JSON parsing altogether and return the raw server response 48 | * client.executeCommand('KEYS *', { parseJson: false }) 49 | * 50 | */ 51 | executeCommand(command, opts = {}) { 52 | let returnProp = opts.returnProp || 'ok'; 53 | let parseJson = (opts.parseJson === undefined) ? true : opts.parseJson; // could be set as false 54 | let opt = parseJson ? returnProp : false; 55 | let commandArr = command.split(' '); 56 | return this.sendCommand(commandArr.shift(), opt, commandArr); 57 | } 58 | 59 | /* 60 | * send a command with optional arguments to the Redis driver, and return the response in a Promise. 61 | * If returnProp is set, it will assume that the response is a JSON string, then parse and return 62 | * the given property from that string. 63 | */ 64 | sendCommand(cmd, returnProp, args) { 65 | // make args an array if it's not already one 66 | if (!args) { 67 | args = [] 68 | } else if (!Array.isArray(args)) { 69 | args = [args]; 70 | } 71 | return new Promise((resolve, reject) => { 72 | if (this.debug) { 73 | this.logger.log(`sending command "${cmd} ${args.join(' ')}"`); 74 | } 75 | this.client.send_command(cmd, args, (err, result) => { 76 | if (err) { 77 | if (this.debug) this.logger.error(err); 78 | reject(err); 79 | } else { 80 | if (this.debug) this.logger.log(result); 81 | try { 82 | if (!returnProp) { 83 | // return the raw response 84 | resolve(result); 85 | } else { 86 | let res = JSON.parse(result); 87 | if (!res.ok) { 88 | if (res.err) { 89 | reject(new Error(res.err)); 90 | } else { 91 | reject(new Error(`unexpected response: ${result}`)); 92 | } 93 | } else { 94 | if (returnProp == 1) { 95 | // 1 has a special meaning. Return the entire response minus 96 | // 'ok' and 'elapsed' properties 97 | delete res.ok; 98 | delete res.elapsed; 99 | resolve(res); 100 | } else { 101 | resolve(res[returnProp]); 102 | } 103 | } 104 | } 105 | } catch (error) { 106 | reject(new Error(`response isn't valid JSON: ${result}`)); 107 | } 108 | } 109 | }); 110 | }); 111 | } 112 | 113 | // calls the PING command and returns a promise to the expected PONG response 114 | ping() { 115 | return this.sendCommand('PING', 'ping'); 116 | } 117 | 118 | quit() { 119 | return this.sendCommand('QUIT'); 120 | } 121 | 122 | server() { 123 | return this.sendCommand('SERVER', 'stats'); 124 | } 125 | 126 | // force the garbage collector 127 | gc() { 128 | return this.sendCommand('GC', 'ok'); 129 | } 130 | 131 | configGet(prop) { 132 | return this.sendCommand('CONFIG GET', 'properties', prop); 133 | } 134 | 135 | // sets a configuration value in the database. Will return true if successful. 136 | // Note that the value does not get persisted until configRewrite is called. 137 | configSet(prop, value) { 138 | return this.sendCommand('CONFIG SET', 'ok', [prop, value]); 139 | } 140 | 141 | // persists changes made by configSet command. Will return true if successful 142 | configRewrite() { 143 | return this.sendCommand('CONFIG REWRITE', 'ok'); 144 | } 145 | 146 | // flushes all data from the db. Will return true value if successful 147 | flushdb() { 148 | return this.sendCommand('FLUSHDB', 'ok'); 149 | } 150 | 151 | // turns on or off readonly mode. (Pass true value to turn on) 152 | readOnly(val) { 153 | return this.sendCommand('READONLY', 'ok', (val ? 'yes' : 'no')); 154 | } 155 | 156 | // Returns the minimum bounding rectangle for all objects in a key. 157 | bounds(key) { 158 | return this.sendCommand('BOUNDS', 'bounds', key); 159 | } 160 | 161 | // Set a timeout on an id. 162 | expire(key, id, seconds) { 163 | return this.sendCommand('EXPIRE', 'ok', [key, id, seconds]); 164 | } 165 | 166 | // Get a timeout on an id 167 | ttl(key, id) { 168 | return this.sendCommand('TTL', 'ttl', [key, id]); 169 | } 170 | 171 | persist(key, id) { 172 | return this.sendCommand('PERSIST', 'ok', [key, id]); 173 | } 174 | 175 | rename(oldKey, newKey) { 176 | return this.sendCommand('RENAME', 'ok', [oldKey, newKey]); 177 | } 178 | 179 | renamenx(oldKey, newKey) { 180 | // note that the Tile38 documentation states that the response 181 | // should be 0, 1 or ERR. However, in testing I found 182 | // it returns the typical 'OK' json response. 183 | return this.sendCommand('RENAMENX', 'ok', [oldKey, newKey]); 184 | } 185 | 186 | // Returns all keys matching pattern. 187 | keys(pattern) { 188 | return this.sendCommand('KEYS', 'keys', pattern); 189 | } 190 | 191 | // authenticate with server 192 | auth(password) { 193 | return this.sendCommand('AUTH', 'ok', password) 194 | } 195 | 196 | /* obj can be one of the following: 197 | * - an array with lat, lng and optional z coordinate, representing a point. 198 | * - an array of 4 coordinates, representing a bounding box. 199 | * - a string representing a Geohash 200 | * - a GeoJson object. 201 | * fields should be a simple object with key value pairs 202 | * opts can be used to set additional options, such as: 203 | * - expire: 3600 // to set expiration date of object 204 | * - onlyIfExists: true // only set object if the id already exists 205 | * - onlyIfNotExists: true // only set object if id does not exist yet 206 | * - type: 'string' // to set string values (otherwise interpreted as geohash) 207 | * Examples: 208 | * 209 | * // set a simple lat/lng coordinate 210 | * set('fleet', 'truck1', [33.5123, -112.2693]) 211 | * // set with additional fields 212 | * set('fleet', 'truck1', [33.5123, -112.2693], { field1: 10, field2: 20}); 213 | * // set lat/lon/alt coordinates, and expire in 120 secs 214 | * set('fleet', 'truck1', [33.5123, -112.2693, 120.0], {}, {expire: 120}) 215 | * // set bounds 216 | * set('props', 'house1', [33.7840, -112.1520, 33.7848, -112.1512]) 217 | * // set an ID by geohash 218 | * set('props', 'area1', '9tbnwg') // assumes HASH by default if only one extra parameter 219 | * // set a String value 220 | * set('props', 'area2', 'my string value', {}, {type: 'string'}) # or force to String type 221 | * // set with geoJson object 222 | * set('cities', 'tempe', geoJsonObject) 223 | * 224 | */ 225 | set(key, id, obj, fields = {}, opts = {}) { 226 | let cmd = [key, id]; 227 | for (let f in fields) { 228 | cmd = cmd.concat(['FIELD', f, fields[f]]); 229 | } 230 | let expire = opts['expire']; 231 | if (expire > 0) { 232 | cmd.push('EX'); 233 | cmd.push(expire); 234 | } 235 | if (opts['onlyIfNotExists']) { 236 | cmd.push('NX'); 237 | } 238 | if (opts['onlyIfExists']) { 239 | cmd.push('XX'); 240 | } 241 | if (Array.isArray(obj)) { 242 | // if obj is an array, it must be either POINT or BOUNDS 243 | if (obj.length < 4) { 244 | cmd.push('POINT'); 245 | cmd = cmd.concat(obj); 246 | } else if (obj.length == 4) { 247 | cmd.push('BOUNDS'); 248 | cmd = cmd.concat(obj); 249 | } else { 250 | throw Error("incorrect number of values"); 251 | } 252 | } else if (typeof obj == 'string') { 253 | // if obj is a string, it must be String or geohash 254 | if (opts['type'] == 'string') { 255 | cmd.push('STRING'); 256 | cmd.push(obj); 257 | } else { 258 | cmd.push('HASH'); 259 | cmd.push(obj); 260 | } 261 | } else { 262 | // must be a Geojson object 263 | cmd.push('OBJECT'); 264 | cmd.push(JSON.stringify(obj)); 265 | } 266 | return this.sendCommand('SET', 'ok', cmd); 267 | } 268 | 269 | // convenience method for set() with options.type = 'string' 270 | setString(key, id, obj, fields = {}, opts = {}) { 271 | opts.type = 'string'; 272 | return this.set(key, id, obj, fields, opts); 273 | } 274 | 275 | 276 | /** 277 | * Set the value for a one or more fields. This can be called in one of two ways: 278 | * fset('fleet', 'truck1', 'speed', 16) 279 | * or to set multiple values: 280 | * fset('fleet', 'truck1', { speed: 16, driver: 1224 }) 281 | */ 282 | fset(key, id, field, value) { 283 | let params; 284 | if (typeof field == 'string') { 285 | params = [key, id, field, value]; 286 | } else { 287 | // object passed in 288 | let set = new Set(); 289 | set.add(key); 290 | set.add(id); 291 | for (let k in field) { 292 | set.add(k); 293 | set.add(field[k]); 294 | } 295 | params = Array.from(set); 296 | } 297 | return this.sendCommand('FSET', 'ok', params); 298 | } 299 | 300 | // Delete an id from a key 301 | del(key, id) { 302 | return this.sendCommand('DEL', 'ok', [key, id]); 303 | } 304 | 305 | // Removes objects that match a specified pattern. 306 | pdel(key, pattern) { 307 | return this.sendCommand('PDEL', 'ok', [key, pattern]); 308 | } 309 | 310 | // 311 | /* 312 | * Get the object of an id. The default output format is a GeoJSON object. 313 | * 314 | * The options hash supports 2 properties: 315 | * type: (POINT, BOUNDS, HASH, OBJECT) the type in which to return the ID. Defaults to OBJECT 316 | * withfields: boolean to indicate whether or not fields should be returned. Defaults to false 317 | * 318 | * examples: 319 | * get('fleet', 'truck1') // returns geojson point 320 | * get('fleet', 'truck1', {withfields: true} // include FIELDS 321 | * get('fleet', 'truck1', {type: 'POINT'}) // same as above 322 | * get('fleet', 'truck1', {type: 'BOUNDS'}) // return bounds 323 | * get('fleet', 'truck1', {type: 'HASH 6'}) // return geohash with precision 6 324 | */ 325 | get(key, id, {withfields = false, type = null} = {}) { 326 | 327 | let params = [key, id]; 328 | if (withfields) params.push('WITHFIELDS'); 329 | // TODO: check if startswith HASH and remove separate 'precision' property 330 | // it could just be passed as 'HASH 6' 331 | if (type != null && type.startsWith('HASH')) { 332 | // geohash requested, add precision if set 333 | params.push('HASH'); 334 | let s = type.split(' '); 335 | if (s.length > 1 && parseInt(s[1]) > 0) 336 | params.push(s[1]); 337 | else 338 | throw new Error('missing precision. Please set like this: "HASH 6"'); 339 | } else if (type != null) { 340 | params.push(type) 341 | } 342 | return this.sendCommand('GET', 1, params); 343 | } 344 | 345 | // shortcut for GET method with output POINT 346 | getPoint(key, id, opts = {}) { 347 | opts.type = 'POINT'; 348 | return this.get(key, id, opts); 349 | } 350 | 351 | // shortcut for GET method with output BOUNDS 352 | getBounds(key, id, opts = {}) { 353 | opts.type = 'BOUNDS'; 354 | return this.get(key, id, opts); 355 | } 356 | 357 | // shortcut for GET method with output HASH 358 | getHash(key, id, opts = {}) { 359 | let precision = opts.precision || DEFAULT_HASH_PRECISION; 360 | opts.type = `HASH ${precision}`; 361 | return this.get(key, id, opts); 362 | } 363 | 364 | // Remove all objects from specified key. 365 | drop(key) { 366 | return this.sendCommand('DROP', 'ok', key); 367 | } 368 | 369 | // Return stats for one or more keys. 370 | stats(...keys) { 371 | return this.sendCommand('STATS', 'stats', keys); 372 | } 373 | 374 | // Set a value in a JSON document 375 | jset(key, id, jKey, jVal) { 376 | return this.sendCommand('JSET', 'ok', [key, id, jKey, jVal]); 377 | } 378 | 379 | // Get a value from a json document 380 | jget(key, id, ...other) { 381 | let params = [key, id] 382 | params = params.concat(other) 383 | return this.sendCommand('JGET', 'value', params); 384 | } 385 | 386 | // Delete a json value 387 | jdel(key, id, jKey) { 388 | return this.sendCommand('JDEL', 'ok', [key, id, jKey]); 389 | } 390 | 391 | // returns a Tile38Query object that can be used to further construct an INTERSECTS query 392 | intersectsQuery(key) { 393 | return new Query('INTERSECTS', key, this); 394 | } 395 | 396 | // returns a Tile38Query object that can be used to further construct an SEARCH query 397 | searchQuery(key) { 398 | return new Query('SEARCH', key, this); 399 | } 400 | 401 | // returns a Tile38Query object that can be used to further construct an NEARBY query 402 | nearbyQuery(key) { 403 | return new Query('NEARBY', key, this); 404 | } 405 | 406 | // returns a Tile38Query object that can be used to further construct an SCAN query 407 | scanQuery(key) { 408 | return new Query('SCAN', key, this); 409 | } 410 | 411 | // returns a Tile38Query object that can be used to further construct an WITHIN query 412 | withinQuery(key) { 413 | return new Query('WITHIN', key, this); 414 | } 415 | 416 | /* 417 | * name: webhook name 418 | * endpoint: endpoint url for http/grpc/redis etc 419 | * meta: object with key/value pairs for meta data 420 | * searchType: nearby/within/intersects 421 | * key: the key to monitor 422 | * opts: object for additional options: 423 | * command: del/drop/set 424 | * detect: inside/outside/enter/exit/cross 425 | * get: [key, id] - to fetch an existing object from given key collection 426 | * bounds: [minlat, minlon, maxlat, maxlon] - bounds coordinates 427 | * object: geojson object 428 | * tile: [x,y,z] - tile coordinates 429 | * quadkey: quadkey coordinates 430 | * hash: geohash coordinate 431 | * radius: radius/distance to apply 432 | * 433 | * command and detect may both exist but only one of the following get/bounds/object/tile/quadkey/hash 434 | * may be specified at a time. 435 | * 436 | * TODO: This command should be rewritten to use the same chaining form that the search commands use. 437 | */ 438 | setHook(name, endpoint, meta, searchType, key, opts) { 439 | let cmd = [name, endpoint]; 440 | if (meta) { 441 | for (let m of Object.keys(meta)) { 442 | cmd.push('META'); 443 | cmd.push(m); 444 | cmd.push(meta[m]); 445 | } 446 | } 447 | cmd.push(searchType.toUpperCase()); 448 | cmd.push(key); 449 | cmd.push('FENCE'); 450 | cmd = cmd.concat(processOpts(opts, ['detect', 'commands', 'get', 'point', 'bounds', 'object', 451 | 'tile', 'quadkey', 'hash', 'radius', 'roam'])); 452 | return this.sendCommand('SETHOOK', 'ok', cmd); 453 | } 454 | 455 | // Returns all hooks matching a given pattern (which defaults to '*') 456 | hooks(pattern = "*") { 457 | return this.sendCommand('HOOKS', 'hooks', pattern); 458 | } 459 | 460 | // Remove a specified hook 461 | delhook(name) { 462 | return this.sendCommand('DELHOOK', 'ok', name); 463 | } 464 | 465 | // Removes all hooks that match the specified pattern 466 | pdelhook(pattern) { 467 | return this.sendCommand('PDELHOOK', 'ok', pattern); 468 | } 469 | 470 | // opens a live geofence and returns an instance of LiveGeofence (that can be used to later on close it). 471 | openLiveFence(command, commandArr, callback) { 472 | if (this.debug) { 473 | this.logger.log(`sending live fence command "${command} ${commandArr.join(' ')}"`); 474 | } 475 | let cmd = this.redisEncodeCommand(command, commandArr); 476 | return (new LiveGeofence(this.debug, this.logger)).open(this.host, this.port, this.password, cmd, callback); 477 | } 478 | 479 | // encodes the tile38_query.commandArr() output to be sent to Redis. This is only necessary for the live geofence, 480 | // since the sendCommand() function uses the node_redis library, which handles this for us. 481 | redisEncodeCommand(command, arr) { 482 | // this is a greatly simplified version of the internal_send_command() functionality in 483 | // https://github.com/NodeRedis/node_redis/blob/master/index.js 484 | let cmdStr = '*' + (arr.length + 1) + '\r\n$' + command.length + '\r\n' + command + '\r\n'; 485 | let str; 486 | for (let c of arr) { 487 | if (typeof c === 'string') 488 | str = c; 489 | else 490 | str = c.toString(); 491 | cmdStr += '$' + Buffer.byteLength(str) + '\r\n' + str + '\r\n'; 492 | } 493 | return cmdStr; 494 | } 495 | } 496 | 497 | // processes all options that may be used by any of the search commands 498 | let processOpts = function (opts, names) { 499 | let cmd = []; 500 | if (opts === undefined) 501 | return cmd; // no options 502 | 503 | for (let name of names) { 504 | if (!opts[name]) 505 | continue; // an option with this name was not passed in. 506 | 507 | switch (name) { 508 | case 'cursor': 509 | cmd.push('CURSOR'); 510 | cmd.push(opts.cursor); 511 | break; 512 | case 'limit': 513 | cmd.push('LIMIT'); 514 | cmd.push(opts.limit); 515 | break; 516 | case 'sparse': 517 | cmd.push('SPARSE'); 518 | cmd.push(opts.sparse); 519 | break; 520 | case 'match': 521 | cmd.push('MATCH'); 522 | cmd.push(opts.match); 523 | break; 524 | case 'where': 525 | let w = opts.where; 526 | cmd.push('WHERE'); 527 | for (let k in Object.keys(w)) { 528 | cmd.push(k); 529 | cmd.push(w[k][0]); 530 | cmd.push(w[k][1]); 531 | } 532 | break; 533 | case 'nofields': 534 | if (opts.nofields == true) 535 | cmd.push('NOFIELDS'); 536 | break; 537 | case 'fence': 538 | if (opts.fence == true) 539 | cmd.push('FENCE'); 540 | break; 541 | case 'detect': 542 | cmd.push('DETECT'); 543 | cmd.push(opts.detect); 544 | break; 545 | case 'commands': 546 | cmd.push('COMMANDS'); 547 | cmd.push(opts.commands); // should be comma separated list 548 | break; 549 | case 'select': // COUNT, IDS, OBJECTS, POINTS, BOUNDS, HASHES 550 | cmd.push(ops.select.toUpperCase()); 551 | break; 552 | case 'roam': // roam: [key, pattern, meters] 553 | cmd.push('ROAM'); 554 | cmd = cmd.concat(opts.roam); 555 | break; 556 | case 'order': 557 | cmd.push(opts.order.toUpperCase()); 558 | break; 559 | } 560 | } 561 | return cmd.concat(areaOpts(opts, names)); 562 | } 563 | 564 | // processes all area options for within/intersects and sethook commands, then 565 | // constructs an array with commands. 566 | let areaOpts = function(opts, names) { 567 | let cmd = []; 568 | // iterate over all keys in the opts object and process any known options 569 | for (let name of names) { 570 | if (!opts[name]) 571 | continue; // an option with this name was not passed in. 572 | 573 | switch(name) { 574 | case 'point': // point: [lat, lon, meters] 575 | cmd.push('POINT'); 576 | cmd = cmd.concat(opts.point); 577 | break; 578 | case 'get': // passed like this: 'get: [key,id]' 579 | cmd.push('GET'); 580 | cmd.push(opts.get[0]); 581 | cmd.push(opts.get[1]); 582 | break; 583 | case 'bounds': // bounds: [minlat, minlon, maxlat, maxlon] 584 | cmd.push('BOUNDS'); 585 | cmd.push(opts.bounds[0]); 586 | cmd.push(opts.bounds[1]); 587 | cmd.push(opts.bounds[2]); 588 | cmd.push(opts.bounds[3]); 589 | break; 590 | case 'object': // geojson object 591 | cmd.push('OBJECT'); 592 | cmd.push(JSON.stringify(opts.object)); 593 | break; 594 | case 'tile': 595 | cmd.push('TILE'); 596 | cmd.push(opts.tile[0]); 597 | cmd.push(opts.tile[1]); 598 | cmd.push(opts.tile[2]); 599 | break; 600 | case 'quadkey': 601 | cmd.push('QUADKEY'); 602 | cmd.push(opts.quadkey); 603 | break; 604 | case 'hash': 605 | cmd.push('HASH'); 606 | cmd.push(opts.hash); 607 | break; 608 | case 'radius': 609 | // radius, used ie with POINT or geohash 610 | cmd.push(opts.radius); 611 | break; 612 | } 613 | } 614 | return cmd; 615 | } 616 | 617 | 618 | module.exports = Tile38; 619 | -------------------------------------------------------------------------------- /src/tile38_query.js: -------------------------------------------------------------------------------- 1 | 2 | // adds elements from arr2 to arr1. If arr1 doesn't exist, it will 3 | // simply return arr2 4 | function addToArray(arr1, arr2) { 5 | if (arr1) { 6 | for (let a of arr2) { 7 | arr1.push(a); 8 | } 9 | return arr1; 10 | } else { 11 | return arr2; 12 | } 13 | } 14 | 15 | class Tile38Query { 16 | 17 | 18 | constructor(type, key, client) { 19 | this.type = type; 20 | this.key = key; 21 | this.client = client; 22 | this.options = {}; 23 | } 24 | 25 | cursor(start) { 26 | this.options.cursor = ['CURSOR', start]; 27 | return this; 28 | } 29 | 30 | limit(count) { 31 | this.options.limit = ['LIMIT', count]; 32 | return this; 33 | } 34 | 35 | sparse(spread) { 36 | this.options.sparse = ['SPARSE', spread]; 37 | return this; 38 | } 39 | 40 | /* 41 | * set a matching query on the object ID. The value is a glob pattern. 42 | * Unlike other query methods in this class, match() may be called multiple times 43 | */ 44 | match(value) { 45 | let m = ['MATCH', value]; 46 | this.options.matches = addToArray(this.options.matches, m); 47 | return this; 48 | } 49 | 50 | // sort order for SCAN query, must be 'asc' or 'desc' 51 | order(val) { 52 | this.options.order = val.toUpperCase(); 53 | return this; 54 | } 55 | 56 | // equivalent of order('asc') 57 | asc() { 58 | return this.order('asc'); 59 | } 60 | // equivalent of order('desc'); 61 | desc() { 62 | return this.order('desc'); 63 | } 64 | 65 | // adds DISTANCE argument for nearby query. 66 | distance() { 67 | this.options.distance = 'DISTANCE'; 68 | return this; 69 | } 70 | 71 | /* 72 | * set a where search pattern. Like match, this method may be chained multiple times 73 | * as well. For example: 74 | * query.where('speed', 70, '+inf').where('age', '-inf', 24) 75 | */ 76 | where(field, ...criteria) { 77 | let arr = ['WHERE', field].concat(criteria); 78 | this.options.where = addToArray(this.options.where, arr); 79 | return this; 80 | } 81 | 82 | /* 83 | * set a wherein search pattern. Like match, this method may be chained multiple times 84 | * as well. For example: 85 | * query.wherein('doors', 2, 5).wherein('wheels', 14, 18, 22) 86 | * Would generate the command: 87 | * WHEREIN doors 2 2 5 WHEREIN wheels 3 14 18 22 88 | * (note that the command to the server includes the argument count, while the 89 | * js api doesn't need this) 90 | */ 91 | whereIn(field, ...values) { 92 | let arr = ['WHEREIN', field, values.length].concat(values); 93 | this.options.whereIn = addToArray(this.options.whereIn, arr); 94 | return this; 95 | } 96 | whereEval(script, ...args) { 97 | let arr = ['WHEREEVAL', script, args.length].concat(args); 98 | this.options.whereEval = addToArray(this.options.whereEval, arr); 99 | return this; 100 | } 101 | whereEvalSha(sha, ...args) { 102 | let arr = ['WHEREEVALSHA', sha, args.length].concat(args); 103 | this.options.whereEvalSha = addToArray(this.options.whereEvalSha, arr); 104 | return this; 105 | } 106 | 107 | /* 108 | * clip intersecting objects 109 | */ 110 | clip() { 111 | this.options.clip = 'CLIP'; 112 | return this; 113 | } 114 | 115 | /* 116 | * call nofields to exclude field values from search results 117 | */ 118 | nofields() { 119 | this.options.nofields = 'NOFIELDS'; 120 | return this; 121 | } 122 | 123 | /* 124 | * sets one or more detect values. For example: 125 | * query.detect('inside', 'outside'); 126 | * or 127 | * query.detect('inside,outside'); 128 | * 129 | * whichever you prefer 130 | */ 131 | detect(...values) { 132 | this.options.detect = ['DETECT'].concat(values.join(',')); 133 | return this; 134 | } 135 | 136 | /** 137 | * sets commands to listen for. Expected values: del, drop and set 138 | * You may pass these as separate parameters, 139 | * query.commands('del', 'drop', 'set'); 140 | * 141 | * or as a single comma separated parameter 142 | * query.commands('del,drop,set'); 143 | */ 144 | commands(...values) { 145 | this.options.commands = ['COMMANDS'].concat(values.join(',')); 146 | return this; 147 | } 148 | 149 | /** 150 | * set output type. Allowed values: 151 | * count 152 | * ids 153 | * objects 154 | * points 155 | * bounds 156 | * hashes 157 | * 158 | * If 'hashes' is used a second parameter should specify the precision, ie 159 | * query.output('hashes', 6); 160 | * 161 | * Note that all of these types, except for 'bounds' can be called using convenience methods as well, 162 | * so 163 | * objects() instead of output('objects') 164 | * and 165 | * hashes(6) instead of output('hashes', 6) 166 | * 167 | */ 168 | output(type, precision) { 169 | type = type.toUpperCase(); 170 | if (type == 'HASHES' && precision != undefined) { 171 | this.options.output = [type, precision]; 172 | } else { 173 | this.options.output = [type]; 174 | } 175 | return this; 176 | } 177 | 178 | // shortcut for .output('ids') 179 | ids() { 180 | return this.output('ids'); 181 | } 182 | // shortcut for .output('count') 183 | count() { 184 | return this.output('count'); 185 | } 186 | // shortcut for .output('objects') 187 | objects() { 188 | return this.output('objects'); 189 | } 190 | // shortcut for .output('points') 191 | points() { 192 | return this.output('points'); 193 | } 194 | // shortcut for .output('points') 195 | hashes(precision) { 196 | return this.output('hashes', precision); 197 | } 198 | nodwell() { 199 | return this.output('nodwell'); 200 | } 201 | 202 | // add a timeout to a query 203 | timeout(secs) { 204 | this.options.timeout = ['TIMEOUT', secs]; 205 | return this; 206 | } 207 | 208 | /** 209 | * conducts search with an object that's already in the database 210 | */ 211 | getObject(key, id) { 212 | this.options.getObject = ['GET', key, id]; 213 | return this; 214 | } 215 | 216 | /** 217 | * conducts search with bounds coordinates 218 | */ 219 | bounds(minlat, minlon, maxlat, maxlon) { 220 | this.options.bounds = ['BOUNDS', minlat, minlon, maxlat, maxlon]; 221 | return this; 222 | } 223 | 224 | /** 225 | * conducts search with geojson object 226 | */ 227 | object(geojson) { 228 | this.options.geojson = ['OBJECT', JSON.stringify(geojson)]; 229 | return this; 230 | } 231 | 232 | tile(x, y, z) { 233 | this.options.tile = ['TILE', x, y, z]; 234 | return this; 235 | } 236 | 237 | quadKey(key) { 238 | this.options.quadKey = ['QUADKEY', key]; 239 | return this; 240 | } 241 | 242 | hash(geohash) { 243 | this.options.hash = ['HASH', geohash]; 244 | return this; 245 | } 246 | 247 | // adds CIRCLE arguments to WITHIN / INTERSECTS queries 248 | circle(lat, lon, meters) { 249 | this.options.circle = ['CIRCLE', lat, lon, meters]; 250 | return this; 251 | } 252 | 253 | // adds POINT arguments to NEARBY query. 254 | point(lat, lon, meters) { 255 | this.options.point = ['POINT', lat, lon]; 256 | if (meters !== undefined) { 257 | this.options.point.push(meters); 258 | } 259 | return this; 260 | } 261 | 262 | // adds ROAM arguments to NEARBY query 263 | roam(key, pattern, meters) { 264 | // TODO throw error if type != 'NEARBY' 265 | this.options.roam = ['ROAM', key, pattern, meters]; 266 | return this; 267 | } 268 | 269 | // return all the commands of the query chain, as a string, the way it will 270 | // be sent to Tile38 271 | commandStr() { 272 | // if set, TIMEOUT goes before anything else in the command string 273 | let t = this.options.timeout; 274 | let commandStr = t ? `${t[0]} ${t[1]} ` : ''; 275 | 276 | return `${commandStr}${this.type} ${this.commandArr().join(' ')}`; 277 | } 278 | 279 | // constructs the full array for all arguments of the query. 280 | commandArr() { 281 | let cmd = [this.key]; 282 | let o = this.options; 283 | 284 | // construct an array of commands in this order 285 | let commands = ['cursor', 'limit', 'sparse', 'matches', 'order', 'distance', 'where', 286 | 'whereIn', 'whereEval', 'whereEvalSha', 'clip', 'nofields', 'fence', 'detect', 287 | 'commands', 'output', 'getObject', 'bounds', 'geojson', 'tile', 'quadKey', 'hash', 288 | 'point', 'circle', 'nodwell', 'roam' ]; 289 | for (let c of commands) { 290 | let opt = o[c]; 291 | if (opt !== undefined) { 292 | if (opt instanceof Array) { 293 | // array of objects 294 | for (let i of o[c]) { 295 | cmd.push(i); 296 | } 297 | } else { 298 | // simple string 299 | cmd.push(opt); 300 | } 301 | } 302 | } 303 | return cmd; 304 | } 305 | 306 | /** 307 | * will execute the query and return a Promise to the result. 308 | * To use the live fence with streaming results, use fence() instead. 309 | */ 310 | execute() { 311 | return this.client.sendCommand(this.type, 1, this.commandArr()); 312 | } 313 | 314 | /** 315 | * returns streaming results for a live geofence. This function will repeatedly call the specified callback 316 | * method when results are received. 317 | * This method returns an instance of LiveGeofence, which can be used to close the fence if necessary by calling 318 | * its close() method. 319 | */ 320 | executeFence(callback) { 321 | this.options.fence = 'FENCE'; 322 | return this.client.openLiveFence(this.type, this.commandArr(), callback); 323 | } 324 | 325 | /* 326 | * factory method to create a new Tile38Query object for an INTERSECTS search. 327 | * These factory methods are used in the test suite, but since these don't have 328 | * access to a Tile38 client object, they cannot be used to actually execute 329 | * a query on the server. 330 | * Use the Tile38.intersectsQuery() method instead. 331 | */ 332 | static intersects(key) { 333 | return new Tile38Query('INTERSECTS', key); 334 | } 335 | 336 | // Use Tile38.searchQuery() method instead 337 | static search(key) { 338 | return new Tile38Query('SEARCH', key); 339 | } 340 | 341 | // Use Tile38.nearbyQuery() method instead 342 | static nearby(key) { 343 | return new Tile38Query('NEARBY', key); 344 | } 345 | 346 | // Use Tile38.scanQuery() method instead 347 | static scan(key) { 348 | return new Tile38Query('SCAN', key); 349 | } 350 | 351 | // Use Tile38.withinQuery() method instead 352 | static within(key) { 353 | return new Tile38Query('WITHIN', key); 354 | } 355 | } 356 | 357 | module.exports = Tile38Query; 358 | -------------------------------------------------------------------------------- /test/test_hook_commands.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Tile38 = require('../src/tile38'); 3 | require('chai').should(); 4 | const expect = require('chai').expect; 5 | // var assert = require('assert'); 6 | 7 | describe('hook commands', function() { 8 | let tile38; 9 | 10 | beforeEach(function(done) { 11 | tile38 = new Tile38({debug: false}); 12 | done(); 13 | // tile38.set('fleet', 'truck1', [33.5123, -112.2693]).then(() => { 14 | // tile38.set('fleet', 'truck2', [33.5011, -112.2710]).then(() => { 15 | // done(); 16 | // }); 17 | // }); 18 | }); 19 | 20 | describe('sethook', function() { 21 | it("should do a nearby search with point", done => { 22 | let meta = { field: 'val1', field2: 'val2'}; 23 | let opts = { 24 | point: [33.637276, -84.434006], 25 | radius: 500 26 | }; 27 | tile38.setHook('testhook0', 'http://httpbin.org/post', meta, 'nearby', 'fleet', opts).then(res => { 28 | res.should.be.true; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('sethook', function() { 35 | it("should do a nearby search with point", done => { 36 | let meta = { field: 'val1', field2: 'val2'}; 37 | let opts = { 38 | roam: ['fleet', '*', 500] 39 | }; 40 | tile38.setHook('testhook1', 'http://httpbin.org/post', meta, 'nearby', 'fleet', opts).then(res => { 41 | res.should.be.true; 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('list hooks', function() { 48 | it("should return all hooks", done => { 49 | tile38.hooks('*').then(res => { 50 | expect(res.length).to.be.above(0); 51 | done(); 52 | }); 53 | }); 54 | it("should return all testhook", done => { 55 | tile38.hooks('testhook*').then(res => { 56 | expect(res.length).to.equal(2); 57 | expect(res[0].name).to.equal('testhook0'); 58 | expect(res[1].name).to.equal('testhook1'); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/test_keys_commands.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | const Tile38 = require('../src/tile38'); 3 | require('chai').should(); 4 | const expect = require('chai').expect; 5 | 6 | 7 | var assert = require('assert'); 8 | describe('key commands', function() { 9 | let tile38; 10 | 11 | beforeEach(function(done) { 12 | tile38 = new Tile38({debug: false}); 13 | 14 | tile38.set('fleet', 'truck1', [33.5123, -112.2693]).then(() => { 15 | tile38.set('fleet', 'truck2', [33.5011, -112.2710]).then(() => { 16 | done(); 17 | }); 18 | }); 19 | }); 20 | 21 | describe('bounds', function() { 22 | it("should return bounds for a key", (done) => { 23 | tile38.bounds('fleet').then((bounds) => { 24 | bounds.type.should.equal('Polygon'); 25 | bounds.coordinates.should.be.an('array'); 26 | done(); 27 | }) 28 | }); 29 | }); 30 | 31 | describe('hooks', function() { 32 | it("should return an array when calling hooks", (done) => { 33 | tile38.hooks().then((hooks) => { 34 | hooks.should.be.an('array'); 35 | done(); 36 | }) 37 | }); 38 | }); 39 | 40 | describe('expiration', function() { 41 | let randExpiration = Math.floor(Math.random() * 100) + 10; 42 | 43 | it("should set expiration on an id", (done) => { 44 | tile38.expire('fleet', 'truck2', randExpiration).then((res) => { 45 | // this will always return ok, even if the key/id doesn't exist 46 | res.should.be.true; 47 | // read back the TTL, which will already be lower than the value we set 48 | tile38.ttl('fleet', 'truck2').then((res) => { 49 | res.should.be.above(randExpiration - 2); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | it("should persist object", (done) => { 56 | // first, expire it 57 | tile38.expire('fleet', 'truck2', 20).then((res) => { 58 | tile38.persist('fleet', 'truck2').then((r) => { 59 | r.should.be.true; 60 | done(); 61 | }) 62 | }); 63 | }); 64 | }); 65 | 66 | describe('keys', function() { 67 | it("should return all keys matching 'fleet:*' pattern", (done) => { 68 | tile38.keys('fl??t').then((keys) => { 69 | keys.length.should.equal(1); 70 | keys[0].should.equal('fleet'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it("should drop all elements for a key", (done) => { 76 | tile38.set('somekey', 'truck1', [33.5123, -112.2693]).then(() => { 77 | tile38.set('somekey', 'truck2', [34.5011, -113.2710]).then(() => { 78 | tile38.drop('somekey').then((res) => { 79 | tile38.scanQuery('somekey').execute().then((obj) => { 80 | obj.objects.length.should.equal(0); 81 | done(); 82 | }) 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | 90 | 91 | describe('get', function() { 92 | it("should fetch with default type", (done) => { 93 | tile38.get('fleet', 'truck1').then((res) => { 94 | res.object.type.should.equal('Point'); 95 | done(); 96 | }); 97 | }); 98 | 99 | it("should fetch with geojson type", (done) => { 100 | tile38.get('fleet', 'truck1', {type: 'OBJECT'}).then((res) => { 101 | res.object.type.should.equal('Point'); 102 | done(); 103 | }); 104 | }); 105 | 106 | it("should fetch as point", (done) => { 107 | tile38.get('fleet', 'truck1', {type: 'POINT'}).then((res) => { 108 | res.point.lat.should.equal(33.5123); 109 | res.point.lon.should.equal(-112.2693); 110 | done(); 111 | }); 112 | }); 113 | 114 | it("should fetch as point using getPoint method", (done) => { 115 | tile38.getPoint('fleet', 'truck1').then((res) => { 116 | res.point.lat.should.equal(33.5123); 117 | res.point.lon.should.equal(-112.2693); 118 | done(); 119 | }); 120 | }); 121 | 122 | it("should fetch as bounds", (done) => { 123 | tile38.get('fleet', 'truck1', {type: 'BOUNDS'}).then((res) => { 124 | res.bounds.sw.should.exist; 125 | res.bounds.ne.should.exist; 126 | done(); 127 | }); 128 | }); 129 | it("should fetch as bounds using getBounds method", (done) => { 130 | tile38.getBounds('fleet', 'truck1').then((res) => { 131 | res.bounds.sw.should.exist; 132 | res.bounds.ne.should.exist; 133 | done(); 134 | }); 135 | }); 136 | 137 | it("should fetch as hash", (done) => { 138 | tile38.get('fleet', 'truck1', {type: 'HASH 6'}).then((res) => { 139 | res.hash.should.exist; 140 | res.hash.length.should.equal(6); 141 | done(); 142 | }); 143 | }) 144 | it("missing precision should throw an error", (done) => { 145 | try { 146 | tile38.get('fleet', 'truck1', {type: 'HASH'}).then((res) => { 147 | }); 148 | } catch (err) { 149 | expect(err.message).to.equal('missing precision. Please set like this: "HASH 6"'); 150 | done(); 151 | } 152 | }) 153 | it("should fetch as hash using getHash method", (done) => { 154 | tile38.getHash('fleet', 'truck1', {precision: 8}).then((res) => { 155 | res.hash.should.exist; 156 | res.hash.length.should.equal(8); 157 | done(); 158 | }); 159 | }) 160 | }); 161 | 162 | describe('del', function() { 163 | it("should delete the id", (done) => { 164 | tile38.del('fleet', 'truck1').then((res) => { 165 | res.should.be.true; 166 | // it should now no longer exist 167 | tile38.get('fleet', 'truck1').then((thing) => { 168 | throw error('this shouldn\'t happen'); 169 | console.log(thing); 170 | }).catch((err) => { 171 | // since the key should no longer exist, we expect an error here 172 | err.message.should.equal('id not found'); 173 | done(); 174 | }); 175 | }) 176 | }); 177 | }); 178 | 179 | describe('fields', function() { 180 | it("should not return fields by default", (done) => { 181 | tile38.fset('fleet', 'truck2', 'value', 20).then((res) => { 182 | res.should.be.true; 183 | // fetch it back to verify that we do not receive this field 184 | tile38.get('fleet', 'truck2').then((res) => { 185 | expect(res.fields).to.be.undefined; 186 | done(); 187 | }); 188 | }) 189 | }); 190 | it("should set a field on an id", (done) => { 191 | tile38.fset('fleet', 'truck2', 'value', 30).then((res) => { 192 | res.should.be.true; 193 | // fetch it back to verify the field is set 194 | tile38.get('fleet', 'truck2', {withfields: true}).then((res) => { 195 | res.fields.value.should.equal(30); 196 | done(); 197 | }); 198 | }); 199 | }); 200 | it("should alllow multiple fields to be set", (done) => { 201 | tile38.fset('fleet', 'truck2', {name1: 88, name2: 99}).then((res) => { 202 | res.should.be.true; 203 | // fetch it back to verify the field is set 204 | tile38.get('fleet', 'truck2', {withfields: true}).then((res) => { 205 | res.fields.name1.should.equal(88); 206 | res.fields.name2.should.equal(99); 207 | done(); 208 | }); 209 | }); 210 | }); 211 | }); 212 | 213 | describe('rename', function() { 214 | it("should succesfully rename a collection", (done) => { 215 | tile38.set('oldname', 'truck', [33.5123, -112.2693]).then((res) => { 216 | res.should.be.true; 217 | tile38.rename('oldname', 'newname').then((res) => { 218 | res.should.be.true; 219 | done(); 220 | }); 221 | }); 222 | }); 223 | it("should succesfully rename a collection using renamenx", (done) => { 224 | tile38.renamenx('newname', 'anothername').then((res) => { 225 | res.should.be.true; 226 | done(); 227 | }); 228 | }); 229 | }); 230 | 231 | describe('set', function() { 232 | it("should set object with simple coordinates", (done) => { 233 | tile38.set('fleet', 'truck1', [33.5123, -112.2693]).then((res) => { 234 | res.should.be.true; 235 | done(); 236 | }); 237 | }); 238 | it("should set object with simple coordinates plus altitude", (done) => { 239 | tile38.set('fleet', 'truck1', [33.5123, -112.2693, 230]).then((res) => { 240 | res.should.be.true; 241 | done(); 242 | }); 243 | }); 244 | it("should set object with bounds", (done) => { 245 | tile38.set('fleet', 'area', [33.7840, -112.1520, 33.7848, -112.1512]).then((res) => { 246 | res.should.be.true; 247 | done(); 248 | }); 249 | }); 250 | it("should set object with geohash", (done) => { 251 | let hashVal = '9tbnwg'; 252 | tile38.set('props', 'area1', hashVal).then((res) => { 253 | res.should.be.true; 254 | tile38.getHash('props', 'area1').then((res) => { 255 | res.hash.should.equal(hashVal); 256 | done(); 257 | }); 258 | }); 259 | }); 260 | it("should set object with string", (done) => { 261 | let strVal = 'my string value'; 262 | tile38.set('fleet', 'somewhere', strVal, {}, {'type':'string'}).then((res) => { 263 | res.should.be.true; 264 | tile38.get('fleet', 'somewhere').then((res) => { 265 | res.object.should.equal(strVal); 266 | done(); 267 | }); 268 | }); 269 | }); 270 | it("should set object with setString", (done) => { 271 | let strVal = 'my string value'; 272 | tile38.setString('fleet', 'somestring', strVal, {}).then((res) => { 273 | res.should.be.true; 274 | tile38.get('fleet', 'somestring').then((res) => { 275 | res.object.should.equal(strVal); 276 | done(); 277 | }); 278 | }); 279 | }); 280 | }); 281 | 282 | describe('json set/get', function() { 283 | // set a json value 284 | beforeEach(function(done) { 285 | tile38.jset('user', '100', 'attr.name', 'peter').then((res) => { 286 | tile38.jset('user', '100', 'attr.age', 10).then((res) => { 287 | done(); 288 | }); 289 | }); 290 | }); 291 | 292 | it("should get the entire json", (done) => { 293 | tile38.jget('user', '100').then((res) => { 294 | let obj = JSON.parse(res); 295 | expect(obj.attr.age).to.equal(10); 296 | expect(obj.attr.name).to.equal('peter'); 297 | done(); 298 | }); 299 | }); 300 | 301 | it("should get a specific json property", (done) => { 302 | tile38.jget('user', '100', 'attr.name').then((res) => { 303 | expect(res).to.equal('peter'); 304 | done(); 305 | }); 306 | }); 307 | 308 | it("should delete a json property", (done) => { 309 | tile38.jdel('user', '100', 'attr.age').then((res) => { 310 | res.should.be.true; 311 | // now, we should only have the name property left 312 | tile38.jget('user', '100').then((res) => { 313 | let obj = JSON.parse(res); 314 | expect(obj.attr.name).to.equal('peter'); 315 | done(); 316 | }); 317 | }); 318 | }); 319 | }); 320 | 321 | /* this test is still failing due to the Tile38 not responding within the timeout period. 322 | * I did fix as suggested here: https://github.com/tidwall/tile38/issues/627 323 | * 324 | describe('whereeval', function() { 325 | // this tests a fix for issue #28 326 | it("should execute nearby query using whereeval", (done) => { 327 | // set area first 328 | tile38.set('area', '9621a40b0f90f467', [13.3252703267768, 52.5029839702301], {areaId: 12345}).then((res) => { 329 | // now execute nearby query using a script 330 | 331 | tile38.nearbyQuery('area').distance().point(13.3252703267768, 52.5029839702301, 3500) 332 | .limit(1).whereEval("return FIELDS.areaId ~= ARGV[1]", 1, 111631).execute((res) => { 333 | console.dir(res); 334 | done(); 335 | }).catch((err) => { 336 | console.dir(err); 337 | done(err); 338 | }); 339 | }); 340 | }); 341 | }); 342 | */ 343 | }); 344 | -------------------------------------------------------------------------------- /test/test_tile38.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | const Tile38 = require('../src/tile38'); 3 | const Query = require('../src/tile38_query'); 4 | require('chai').should(); 5 | const expect = require('chai').expect; 6 | 7 | 8 | var assert = require('assert'); 9 | describe('tile38', function() { 10 | let tile38; 11 | 12 | beforeEach(function() { 13 | tile38 = new Tile38(); 14 | }); 15 | 16 | describe('ping', function() { 17 | 18 | it("should return 'pong'", (done) => { 19 | tile38.ping().then((resp) => { 20 | done(); 21 | }).catch((err) => { 22 | done(new Error(err)); 23 | }) 24 | }); 25 | }); 26 | 27 | describe('quit', function() { 28 | 29 | it ("should close the connection", (done) => { 30 | setTimeout(() => { 31 | tile38.quit().then((resp) => { 32 | resp.should.equal('OK'); 33 | done(); 34 | }); 35 | }, 300); // avoid warning message in on connect handler that connection is already closed 36 | }); 37 | }); 38 | 39 | describe('server', function() { 40 | 41 | it ("should return server status", (done) => { 42 | tile38.server().then((resp) => { 43 | // check a couple of properties from the response 44 | resp.id.should.exist; 45 | resp.mem_alloc.should.exist; 46 | resp.aof_size.should.exist; 47 | done(); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('garbage collector', function() { 53 | 54 | it ("should call the garbage collector", (done) => { 55 | tile38.gc().then((resp) => { 56 | resp.should.be.true; 57 | done(); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('flushdb', function() { 63 | 64 | it ("should destroy all data", (done) => { 65 | tile38.flushdb().then((resp) => { 66 | resp.should.be.true; 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('config', function() { 73 | let randMemory = Math.floor(Math.random() * (256) + 256); 74 | 75 | it ("should get a config parameter", (done) => { 76 | tile38.configGet('protected-mode').then((properties) => { 77 | properties['protected-mode'].should.equal('yes'); 78 | done(); 79 | }); 80 | }); 81 | 82 | it ("should set a config parameter", (done) => { 83 | tile38.configSet('maxmemory', `${randMemory}MB`).then((res) => { 84 | res.should.be.true; 85 | done(); 86 | }); 87 | }); 88 | 89 | it ("should persist changes set with configRewrite", (done) => { 90 | tile38.configRewrite().then((res) => { 91 | res.should.be.true; 92 | tile38.configGet('maxmemory').then((properties) => { 93 | properties['maxmemory'].should.equal(`${randMemory}mb`); 94 | done(); 95 | }); 96 | }); 97 | }); 98 | 99 | }); 100 | 101 | describe('readonly', function() { 102 | 103 | it("should turn on readonly mode", (done) => { 104 | tile38.readOnly(true).then((res) => { 105 | res.should.be.true; 106 | done(); 107 | }); 108 | }); 109 | it("should turn off readonly mode", (done) => { 110 | tile38.readOnly(false).then((res) => { 111 | res.should.be.true; 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | 118 | describe('redisEncodeCmd', function() { 119 | it("should encode roam query", function (done) { 120 | let q = Query.nearby('fleet').roam('key', 'ptn', 3000); 121 | let cmd = tile38.redisEncodeCommand(q.type, q.commandArr()); 122 | expect(cmd).to.equal("*6\r\n$6\r\nNEARBY\r\n$5\r\nfleet\r\n$4\r\nROAM\r\n$3\r\nkey\r\n$3\r\nptn\r\n$4\r\n3000\r\n"); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/test_tile38_query.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require('chai').expect; 3 | 4 | const Query = require('../src/tile38_query'); 5 | 6 | describe('tile38 search query', function() { 7 | 8 | describe('factory methods', function () { 9 | it("should create INTERSECTS query", function () { 10 | let q = Query.intersects('fleet'); 11 | let cmd = q.commandStr(); 12 | expect(cmd).to.equal('INTERSECTS fleet'); 13 | }); 14 | }); 15 | 16 | describe('offset and limit', function() { 17 | it("should parse offset option", function() { 18 | let q = Query.intersects('fleet').cursor(10); 19 | let cmd = q.commandArr(); 20 | expect(cmd[1]).to.equal('CURSOR'); 21 | expect(cmd[2]).to.equal(10); 22 | }); 23 | it("should parse limit option", function() { 24 | let q = Query.intersects('fleet').limit(50); 25 | let cmd = q.commandArr(); 26 | expect(cmd[1]).to.equal('LIMIT'); 27 | expect(cmd[2]).to.equal(50); 28 | }); 29 | it("should parse offset and limit", function() { 30 | let q = Query.intersects('fleet').cursor(100).limit(50); 31 | let cmd = q.commandArr(); 32 | expect(cmd[1]).to.equal('CURSOR'); 33 | expect(cmd[2]).to.equal(100); 34 | expect(cmd[3]).to.equal('LIMIT'); 35 | expect(cmd[4]).to.equal(50); 36 | }); 37 | }); 38 | 39 | describe('sparse', function() { 40 | it("should set sparse spread value", function () { 41 | let q = Query.intersects('fleet').sparse(5); 42 | let cmd = q.commandArr(); 43 | expect(cmd[1]).to.equal('SPARSE'); 44 | expect(cmd[2]).to.equal(5); 45 | }); 46 | }); 47 | 48 | describe('match', function() { 49 | it("should set a match value", function () { 50 | let q = Query.intersects('fleet').match('someid*'); 51 | let cmd = q.commandArr(); 52 | expect(cmd[1]).to.equal('MATCH'); 53 | expect(cmd[2]).to.equal('someid*'); 54 | }); 55 | it("should set multiple match values", function () { 56 | let q = Query.intersects('fleet').match('someid*').match('other'); 57 | let cmd = q.commandArr(); 58 | expect(cmd[1]).to.equal('MATCH'); 59 | expect(cmd[2]).to.equal('someid*'); 60 | expect(cmd[3]).to.equal('MATCH'); 61 | expect(cmd[4]).to.equal('other'); 62 | }); 63 | }); 64 | 65 | describe('order', function() { 66 | it("should set ASC or DESC in scan query", function () { 67 | let q = Query.scan('fleet').order('desc'); 68 | expect(q.commandStr()).to.equal('SCAN fleet DESC'); 69 | }); 70 | it("should use shortcut asc() in scan query", function () { 71 | let q = Query.scan('fleet').asc(); 72 | expect(q.commandStr()).to.equal('SCAN fleet ASC'); 73 | }); 74 | it("should use shortcut desc() in scan query", function () { 75 | let q = Query.scan('fleet').desc(); 76 | expect(q.commandStr()).to.equal('SCAN fleet DESC'); 77 | }); 78 | }); 79 | 80 | describe('timeout', function() { 81 | it("should set timeout on a scan query", function() { 82 | let q = Query.scan('mykey').where('foo', 1, 2).count().timeout(0.1); 83 | expect(q.commandStr()).to.equal("TIMEOUT 0.1 SCAN mykey WHERE foo 1 2 COUNT"); 84 | }); 85 | }); 86 | 87 | describe('distance', function() { 88 | it("should set DISTANCE argument in nearby query", function () { 89 | let q = Query.nearby('fleet').distance(); 90 | let cmd = q.commandArr(); 91 | expect(cmd[1]).to.equal('DISTANCE'); 92 | }); 93 | }); 94 | 95 | describe('where', function() { 96 | it("should set a where search", function () { 97 | let q = Query.intersects('fleet').where('age', 50, '+inf'); 98 | let cmd = q.commandArr(); 99 | expect(cmd[1]).to.equal('WHERE'); 100 | expect(cmd[2]).to.equal('age'); 101 | expect(cmd[3]).to.equal(50); 102 | expect(cmd[4]).to.equal('+inf'); 103 | }); 104 | it("should set multiple where searches", function () { 105 | let q = Query.intersects('fleet').where('age', 60, '+inf').where('speed', 20); 106 | let cmd = q.commandArr(); 107 | expect(cmd[1]).to.equal('WHERE'); 108 | expect(cmd[2]).to.equal('age'); 109 | expect(cmd[3]).to.equal(60); 110 | expect(cmd[4]).to.equal('+inf'); 111 | expect(cmd[5]).to.equal('WHERE'); 112 | expect(cmd[6]).to.equal('speed'); 113 | expect(cmd[7]).to.equal(20); 114 | }); 115 | }); 116 | 117 | describe('wherein', function() { 118 | it("should set a wherein search", function () { 119 | let q = Query.intersects('fleet').whereIn('wheels', 8, 14, 18, 22); 120 | let cmd = q.commandArr(); 121 | expect(cmd[1]).to.equal('WHEREIN'); 122 | expect(cmd[2]).to.equal('wheels'); 123 | expect(cmd[3]).to.equal(4); 124 | expect(cmd[4]).to.equal(8); 125 | expect(cmd[5]).to.equal(14); 126 | expect(cmd[6]).to.equal(18); 127 | expect(cmd[7]).to.equal(22); 128 | }); 129 | }); 130 | 131 | describe('whereeval', function() { 132 | it("should set a whereeval search", function () { 133 | let luaStr = "return FIELDS.wheels > ARGV[1] or (FIELDS.length * FIELDS.width) > ARGV[2]"; 134 | let q = Query.intersects('fleet').whereEval(luaStr, 8, 120); 135 | 136 | let cmd = q.commandArr(); 137 | expect(cmd[1]).to.equal('WHEREEVAL'); 138 | expect(cmd[2]).to.equal(luaStr); // lua script should be quoted 139 | expect(cmd[3]).to.equal(2); 140 | expect(cmd[4]).to.equal(8); 141 | expect(cmd[5]).to.equal(120); 142 | }); 143 | it("should set multiple whereeval searches", function () { 144 | let q = Query.intersects('fleet').whereEval('script1', 10, 20).whereEval('script2', 30, 40, 50); 145 | expect(q.commandStr()).to.equal('INTERSECTS fleet WHEREEVAL script1 2 10 20 WHEREEVAL script2 3 30 40 50'); 146 | }); 147 | }); 148 | 149 | describe('whereevalsha', function() { 150 | it("should set a whereevalsha search", function () { 151 | let q = Query.intersects('fleet').whereEvalSha('1234abc', 10, 20, 30, 40); 152 | expect(q.commandStr()).to.equal('INTERSECTS fleet WHEREEVALSHA 1234abc 4 10 20 30 40'); 153 | }); 154 | }); 155 | 156 | describe('nofields', function() { 157 | it("should set nofields property", function () { 158 | let q = Query.intersects('fleet').nofields(); 159 | let cmd = q.commandArr(); 160 | expect(cmd[1]).to.equal('NOFIELDS'); 161 | }); 162 | }); 163 | 164 | describe('detect', function() { 165 | it("should set single detect value", function () { 166 | let q = Query.intersects('fleet').detect('inside'); 167 | let cmd = q.commandArr(); 168 | expect(cmd[1]).to.equal('DETECT'); 169 | expect(cmd[2]).to.equal('inside'); 170 | }); 171 | it("should set multiple detect values from a single parameter", function () { 172 | let q = Query.intersects('fleet').detect('inside,outside'); 173 | let cmd = q.commandArr(); 174 | expect(cmd[1]).to.equal('DETECT'); 175 | expect(cmd[2]).to.equal('inside,outside'); 176 | }); 177 | it("should set multiple detect values from multiple parameters", function () { 178 | let q = Query.intersects('fleet').detect('inside','outside'); 179 | let cmd = q.commandArr(); 180 | expect(cmd[1]).to.equal('DETECT'); 181 | expect(cmd[2]).to.equal('inside,outside'); 182 | }); 183 | }); 184 | describe('commands', function() { 185 | it("should set single command", function () { 186 | let q = Query.intersects('fleet').commands('del'); 187 | let cmd = q.commandArr(); 188 | expect(cmd[1]).to.equal('COMMANDS'); 189 | expect(cmd[2]).to.equal('del'); 190 | }); 191 | it("should set multiple detect values from a single parameter", function () { 192 | let q = Query.intersects('fleet').commands('del,drop'); 193 | let cmd = q.commandArr(); 194 | expect(cmd[1]).to.equal('COMMANDS'); 195 | expect(cmd[2]).to.equal('del,drop'); 196 | }); 197 | it("should set multiple detect values from multiple parameters", function () { 198 | let q = Query.intersects('fleet').commands('del','set'); 199 | let cmd = q.commandArr(); 200 | expect(cmd[1]).to.equal('COMMANDS'); 201 | expect(cmd[2]).to.equal('del,set'); 202 | }); 203 | 204 | }); 205 | describe('output', function() { 206 | it("should set COUNT output format", function () { 207 | let q = Query.intersects('fleet').output('count'); 208 | let cmd = q.commandArr(); 209 | expect(cmd[1]).to.equal('COUNT'); 210 | }); 211 | it("should set COUNT output format using convenience method", function () { 212 | let q = Query.intersects('fleet').count(); 213 | let cmd = q.commandArr(); 214 | expect(cmd[1]).to.equal('COUNT'); 215 | }); 216 | it("should set POINTS output format", function () { 217 | let q = Query.intersects('fleet').output('points'); 218 | let cmd = q.commandArr(); 219 | expect(cmd[1]).to.equal('POINTS'); 220 | }); 221 | it("should set POINTS output format using convenience method", function () { 222 | let q = Query.intersects('fleet').points(); 223 | let cmd = q.commandArr(); 224 | expect(cmd[1]).to.equal('POINTS'); 225 | }); 226 | it("should set IDS output format using convenience method", function () { 227 | let q = Query.intersects('fleet').ids(); 228 | let cmd = q.commandArr(); 229 | expect(cmd[1]).to.equal('IDS'); 230 | }); 231 | it("should set HASHES output format, with precision", function () { 232 | let q = Query.intersects('fleet').output('hashes', 8); 233 | let cmd = q.commandArr(); 234 | expect(cmd[1]).to.equal('HASHES'); 235 | expect(cmd[2]).to.equal(8); 236 | }); 237 | }); 238 | describe('get', function() { 239 | it("should store get query", function () { 240 | let q = Query.intersects('fleet').getObject('cities', 'oakland'); 241 | let cmd = q.commandArr(); 242 | expect(cmd[1]).to.equal('GET'); 243 | expect(cmd[2]).to.equal('cities'); 244 | expect(cmd[3]).to.equal('oakland'); 245 | }); 246 | }); 247 | describe('bounds', function() { 248 | it("should store bounds query", function () { 249 | let q = Query.intersects('fleet').bounds(33.462, -112.268, 33.491, -112.245); 250 | let cmd = q.commandArr(); 251 | expect(cmd[1]).to.equal('BOUNDS'); 252 | expect(cmd[2]).to.equal(33.462); 253 | expect(cmd[3]).to.equal(-112.268); 254 | expect(cmd[4]).to.equal(33.491); 255 | expect(cmd[5]).to.equal(-112.245); 256 | }); 257 | }); 258 | describe('geojson', function() { 259 | it("should store geojson query", function () { 260 | let polygon = {"type":"Polygon","coordinates": 261 | [[[-111.9787,33.4411],[-111.8902,33.4377],[-111.8950,33.2892],[-111.9739,33.2932],[-111.9787,33.4411]]]}; 262 | let exp = '{"type":"Polygon","coordinates":[[[-111.9787,33.4411],[-111.8902,33.4377],[-111.895,33.2892],[-111.9739,33.2932],[-111.9787,33.4411]]]}'; 263 | 264 | let q = Query.intersects('fleet').object(polygon); 265 | let cmd = q.commandArr(); 266 | expect(cmd[1]).to.equal('OBJECT'); 267 | expect(cmd[2]).to.equal(exp); 268 | }); 269 | }); 270 | describe('tile', function() { 271 | it("should store tile query", function () { 272 | let q = Query.intersects('fleet').tile(10,20,30); 273 | let cmd = q.commandStr(); 274 | expect(cmd).to.equal('INTERSECTS fleet TILE 10 20 30'); 275 | }); 276 | }); 277 | describe('quadkey', function() { 278 | it("should store quadkey query", function () { 279 | let q = Query.intersects('fleet').quadKey(3242421); 280 | let cmd = q.commandStr(); 281 | expect(cmd).to.equal('INTERSECTS fleet QUADKEY 3242421'); 282 | }); 283 | }); 284 | describe('hash', function() { 285 | it("should store hash query", function () { 286 | let q = Query.intersects('fleet').hash('382ad23e'); 287 | let cmd = q.commandStr(); 288 | expect(cmd).to.equal('INTERSECTS fleet HASH 382ad23e'); 289 | }); 290 | }); 291 | describe('point', function() { 292 | it("should store point query", function () { 293 | let q = Query.nearby('fleet').point(33.462, -112.268, 6000); 294 | let cmd = q.commandStr(); 295 | expect(cmd).to.equal('NEARBY fleet POINT 33.462 -112.268 6000'); 296 | }); 297 | it("should allow nearby point query without radius", function () { 298 | let q = Query.nearby('fleet').point(33.462, -112.268); 299 | expect(q.commandArr().length).to.equal(4); 300 | let cmd = q.commandStr(); 301 | expect(cmd).to.equal('NEARBY fleet POINT 33.462 -112.268'); 302 | }); 303 | 304 | }); 305 | describe('circle', function() { 306 | it("should store circle query", function () { 307 | let q = Query.intersects('fleet').circle(33.462, -112.268, 200) 308 | let c = q.commandStr(); 309 | expect(c).to.equal('INTERSECTS fleet CIRCLE 33.462 -112.268 200'); 310 | }); 311 | }); 312 | describe('roam', function() { 313 | it("should store roam query", function () { 314 | let q = Query.nearby('fleet').roam('key', 'ptn', 3000); 315 | let cmd = q.commandStr(); 316 | expect(cmd).to.equal('NEARBY fleet ROAM key ptn 3000'); 317 | }); 318 | it("should pass nodwell property", function () { 319 | let q = Query.nearby('people').nodwell().roam('friends', '*', 100); 320 | // this property is usually set when executeFence() is call. 321 | // Setting it manually here. 322 | q.options.fence = 'FENCE'; 323 | expect(q.commandStr()).to.equal('NEARBY people FENCE NODWELL ROAM friends * 100'); 324 | }); 325 | }); 326 | 327 | }); 328 | --------------------------------------------------------------------------------