From 1896f18518de34e472f4c45b1120979245951a88 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Sat, 10 Oct 2020 16:46:39 -0230 Subject: [PATCH 01/75] Add yarn-error.log to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b06bc78..4601041 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +yarn-error.log + node_modules/ *.tsbuildinfo From 22badc6c759da41849af89b209850939b174175d Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Sat, 10 Oct 2020 21:00:55 -0230 Subject: [PATCH 02/75] Reorg endpoints for testing --- dev/test | 19 +++++++++++++++++++ package.json | 3 ++- src/endpoints/ping.test.ts | 18 ++++++++++++++++++ src/endpoints/ping.ts | 7 +++++++ src/index.ts | 9 ++++++--- src/utils.ts | 14 ++++++++++++++ types/baretest.d.ts | 14 ++++++++++++++ yarn.lock | 12 ++++++++++++ 8 files changed, 92 insertions(+), 4 deletions(-) create mode 100755 dev/test create mode 100644 src/endpoints/ping.test.ts create mode 100644 src/endpoints/ping.ts create mode 100644 src/utils.ts create mode 100644 types/baretest.d.ts diff --git a/dev/test b/dev/test new file mode 100755 index 0000000..2c030f3 --- /dev/null +++ b/dev/test @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -u +set -o pipefail + +readonly all_files=$( find src -type f -name '*.test.js' ) + +for f in ${1:-$all_files} +do + output=$(node "$f") + status="$?" + if [[ "$status" -eq 0 ]] + then + printf '%s' "${output}" + else + printf '%s\n' "${output}" + exit "$status" + fi +done diff --git a/package.json b/package.json index 4a92f67..92d3c25 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "main": "src/index.js", "scripts": { "build": "tsc --project .", - "test": ":", + "test": "dev/test", "start": "node ." }, "repository": { @@ -26,6 +26,7 @@ }, "devDependencies": { "@types/node": "^12.12.67", + "baretest": "^2.0.0", "typescript": "^4.0.3" } } diff --git a/src/endpoints/ping.test.ts b/src/endpoints/ping.test.ts new file mode 100644 index 0000000..42d8799 --- /dev/null +++ b/src/endpoints/ping.test.ts @@ -0,0 +1,18 @@ +import { strict as assert } from 'assert'; +import baretest from 'baretest'; +import fastify from 'fastify'; +import path from 'path'; +import createPingEndpoint from './ping'; + +const test = baretest(path.basename(__filename, '.test.js')); + +test('returns a status code of 200', async () => { + const app = createPingEndpoint(fastify()); + const response = await app.inject({ method: 'GET', url: '/ping' }); + + assert.equal(response.statusCode, 200); + + await app.close(); +}); + +(async () => { process.exitCode = (await test.run()) ? 0 : 1; })(); diff --git a/src/endpoints/ping.ts b/src/endpoints/ping.ts new file mode 100644 index 0000000..2630fd1 --- /dev/null +++ b/src/endpoints/ping.ts @@ -0,0 +1,7 @@ +import type { FastifyInstance } from 'fastify'; + +export default function createPingEndpoint(app: FastifyInstance) { + app.get('/ping', async () => `${JSON.stringify({ status: 'ok', date: Date.now() })}\n`); + + return app; +} diff --git a/src/index.ts b/src/index.ts index d0f7a66..fd23f71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ import fastify from 'fastify'; +import createPingEndpoint from './endpoints/ping'; +import { addRoutes } from './utils'; const port = Number(process.env.PORT || 8080); -const server = fastify(); - -server.get('/ping', async () => `${JSON.stringify({ status: 'ok', date: Date.now() })}\n`); +const server = addRoutes( + fastify(), + createPingEndpoint, +); server.listen(port, '::', (err, address) => { if (err) { diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..9f1ffb1 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,14 @@ +import type { FastifyInstance } from 'fastify'; + +type EndpointFunction = (s: FastifyInstance) => FastifyInstance; + +/** + * Returns a Fastify instance composed of all the given endpoints + * + * @param server - the Fastify instance to build on + * @param endpoints - a list of endpoint creators + * @returns the built Fastify instance + */ +export function addRoutes(server: FastifyInstance, ...endpoints: EndpointFunction[]) { + return endpoints.reduce((prev, curr) => curr(prev), server); +} diff --git a/types/baretest.d.ts b/types/baretest.d.ts new file mode 100644 index 0000000..ac80e40 --- /dev/null +++ b/types/baretest.d.ts @@ -0,0 +1,14 @@ +declare module 'baretest' { + export interface Test { + (name: string, fn: () => Promise): void; + run(): Promise; + skip(fn: () => Promise): void; + // This isn't real, but skip ignores its args + skip(name: string, fn: () => Promise): void; + // This isn't real, but skip ignores its args + skip(name: string): void; + only(name: string, fn: () => Promise): void; + } + + export default function (headline: string): Test; +} diff --git a/yarn.lock b/yarn.lock index 3cc32d8..a35b0e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,6 +42,18 @@ avvio@^7.1.2: fastq "^1.6.1" queue-microtask "^1.1.2" +barecolor@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/barecolor/-/barecolor-1.0.1.tgz#1bbbbfc41cedecf74e23fb7d9d054cd8763d92cc" + integrity sha512-ncJ680U+r1CGBt73L3O6V9GIAPy3hbDmWODEQajwEnDmmzeStvc4UYhapUSxUpS76+MHxyRihzZfwhyl122Zdw== + +baretest@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/baretest/-/baretest-2.0.0.tgz#5791667b47329c452e1f9bade879ab8dc0b304ba" + integrity sha512-hRmYnBojeijT3jH0GtqLoHus+adPoeYh2NmcNT3wBPH903AUphcFqs1gJ64fBovDXql51Df24g9D9jcXRZd4vA== + dependencies: + barecolor "1.0.1" + cookie@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" From 95d58cb2a9e5512f2c50ce308055afb1210a15fc Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 14 Nov 2020 20:41:04 -0330 Subject: [PATCH 03/75] Added a number of prototype routes as well as the basic config for firestore --- .gitignore | 1 + package-lock.json | 358 +++ package.json | 5 + src/database/dataLayer.ts | 5 + src/database/firestore.ts | 14 + src/endpoints/ping.test.ts | 18 - src/endpoints/regionBusinesses.ts | 27 + src/endpoints/regions.ts | 20 + src/endpoints/userRegions.ts | 27 + tests/dataLayer.test.ts | 7 + tests/ping.test.ts | 11 + tests/regionBusinesses.test.ts | 12 + tests/regions.test.ts | 11 + tests/userRegions.test.ts | 16 + tsconfig.json | 2 +- yarn.lock | 4206 ++++++++++++++++++++++++++++- 16 files changed, 4689 insertions(+), 51 deletions(-) create mode 100644 package-lock.json create mode 100644 src/database/dataLayer.ts create mode 100644 src/database/firestore.ts delete mode 100644 src/endpoints/ping.test.ts create mode 100644 src/endpoints/regionBusinesses.ts create mode 100644 src/endpoints/regions.ts create mode 100644 src/endpoints/userRegions.ts create mode 100644 tests/dataLayer.test.ts create mode 100644 tests/ping.test.ts create mode 100644 tests/regionBusinesses.test.ts create mode 100644 tests/regions.test.ts create mode 100644 tests/userRegions.test.ts diff --git a/.gitignore b/.gitignore index 4601041..49a9b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ yarn-error.log +.idea/ node_modules/ *.tsbuildinfo diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a031c5c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,358 @@ +{ + "name": "@ranlab/api", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "12.19.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.4.tgz", + "integrity": "sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w==", + "dev": true + }, + "abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" + }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, + "avvio": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.0.tgz", + "integrity": "sha512-KtC63UyZARidAoIV8wXutAZnDIbZcXBqLjTAhZOX+mdMZBQCh5il/15MvCvma1178nhTwvN2D0TOAdiKG1MpUA==", + "requires": { + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.1", + "queue-microtask": "^1.1.2" + } + }, + "barecolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/barecolor/-/barecolor-1.0.1.tgz", + "integrity": "sha512-ncJ680U+r1CGBt73L3O6V9GIAPy3hbDmWODEQajwEnDmmzeStvc4UYhapUSxUpS76+MHxyRihzZfwhyl122Zdw==", + "dev": true + }, + "baretest": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/baretest/-/baretest-2.0.0.tgz", + "integrity": "sha512-hRmYnBojeijT3jH0GtqLoHus+adPoeYh2NmcNT3wBPH903AUphcFqs1gJ64fBovDXql51Df24g9D9jcXRZd4vA==", + "dev": true, + "requires": { + "barecolor": "1.0.1" + } + }, + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-json-stringify": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.2.9.tgz", + "integrity": "sha512-O8YmNoc7LnfSafVaTfa1yXVFT4UMsi/N7cYcNZw6w5D5tltyu6XGXvH45mvWfsrcFoSK+H0q0exGXsUqC18z/g==", + "requires": { + "ajv": "^6.11.0", + "deepmerge": "^4.2.2", + "string-similarity": "^4.0.1" + } + }, + "fast-redact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.0.0.tgz", + "integrity": "sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w==" + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fastify": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.8.0.tgz", + "integrity": "sha512-w57/uvyQWzF/KSr9CbWQ5nfTqSSfYcmrems9Lc3VvtrAF7EsLbfZQBQZul6xwvE1uEfxA4nGdoUKqpU7xiv7cw==", + "requires": { + "abstract-logging": "^2.0.0", + "ajv": "^6.12.2", + "avvio": "^7.1.2", + "fast-json-stringify": "^2.2.1", + "fastify-error": "^0.2.0", + "fastify-warning": "^0.2.0", + "find-my-way": "^3.0.5", + "flatstr": "^1.0.12", + "light-my-request": "^4.2.0", + "pino": "^6.2.1", + "proxy-addr": "^2.0.5", + "readable-stream": "^3.4.0", + "rfdc": "^1.1.4", + "secure-json-parse": "^2.0.0", + "semver": "^7.3.2", + "tiny-lru": "^7.0.0" + } + }, + "fastify-error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.2.0.tgz", + "integrity": "sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA==" + }, + "fastify-warning": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", + "integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==" + }, + "fastq": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", + "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", + "requires": { + "reusify": "^1.0.4" + } + }, + "find-my-way": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-3.0.5.tgz", + "integrity": "sha512-FweGg0cv1sBX8z7WhvBX5B5AECW4Zdh/NiB38Oa0qwSNIyPgRBCl/YjxuZn/rz38E/MMBHeVKJ22i7W3c626Gg==", + "requires": { + "fast-decode-uri-component": "^1.0.1", + "safe-regex2": "^2.0.0", + "semver-store": "^0.3.0" + } + }, + "flatstr": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", + "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "light-my-request": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.3.0.tgz", + "integrity": "sha512-WrEvI7V41ZbEUe0bsfuS170QrYSVADKA0JiWyK/lVtm4Ra26pl9CYKBdlr823/s37N2wMJze8YNkHbg11aZWAw==", + "requires": { + "ajv": "^6.12.2", + "cookie": "^0.4.0", + "fastify-warning": "^0.2.0", + "readable-stream": "^3.6.0", + "set-cookie-parser": "^2.4.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "pino": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.7.0.tgz", + "integrity": "sha512-vPXJ4P9rWCwzlTJt+f0Ni4THc3DWyt8iDDCO4edQ8narTu6hnpzdXu8FqeSJCGndl1W6lfbYQUQihUO54y66Lw==", + "requires": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.7", + "flatstr": "^1.0.12", + "pino-std-serializers": "^2.4.2", + "quick-format-unescaped": "^4.0.1", + "sonic-boom": "^1.0.2" + } + }, + "pino-std-serializers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz", + "integrity": "sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg==" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "queue-microtask": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.0.tgz", + "integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w==" + }, + "quick-format-unescaped": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz", + "integrity": "sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "requires": { + "ret": "~0.2.0" + } + }, + "secure-json-parse": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.1.0.tgz", + "integrity": "sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA==" + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "semver-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", + "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==" + }, + "set-cookie-parser": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz", + "integrity": "sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg==" + }, + "sonic-boom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.3.0.tgz", + "integrity": "sha512-4nX6OYvOYr6R76xfQKi6cZpTO3YSWe/vd+QdIfoH0lBy0MnPkeAbb2rRWgmgADkXUeCKPwO1FZAKlAVWAadELw==", + "requires": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, + "string-similarity": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.3.tgz", + "integrity": "sha512-QEwJzNFCqq+5AGImk5z4vbsEPTN/+gtyKfXBVLBcbPBRPNganZGfQnIuf9yJ+GiwSnD65sT8xrw/uwU1Q1WmfQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tiny-lru": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", + "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==" + }, + "typescript": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", + "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + } + } +} diff --git a/package.json b/package.json index 92d3c25..e3d751c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "build": "tsc --project .", "test": "dev/test", + "jest": "jest", "start": "node ." }, "repository": { @@ -25,8 +26,12 @@ "fastify": "^3.5.1" }, "devDependencies": { + "@types/jest": "^26.0.15", "@types/node": "^12.12.67", "baretest": "^2.0.0", + "firebase": "^8.0.2", + "jest": "^26.6.3", + "ts-jest": "^26.4.4", "typescript": "^4.0.3" } } diff --git a/src/database/dataLayer.ts b/src/database/dataLayer.ts new file mode 100644 index 0000000..8cf1781 --- /dev/null +++ b/src/database/dataLayer.ts @@ -0,0 +1,5 @@ +export const dataLayer = { + getBusinesses(database: any) : any { + return database.businesses; + } +}; diff --git a/src/database/firestore.ts b/src/database/firestore.ts new file mode 100644 index 0000000..5f67331 --- /dev/null +++ b/src/database/firestore.ts @@ -0,0 +1,14 @@ +import firebase from "firebase"; + +let firebaseConfig = { + apiKey: "AIzaSyA6GO5fwNJrBklVKvYQ9LJSIkznxB6oy4M", + authDomain: "ranlab-mvp.firebaseapp.com", + databaseURL: "https://ranlab-mvp.firebaseio.com", + projectId: "ranlab-mvp", + storageBucket: "ranlab-mvp.appspot.com", + messagingSenderId: "234026995986", + appId: "1:234026995986:web:ed82d23d535394775563cb" +}; + +export const app = firebase.initializeApp(firebaseConfig); +export const firestore = app.firestore(); diff --git a/src/endpoints/ping.test.ts b/src/endpoints/ping.test.ts deleted file mode 100644 index 42d8799..0000000 --- a/src/endpoints/ping.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { strict as assert } from 'assert'; -import baretest from 'baretest'; -import fastify from 'fastify'; -import path from 'path'; -import createPingEndpoint from './ping'; - -const test = baretest(path.basename(__filename, '.test.js')); - -test('returns a status code of 200', async () => { - const app = createPingEndpoint(fastify()); - const response = await app.inject({ method: 'GET', url: '/ping' }); - - assert.equal(response.statusCode, 200); - - await app.close(); -}); - -(async () => { process.exitCode = (await test.run()) ? 0 : 1; })(); diff --git a/src/endpoints/regionBusinesses.ts b/src/endpoints/regionBusinesses.ts new file mode 100644 index 0000000..f5b142e --- /dev/null +++ b/src/endpoints/regionBusinesses.ts @@ -0,0 +1,27 @@ +import type {FastifyInstance, RequestGenericInterface} from 'fastify'; +import {firestore} from "../database/firestore"; + +interface RegionBusinessRequest extends RequestGenericInterface { + Params: { + regionId: number + } +} + +export default function createRegionBusinessesEndpoint(app: FastifyInstance) { + app.get('/regions/:regionId/businesses', + async (request) => { + let response = { + status: "ok", + date: Date.now(), + region: request.params.regionId, + businesses: [] + } + let businessSnapshot = await firestore.collection("businesses").where("region", "==", request.params.regionId).get(); + response.businesses = await businessSnapshot.docs.map((b) => ({'id': b.id, 'data': b.data()})); + console.log(response); + return JSON.stringify(response); + } + ); + + return app; +} diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts new file mode 100644 index 0000000..3ebc917 --- /dev/null +++ b/src/endpoints/regions.ts @@ -0,0 +1,20 @@ +import type { FastifyInstance } from 'fastify'; +import {firestore} from "../database/firestore"; + +export default function createRegionsEndpoint(app: FastifyInstance) { + app.get('/regions', + async () => { + let response = { + status: "ok", + date: Date.now(), + regions: [] + } + let regionsSnapshot = await firestore.collection("regions").get(); + response.regions = await regionsSnapshot.docs.map((r) => ({'id': r.id, 'data': r.data()})); + console.log(response); + return JSON.stringify(response); + } + ); + + return app; +} diff --git a/src/endpoints/userRegions.ts b/src/endpoints/userRegions.ts new file mode 100644 index 0000000..52d2bc0 --- /dev/null +++ b/src/endpoints/userRegions.ts @@ -0,0 +1,27 @@ +import type {FastifyInstance, RequestGenericInterface} from 'fastify'; +import {firestore} from "../database/firestore"; + +interface UserRegionsRequest extends RequestGenericInterface { + Params: { + userId: string + } +} + +export default function createUserRegionsEndpoint(app: FastifyInstance) { + app.get('/users/:userId/regions', + async (request) => { + let userId = request.params.userId; + let response = { + status: "ok", + date: Date.now(), + user: userId, + regions: [] + } + let regionsSnapshot = await firestore.collection("regions").where("manager", "==", userId).get(); + response.regions = await regionsSnapshot.docs.map((b) => ({'id': b.id, 'data': b.data()})); + return JSON.stringify(response); + } + ); + + return app; +} diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts new file mode 100644 index 0000000..e887917 --- /dev/null +++ b/tests/dataLayer.test.ts @@ -0,0 +1,7 @@ +import {dataLayer} from "../src/database/dataLayer"; + +test("Gets expected data", () => { + let out = dataLayer.getBusinesses({businesses: [{name:"dummy"}]}); + + expect(out).toStrictEqual([{name:"dummy"}]); +}); diff --git a/tests/ping.test.ts b/tests/ping.test.ts new file mode 100644 index 0000000..5079532 --- /dev/null +++ b/tests/ping.test.ts @@ -0,0 +1,11 @@ +import fastify from 'fastify'; +import createPingEndpoint from '../src/endpoints/ping'; + +test('returns a status code of 200', async () => { + const app = createPingEndpoint(fastify()); + const response = await app.inject({ method: 'GET', url: '/ping' }); + + expect(response.statusCode).toBe(200); + + await app.close(); +}); diff --git a/tests/regionBusinesses.test.ts b/tests/regionBusinesses.test.ts new file mode 100644 index 0000000..df33965 --- /dev/null +++ b/tests/regionBusinesses.test.ts @@ -0,0 +1,12 @@ +import createRegionBusinessesEndpoint from "../src/endpoints/regionBusinesses"; +import {fastify} from "fastify"; + +test('returns a status code of 200', async (done) => { + const app = createRegionBusinessesEndpoint(fastify()); + const response = await app.inject({ method: 'GET', url: '/regions/Bay%20Bulls/businesses' }); + + expect(response.statusCode).toBe(200); + + await app.close(); + done(); +}); diff --git a/tests/regions.test.ts b/tests/regions.test.ts new file mode 100644 index 0000000..dc2ee7a --- /dev/null +++ b/tests/regions.test.ts @@ -0,0 +1,11 @@ +import createRegionsEndpoint from "../src/endpoints/regions"; +import {fastify} from "fastify"; + +test('returns a status code of 200', async () => { + const app = createRegionsEndpoint(fastify()); + const response = await app.inject({ method: 'GET', url: '/regions' }); + + expect(response.statusCode).toBe(200); + + await app.close(); +}); diff --git a/tests/userRegions.test.ts b/tests/userRegions.test.ts new file mode 100644 index 0000000..03ac52c --- /dev/null +++ b/tests/userRegions.test.ts @@ -0,0 +1,16 @@ +import {fastify} from "fastify"; +import createUserRegionsEndpoint from "../src/endpoints/userRegions"; + + +test('returns a status code of 200', async () => { + const app = createUserRegionsEndpoint(fastify()); + const response = await app.inject({ method: 'GET', url: '/users/joe/regions' }); + console.log(response); + expect(response.statusCode).toBe(200); + const payload: { date: Date; regions: string[] } = JSON.parse( + response.payload + ); + expect(payload.regions.length).toBe(1); + + await app.close(); +}); diff --git a/tsconfig.json b/tsconfig.json index 1fd317d..c4a1207 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Basic Options */ "incremental": true, "target": "ES2017", - "module": "CommonJS", + "module": "ES6", "declaration": true, /* Strict Type-Checking Options */ diff --git a/yarn.lock b/yarn.lock index a35b0e1..662a798 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,939 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/core@^7.1.0", "@babel/core@^7.7.5": + version "7.12.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" + integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.1" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.1" + "@babel/parser" "^7.12.3" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.12.1", "@babel/generator@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" + integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== + dependencies: + "@babel/types" "^7.12.5" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-member-expression-to-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" + integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== + dependencies: + "@babel/types" "^7.12.1" + +"@babel/helper-module-imports@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" + integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== + dependencies: + "@babel/types" "^7.12.5" + +"@babel/helper-module-transforms@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + lodash "^4.17.19" + +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + +"@babel/helper-replace-supers@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9" + integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + +"@babel/helper-simple-access@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== + dependencies: + "@babel/types" "^7.12.1" + +"@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helpers@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" + integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.12.3", "@babel/parser@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" + integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" + integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/template@^7.10.4", "@babel/template@^7.3.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" + integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.12.5" + "@babel/types" "^7.12.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.12.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" + integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@firebase/analytics-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz#d6716f9fa36a6e340bc0ecfe68af325aa6f60508" + integrity sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA== + +"@firebase/analytics@0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.6.2.tgz#7f45675a1b524fff4d9e9fe318fd6e2ed067a325" + integrity sha512-4Ceov+rPfOEPIdbjlpTim/wbcUUneIesHag4UOzvmFsRRXqbxLwQpyZQWEbTSriUeU8uTKj9yOW32hsskV9Klg== + dependencies: + "@firebase/analytics-types" "0.4.0" + "@firebase/component" "0.1.21" + "@firebase/installations" "0.4.19" + "@firebase/logger" "0.2.6" + "@firebase/util" "0.3.4" + tslib "^1.11.1" + +"@firebase/app-types@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.1.tgz#dcbd23030a71c0c74fc95d4a3f75ba81653850e9" + integrity sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg== + +"@firebase/app@0.6.13": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.6.13.tgz#f2e9fa9e75815e54161dc34659a60f1fffd9a450" + integrity sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ== + dependencies: + "@firebase/app-types" "0.6.1" + "@firebase/component" "0.1.21" + "@firebase/logger" "0.2.6" + "@firebase/util" "0.3.4" + dom-storage "2.1.0" + tslib "^1.11.1" + xmlhttprequest "1.8.0" + +"@firebase/auth-interop-types@0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz#9fc9bd7c879f16b8d1bb08373a0f48c3a8b74557" + integrity sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw== + +"@firebase/auth-types@0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.1.tgz#7815e71c9c6f072034415524b29ca8f1d1770660" + integrity sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw== + +"@firebase/auth@0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.2.tgz#9ada3f37620d131a1c56994138a599b5c9f9ca2e" + integrity sha512-2n32PBi6x9jVhc0E/ewKLUCYYTzFEXL4PNkvrrlGKbzeTBEkkyzfgUX7OV9UF5wUOG+gurtUthuur1zspZ/9hg== + dependencies: + "@firebase/auth-types" "0.10.1" + +"@firebase/component@0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.1.21.tgz#56062eb0d449dc1e7bbef3c084a9b5fa48c7c14d" + integrity sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg== + dependencies: + "@firebase/util" "0.3.4" + tslib "^1.11.1" + +"@firebase/database-types@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.6.0.tgz#7795bc6b1db93f4cbda9a241c8dfe1bb86033dc6" + integrity sha512-ljpU7/uboCGqFSe9CNgwd3+Xu5N8YCunzfPpeueuj2vjnmmypUi4QWxgC3UKtGbuv1q+crjeudZGLxnUoO0h7w== + dependencies: + "@firebase/app-types" "0.6.1" + +"@firebase/database@0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.7.1.tgz#900d2e6ed734249e65e5f159293830e4f4285d6e" + integrity sha512-8j3KwksaYMSbIsEjOIarZD3vj4jGJjIlLGIAiO/4P4XyOtrlnxIiH7G0UdIZlcvKU4Gsgg0nthT2+EapROmHWA== + dependencies: + "@firebase/auth-interop-types" "0.1.5" + "@firebase/component" "0.1.21" + "@firebase/database-types" "0.6.0" + "@firebase/logger" "0.2.6" + "@firebase/util" "0.3.4" + faye-websocket "0.11.3" + tslib "^1.11.1" + +"@firebase/firestore-types@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.0.0.tgz#1f6212553b240f1a8905bb8dcf1f87769138c5c0" + integrity sha512-ZGb7p1SSQJP0Z+kc9GAUi+Fx5rJatFddBrS1ikkayW+QHfSIz0omU23OgSHcBGTxe8dJCeKiKA2Yf+tkDKO/LA== + +"@firebase/firestore@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.0.2.tgz#300e7430a08d8c8779471e43c892a2cefe405b62" + integrity sha512-6kO/vWUmTOANA/ql+i16DFMc63gamU76Nycyt7k0r8QfcdXu93Cwizw4ff4DNMnpnkAJkTk36fPAxBxEvBXkzw== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/firestore-types" "2.0.0" + "@firebase/logger" "0.2.6" + "@firebase/util" "0.3.4" + "@firebase/webchannel-wrapper" "0.4.0" + "@grpc/grpc-js" "^1.0.0" + "@grpc/proto-loader" "^0.5.0" + node-fetch "2.6.1" + tslib "^1.11.1" + +"@firebase/functions-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.4.0.tgz#0b789f4fe9a9c0b987606c4da10139345b40f6b9" + integrity sha512-3KElyO3887HNxtxNF1ytGFrNmqD+hheqjwmT3sI09FaDCuaxGbOnsXAXH2eQ049XRXw9YQpHMgYws/aUNgXVyQ== + +"@firebase/functions@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.6.1.tgz#32640b8f877637057dfaaeb122be8c8e99ad1af7" + integrity sha512-xNCAY3cLlVWE8Azf+/84OjnaXMoyUstJ3vwVRG0ie22QhsdQuPa1tXTiPX4Tmm+Hbbd/Aw0A/7dkEnuW+zYzaQ== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/functions-types" "0.4.0" + "@firebase/messaging-types" "0.5.0" + node-fetch "2.6.1" + tslib "^1.11.1" + +"@firebase/installations-types@0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.3.4.tgz#589a941d713f4f64bf9f4feb7f463505bab1afa2" + integrity sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q== + +"@firebase/installations@0.4.19": + version "0.4.19" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.4.19.tgz#53f50aeb022996963f89f59560d7b4cf801869da" + integrity sha512-QqAQzosKVVqIx7oMt5ujF4NsIXgtlTnej4JXGJ8sQQuJoMnt3T+PFQRHbr7uOfVaBiHYhEaXCcmmhfKUHwKftw== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/installations-types" "0.3.4" + "@firebase/util" "0.3.4" + idb "3.0.2" + tslib "^1.11.1" + +"@firebase/logger@0.2.6": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" + integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== + +"@firebase/messaging-types@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.5.0.tgz#c5d0ef309ced1758fda93ef3ac70a786de2e73c4" + integrity sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg== + +"@firebase/messaging@0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.7.3.tgz#31dded892455e4d0680e1452ff2fbfdfb9e4ce9b" + integrity sha512-63nOP2SmQJrj9jrhV3K96L5MRKS6AqmFVLX1XbGk6K6lz38ZC4LIoCcHxzUBXY7fCAuZvNmh/YB3pE8B2mTs8A== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/installations" "0.4.19" + "@firebase/messaging-types" "0.5.0" + "@firebase/util" "0.3.4" + idb "3.0.2" + tslib "^1.11.1" + +"@firebase/performance-types@0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.13.tgz#58ce5453f57e34b18186f74ef11550dfc558ede6" + integrity sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA== + +"@firebase/performance@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.4.4.tgz#5f13ea3b9a72a0ae9c36520c419be31448a0955a" + integrity sha512-CY/fzz7qGQ9hUkvOow22MeJhayHSjXmI4+0AqcxaUC4CWk4oQubyIC4pk62aH+yCwZNNeC7JJUEDbtqI/0rGkQ== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/installations" "0.4.19" + "@firebase/logger" "0.2.6" + "@firebase/performance-types" "0.0.13" + "@firebase/util" "0.3.4" + tslib "^1.11.1" + +"@firebase/polyfill@0.3.36": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.36.tgz#c057cce6748170f36966b555749472b25efdb145" + integrity sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg== + dependencies: + core-js "3.6.5" + promise-polyfill "8.1.3" + whatwg-fetch "2.0.4" + +"@firebase/remote-config-types@0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz#fe6bbe4d08f3b6e92fce30e4b7a9f4d6a96d6965" + integrity sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA== + +"@firebase/remote-config@0.1.30": + version "0.1.30" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.30.tgz#2cd6bbbed526a98b154e13a2cc73e748a77d7c3d" + integrity sha512-LAfLDcp1AN0V/7AkxBuTKy+Qnq9fKYKxbA5clrXRNVzJbTVnF5eFGsaUOlkes0ESG6lbqKy5ZcDgdl73zBIhAA== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/installations" "0.4.19" + "@firebase/logger" "0.2.6" + "@firebase/remote-config-types" "0.1.9" + "@firebase/util" "0.3.4" + tslib "^1.11.1" + +"@firebase/storage-types@0.3.13": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.13.tgz#cd43e939a2ab5742e109eb639a313673a48b5458" + integrity sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog== + +"@firebase/storage@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.4.2.tgz#bc5924b87bd2fdd4ab0de49851c0125ebc236b89" + integrity sha512-87CrvKrf8kijVekRBmUs8htsNz7N5X/pDhv3BvJBqw8K65GsUolpyjx0f4QJRkCRUYmh3MSkpa5P08lpVbC6nQ== + dependencies: + "@firebase/component" "0.1.21" + "@firebase/storage-types" "0.3.13" + "@firebase/util" "0.3.4" + tslib "^1.11.1" + +"@firebase/util@0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.3.4.tgz#e389d0e0e2aac88a5235b06ba9431db999d4892b" + integrity sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ== + dependencies: + tslib "^1.11.1" + +"@firebase/webchannel-wrapper@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz#becce788818d3f47f0ac1a74c3c061ac1dcf4f6d" + integrity sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ== + +"@grpc/grpc-js@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.0.tgz#4ff1ac4cdf7eb2af80d3c67316be9c2308d8d9bf" + integrity sha512-09H50V7rmz0gFrGz6IbP49z9A8+2p4yZYcNDEb7bytr90vWn52VBQE1a+LMBlrucmNN0wSsiCr3TJx+dStHTng== + dependencies: + "@types/node" "^12.12.47" + google-auth-library "^6.1.1" + semver "^6.2.0" + +"@grpc/proto-loader@^0.5.0": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.5.tgz#6725e7a1827bdf8e92e29fbf4e9ef0203c0906a9" + integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== + dependencies: + lodash.camelcase "^4.3.0" + protobufjs "^6.8.6" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^26.6.2" + jest-util "^26.6.2" + slash "^3.0.0" + +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" + micromatch "^4.0.2" + p-each-series "^2.1.0" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + dependencies: + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== + dependencies: + "@jest/types" "^26.6.2" + "@sinonjs/fake-timers" "^6.0.1" + "@types/node" "*" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" + +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.4" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^4.0.3" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^7.0.0" + optionalDependencies: + node-notifier "^8.0.0" + +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.4" + source-map "^0.6.0" + +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== + dependencies: + "@jest/test-result" "^26.6.2" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^26.6.2" + babel-plugin-istanbul "^6.0.0" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-regex-util "^26.0.0" + jest-util "^26.6.2" + micromatch "^4.0.2" + pirates "^4.0.1" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + +"@sinonjs/commons@^1.7.0": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" + integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": + version "7.1.12" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" + integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" + integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz#0c888dd70b3ee9eebb6e4f200e809da0076262be" + integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" + integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== + dependencies: + "@babel/types" "^7.3.0" + +"@types/graceful-fs@^4.1.2": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.4.tgz#4ff9f641a7c6d1a3508ff88bc3141b152772e753" + integrity sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" + integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@26.x", "@types/jest@^26.0.15": + version "26.0.15" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" + integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + +"@types/node@*": + version "14.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" + integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== + +"@types/node@^12.12.47": + version "12.19.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" + integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== + "@types/node@^12.12.67": version "12.12.67" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== +"@types/node@^13.7.0": + version "13.13.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.30.tgz#1ed6e01e4ca576d5aec9cc802cc3bcf94c274192" + integrity sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA== + +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/prettier@^2.0.0": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" + integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== + +"@types/stack-utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" + integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + +"@types/yargs@^15.0.0": + version "15.0.9" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19" + integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g== + dependencies: + "@types/yargs-parser" "*" + +abab@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + abstract-logging@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.0.tgz#08a85814946c98ef06f4256ad470aba1886d4490" integrity sha512-/oA9z7JszpIioo6J6dB79LVUgJ3eD3cxkAmdCkvWWS+Y9tPtALs1rLqOekLUXUbYqM2fB9TTK0ibAyZJJOP/CA== +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv@^6.11.0, ajv@^6.12.2: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -22,11 +945,122 @@ ajv@^6.11.0, ajv@^6.12.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -42,6 +1076,82 @@ avvio@^7.1.2: fastq "^1.6.1" queue-microtask "^1.1.2" +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== + dependencies: + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__core" "^7.1.7" + babel-plugin-istanbul "^6.0.0" + babel-preset-jest "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + +babel-plugin-istanbul@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" + integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^4.0.0" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" + integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== + dependencies: + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + barecolor@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/barecolor/-/barecolor-1.0.1.tgz#1bbbbfc41cedecf74e23fb7d9d054cd8763d92cc" @@ -54,23 +1164,604 @@ baretest@^2.0.0: dependencies: barecolor "1.0.1" -cookie@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -debug@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== dependencies: - ms "2.1.2" + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bignumber.js@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" + integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-from@1.x, buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +cookie@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-js@3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" + integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decimal.js@^10.2.0: + version "10.2.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" + integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + +dom-storage@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39" + integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q== + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +emittery@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" + integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escodegen@^1.14.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +exec-sh@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== + dependencies: + "@jest/types" "^26.6.2" + ansi-styles "^4.0.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.2, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + fast-decode-uri-component@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" @@ -81,7 +1772,7 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -95,6 +1786,11 @@ fast-json-stringify@^2.2.1: deepmerge "^4.2.2" string-similarity "^4.0.1" +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + fast-redact@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d" @@ -105,6 +1801,11 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== +fast-text-encoding@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== + fastify-error@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" @@ -144,6 +1845,37 @@ fastq@^1.6.1: dependencies: reusify "^1.0.4" +faye-websocket@0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + find-my-way@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-3.0.4.tgz#a485973d1a3fdafd989ac9f12fd2d88e83cda268" @@ -153,46 +1885,1516 @@ find-my-way@^3.0.0: safe-regex2 "^2.0.0" semver-store "^0.3.0" +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +firebase@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.0.2.tgz#7bc8a220ce6b206121c4a9740eca1ad4be23ff86" + integrity sha512-tPtXQ8sifo82f7bOxYcR/yEdJ4IbL4/fpQrophRHFAaYCsYGp2Q/c1zz0voX9cLap8MH2uwh5LIVBqZ8nyT5ZQ== + dependencies: + "@firebase/analytics" "0.6.2" + "@firebase/app" "0.6.13" + "@firebase/app-types" "0.6.1" + "@firebase/auth" "0.15.2" + "@firebase/database" "0.7.1" + "@firebase/firestore" "2.0.2" + "@firebase/functions" "0.6.1" + "@firebase/installations" "0.4.19" + "@firebase/messaging" "0.7.3" + "@firebase/performance" "0.4.4" + "@firebase/polyfill" "0.3.36" + "@firebase/remote-config" "0.1.30" + "@firebase/storage" "0.4.2" + "@firebase/util" "0.3.4" + flatstr@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= -inherits@^2.0.3: +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.2.1.tgz#1fb02ded2036a8ac288d507a65962bd87b97628d" + integrity sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gaxios@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.0.1.tgz#bc7b205a89d883452822cc75e138620c35e3291e" + integrity sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.3.0" + +gcp-metadata@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" + integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +google-auth-library@^6.1.1: + version "6.1.3" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.3.tgz#39d868140b70d0c4b32c6f6d8f4ccc1400d84dca" + integrity sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-p12-pem@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" + integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== + dependencies: + node-forge "^0.10.0" + +graceful-fs@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +gtoken@^5.0.4: + version "5.0.5" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.0.5.tgz#e752d18538576777dfe237887e30fc0627870eae" + integrity sha512-wvjkecutFh8kVfbcdBdUWqDRrXb+WrgD79DBDEYf1Om8S1FluhylhtFjrL7Tx69vNhh259qA3Q1P4sPtb+kUYw== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.0.3" + jws "^4.0.0" + mime "^2.2.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-parser-js@>=0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +idb@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384" + integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw== + +import-local@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" + integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-docker@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" + integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-potential-custom-element-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" + integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + +istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" + integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== + dependencies: + "@jest/types" "^26.6.2" + execa "^4.0.0" + throat "^5.0.0" + +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== + dependencies: + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + import-local "^3.0.2" + is-ci "^2.0.0" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" + prompts "^2.0.1" + yargs "^15.4.1" + +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" + chalk "^4.0.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.4" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.6.3" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + micromatch "^4.0.2" + pretty-format "^26.6.2" + +jest-diff@^26.0.0, jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-docblock@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" + integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== + dependencies: + detect-newline "^3.0.0" + +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + jest-get-type "^26.3.0" + jest-util "^26.6.2" + pretty-format "^26.6.2" + +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jsdom "^16.4.0" + +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== + +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== + dependencies: + "@jest/types" "^26.6.2" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + jest-regex-util "^26.0.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + micromatch "^4.0.2" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.1.2" + +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^26.6.2" + is-generator-fn "^2.0.0" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" + throat "^5.0.0" + +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== + dependencies: + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== + dependencies: + chalk "^4.0.0" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" + integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== + +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== + dependencies: + "@jest/types" "^26.6.2" + jest-regex-util "^26.0.0" + jest-snapshot "^26.6.2" + +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + jest-pnp-resolver "^1.2.2" + jest-util "^26.6.2" + read-pkg-up "^7.0.1" + resolve "^1.18.1" + slash "^3.0.0" + +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.7.1" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-docblock "^26.0.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" + source-map-support "^0.5.6" + throat "^5.0.0" + +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + cjs-module-lexer "^0.6.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + slash "^3.0.0" + strip-bom "^4.0.0" + yargs "^15.4.1" + +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" + +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.6.2" + graceful-fs "^4.2.4" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + natural-compare "^1.4.0" + pretty-format "^26.6.2" + semver "^7.3.2" + +jest-util@^26.1.0, jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== + dependencies: + "@jest/types" "^26.6.2" + camelcase "^6.0.0" + chalk "^4.0.0" + jest-get-type "^26.3.0" + leven "^3.1.0" + pretty-format "^26.6.2" + +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== + dependencies: + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^26.6.2" + string-length "^4.0.1" + +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== + dependencies: + "@jest/core" "^26.6.3" + import-local "^3.0.2" + jest-cli "^26.6.3" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^16.4.0: + version "16.4.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" + integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== + dependencies: + abab "^2.0.3" + acorn "^7.1.1" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.2.0" + data-urls "^2.0.0" + decimal.js "^10.2.0" + domexception "^2.0.1" + escodegen "^1.14.1" + html-encoding-sniffer "^2.0.1" + is-potential-custom-element-name "^1.0.0" + nwsapi "^2.2.0" + parse5 "5.1.1" + request "^2.88.2" + request-promise-native "^1.0.8" + saxes "^5.0.0" + symbol-tree "^3.2.4" + tough-cookie "^3.0.1" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + ws "^7.2.3" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@2.x, json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +light-my-request@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.1.1.tgz#71a4f5742affd21d54e70895b6c6cec08feb9bad" + integrity sha512-4H0T0PQcFB/fGTIkNV5ShuftWnuUKdtLWq5t2zt+lwMWRZkVviTfmJqGOXeAAqkdREnGJQXa8zJ4wXJ0LrzrTA== + dependencies: + ajv "^6.12.2" + cookie "^0.4.0" + fastify-warning "^0.2.0" + readable-stream "^3.6.0" + set-cookie-parser "^2.4.1" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@^2.2.0: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@1.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch@2.6.1, node-fetch@^2.3.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620" + integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nwsapi@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +p-each-series@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" + integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" + integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== -light-my-request@^4.0.2: - version "4.1.1" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.1.1.tgz#71a4f5742affd21d54e70895b6c6cec08feb9bad" - integrity sha512-4H0T0PQcFB/fGTIkNV5ShuftWnuUKdtLWq5t2zt+lwMWRZkVviTfmJqGOXeAAqkdREnGJQXa8zJ4wXJ0LrzrTA== - dependencies: - ajv "^6.12.2" - cookie "^0.4.0" - fastify-warning "^0.2.0" - readable-stream "^3.6.0" - set-cookie-parser "^2.4.1" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +picomatch@^2.0.4, picomatch@^2.0.5: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== pino-std-serializers@^2.4.2: version "2.5.0" @@ -211,6 +3413,72 @@ pino@^6.2.1: quick-format-unescaped "^4.0.1" sonic-boom "^1.0.2" +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +pretty-format@^26.0.0, pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + +promise-polyfill@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" + integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== + +prompts@^2.0.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" + integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +protobufjs@^6.8.6: + version "6.10.1" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" + integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" "^13.7.0" + long "^4.0.0" + proxy-addr@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -219,11 +3487,29 @@ proxy-addr@^2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" -punycode@^2.1.0: +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + queue-microtask@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.4.tgz#40841ace4356b48b35b5ea61a2e1fe0a23c59ce1" @@ -234,6 +3520,30 @@ quick-format-unescaped@^4.0.1: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701" integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -243,6 +3553,111 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== + dependencies: + lodash "^4.17.19" + +request-promise-native@^1.0.8: + version "1.0.9" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" + integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== + dependencies: + request-promise-core "1.1.4" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.10.0, resolve@^1.18.1, resolve@^1.3.2: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + ret@~0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" @@ -258,11 +3673,28 @@ rfdc@^1.1.4: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== -safe-buffer@~5.2.0: +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex2@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" @@ -270,6 +3702,40 @@ safe-regex2@^2.0.0: dependencies: ret "~0.2.0" +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +saxes@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + secure-json-parse@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20" @@ -280,16 +3746,115 @@ semver-store@^0.3.0: resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== -semver@^7.3.2: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@7.x, semver@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + set-cookie-parser@^2.4.1: version "2.4.6" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz#43bdea028b9e6f176474ee5298e758b4a44799c3" integrity sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg== +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + sonic-boom@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.3.0.tgz#5c77c846ce6c395dddf2eb8e8e65f9cc576f2e76" @@ -298,11 +3863,140 @@ sonic-boom@^1.0.2: atomic-sleep "^1.0.0" flatstr "^1.0.12" +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.6: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-utils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" + integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== + dependencies: + escape-string-regexp "^2.0.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string-length@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" + integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + string-similarity@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.2.tgz#1dc29518d0e92e50509499ce2974368f77bbb1b1" integrity sha512-eCsPPyoQBgY4TMpVD6DVfO7pLrimUONriaO4Xjp3WPUW0YnNLqdHgRj23xotLlqrL90eJhBeq3zdAJf2mQgfBQ== +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -310,16 +4004,239 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" + integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + tiny-lru@^7.0.0: version "7.0.6" resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow== +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +tough-cookie@^2.3.3, tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" + integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== + dependencies: + ip-regex "^2.1.0" + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" + integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== + dependencies: + punycode "^2.1.1" + +ts-jest@^26.4.4: + version "26.4.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49" + integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg== + dependencies: + "@types/jest" "26.x" + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + jest-util "^26.1.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "20.x" + +tslib@^1.11.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typescript@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + uri-js@^4.2.2: version "4.4.0" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" @@ -327,7 +4244,232 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.3.0: + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== + +v8-to-istanbul@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" + integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^8.0.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" + integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^2.0.2" + webidl-conversions "^6.1.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +ws@^7.2.3: + version "7.4.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" + integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xmlhttprequest@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" + integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@20.x: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" From b567174adddbd00ab866cabdb98e9b794d69a582 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Wed, 25 Nov 2020 00:43:32 -0330 Subject: [PATCH 04/75] Added the post for businesses Added the get for filters (supports years only right now) --- src/endpoints/businesses.ts | 36 +++++++++++++++++++++++++++++++ src/endpoints/filters.ts | 26 ++++++++++++++++++++++ src/endpoints/regionBusinesses.ts | 4 ++-- tests/businesses.test.ts | 26 ++++++++++++++++++++++ tests/filters.test.ts | 13 +++++++++++ tests/regionBusinesses.test.ts | 5 +++-- 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/endpoints/businesses.ts create mode 100644 src/endpoints/filters.ts create mode 100644 tests/businesses.test.ts create mode 100644 tests/filters.test.ts diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts new file mode 100644 index 0000000..ecdd064 --- /dev/null +++ b/src/endpoints/businesses.ts @@ -0,0 +1,36 @@ +import {firestore} from "../database/firestore"; +import {FastifyInstance, RequestGenericInterface} from "fastify"; +import firebase from "firebase"; +import GeoPoint = firebase.firestore.GeoPoint; + +interface CreateBusinessRequest extends RequestGenericInterface { + Body: Business +} + +export interface Business { + id: string | undefined; + name: string; + employees: number; + region: string; + year_added: number; + location: GeoPoint +} + +export default function createBusinessesEndpoint(app: FastifyInstance) { + app.post( + '/businesses', + async (request) => { + let bc = firestore.collection("businesses"); + let businessRef = request.body.id ? bc.doc(request.body.id) : bc.doc(); + await businessRef.set(request.body); + await firestore.collection("years").doc(`${request.body.year_added}`).set({}); + await firestore.collection("regions").doc(request.body.region).set({}); + let response = { + status: "ok", + date: Date.now(), + businessId: businessRef.id + }; + return JSON.stringify(response); + }); + return app; +} diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts new file mode 100644 index 0000000..f43b43e --- /dev/null +++ b/src/endpoints/filters.ts @@ -0,0 +1,26 @@ +import type {FastifyInstance, RequestGenericInterface} from 'fastify'; +import {firestore} from "../database/firestore"; + +interface GetFiltersRequest extends RequestGenericInterface { + Params: { + regionId: number + } +} + +export default function createFiltersEndpoint(app: FastifyInstance) { + app.get('/filters', + async () => { + let response = { + status: "ok", + date: Date.now(), + years: [] + } + let yearsSnapshot = await firestore.collection("years").get(); + response.years = await yearsSnapshot.docs.map((b) => Number.parseInt(b.id)); + console.log(response); + return JSON.stringify(response); + } + ); + + return app; +} diff --git a/src/endpoints/regionBusinesses.ts b/src/endpoints/regionBusinesses.ts index f5b142e..e14a227 100644 --- a/src/endpoints/regionBusinesses.ts +++ b/src/endpoints/regionBusinesses.ts @@ -1,14 +1,14 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {firestore} from "../database/firestore"; -interface RegionBusinessRequest extends RequestGenericInterface { +interface GetRegionBusinessRequest extends RequestGenericInterface { Params: { regionId: number } } export default function createRegionBusinessesEndpoint(app: FastifyInstance) { - app.get('/regions/:regionId/businesses', + app.get('/regions/:regionId/businesses', async (request) => { let response = { status: "ok", diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts new file mode 100644 index 0000000..cf5a67e --- /dev/null +++ b/tests/businesses.test.ts @@ -0,0 +1,26 @@ +import createRegionBusinessesEndpoint from "../src/endpoints/regionBusinesses"; +import {fastify} from "fastify"; +import createBusinessesEndpoint from "../src/endpoints/businesses"; + +test('creates and retrieves a valid business', async(done) => { + const app1 = createBusinessesEndpoint(fastify()); + const app2 = createRegionBusinessesEndpoint(fastify()); + const response1 = await app1.inject({ + method: 'POST', + url: '/businesses', + payload: {name: "DummyBiz", region: "DummyRegion", year_added: 2009} + }); + expect(response1.statusCode).toBe(200); + + const response2 = await app2.inject({ + method: 'GET', + url: '/regions/DummyRegion/businesses' + }); + + expect(response2.statusCode).toBe(200); + expect(JSON.parse(response2.payload).businesses).toEqual(expect.arrayContaining(expect.objectContaining({name: "DummyBiz"}))); + + await app1.close(); + await app2.close(); + done(); +}) diff --git a/tests/filters.test.ts b/tests/filters.test.ts new file mode 100644 index 0000000..42b5210 --- /dev/null +++ b/tests/filters.test.ts @@ -0,0 +1,13 @@ +import {fastify} from "fastify"; +import createFiltersEndpoint from "../src/endpoints/filters"; + +test('Returns the prebaked data', async (done) => { + const app = createFiltersEndpoint(fastify()); + const response = await app.inject({ method: 'GET', url: '/filters' }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).years).toContain(2019); + await app.close(); + done(); +}); + diff --git a/tests/regionBusinesses.test.ts b/tests/regionBusinesses.test.ts index df33965..9eaf7d0 100644 --- a/tests/regionBusinesses.test.ts +++ b/tests/regionBusinesses.test.ts @@ -1,12 +1,13 @@ import createRegionBusinessesEndpoint from "../src/endpoints/regionBusinesses"; import {fastify} from "fastify"; -test('returns a status code of 200', async (done) => { +test('Returns the prebaked data', async (done) => { const app = createRegionBusinessesEndpoint(fastify()); const response = await app.inject({ method: 'GET', url: '/regions/Bay%20Bulls/businesses' }); expect(response.statusCode).toBe(200); - + expect(JSON.parse(response.payload).businesses.length).toBe(1); await app.close(); done(); }); + From 99cf387878af4e6999c25680b7614a6e89da1ee6 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 27 Nov 2020 00:02:46 -0330 Subject: [PATCH 05/75] Moved Firestore data manipulation into ProductionDataLayer Cleaned up tests and endpoints accordingly --- src/database/dataLayer.ts | 59 ++++++++++++++++++++++++++++--- src/endpoints/businesses.ts | 54 ++++++++++++++++++---------- src/endpoints/filters.ts | 11 +++--- src/endpoints/regionBusinesses.ts | 27 -------------- src/endpoints/regions.ts | 22 +++++++----- src/endpoints/userRegions.ts | 27 -------------- tests/businesses.test.ts | 17 +++++---- tests/dataLayer.test.ts | 42 +++++++++++++++++++--- tests/filters.test.ts | 25 +++++++++---- tests/regionBusinesses.test.ts | 13 ------- tests/regions.test.ts | 16 ++++++--- tests/userRegions.test.ts | 16 --------- tests/utils/dummyDatalayer.ts | 33 +++++++++++++++++ 13 files changed, 218 insertions(+), 144 deletions(-) delete mode 100644 src/endpoints/regionBusinesses.ts delete mode 100644 src/endpoints/userRegions.ts delete mode 100644 tests/regionBusinesses.test.ts delete mode 100644 tests/userRegions.test.ts create mode 100644 tests/utils/dummyDatalayer.ts diff --git a/src/database/dataLayer.ts b/src/database/dataLayer.ts index 8cf1781..e8ec179 100644 --- a/src/database/dataLayer.ts +++ b/src/database/dataLayer.ts @@ -1,5 +1,56 @@ -export const dataLayer = { - getBusinesses(database: any) : any { - return database.businesses; +import {Business} from "../endpoints/businesses"; +import {firestore} from "./firestore"; + +export interface IdObject { + id: string +} + +export interface Region { + id: string, + manager: string +} + +export interface DataLayer { + setBusiness(business: Business) : Promise; + getBusinessesByRegion(region: string): Promise; + getFilters() : Promise; + getRegionsManagedBy(managerId: string) : Promise; + setRegion(region: Region): Promise; +} + +export interface Filters { + years?: number[] | undefined +} + +export class ProductionDataLayer implements DataLayer { + async getBusinessesByRegion(region: string) : Promise { + let businessSnapshot = await firestore.collection("businesses").where("region", "==", region).get(); + return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); } -}; + + async setBusiness(business: Business) : Promise { + const bc = firestore.collection("businesses"); + let businessRef = business.id ? bc.doc(business.id) : bc.doc(); + await businessRef.set(business); + await firestore.collection("years").doc(`${business.year_added}`).set({}); + return {id: businessRef.id}; + } + + async getFilters() : Promise{ + return { + years: [2019] + } + } + + async getRegionsManagedBy(managerId: string): Promise { + let regionsSnapshot = await firestore.collection("regions").where("manager", "==", managerId).get(); + return regionsSnapshot.docs.map((r) => ({id: r.id, manager: r.data().manager})); + } + + async setRegion(region: Region): Promise { + await firestore.collection("regions").doc(region.id).set({"manager": region.manager}); + return {id: region.id}; + } +} + +export const dataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index ecdd064..7caf471 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -1,36 +1,52 @@ -import {firestore} from "../database/firestore"; -import {FastifyInstance, RequestGenericInterface} from "fastify"; +import type {FastifyInstance, RequestGenericInterface} from 'fastify'; +import {DataLayer} from "../database/dataLayer"; import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; +interface GetRegionBusinessRequest extends RequestGenericInterface { + Params: { + region: string + } +} + interface CreateBusinessRequest extends RequestGenericInterface { Body: Business } export interface Business { - id: string | undefined; + id?: string | undefined; name: string; employees: number; region: string; year_added: number; - location: GeoPoint + location?: GeoPoint | null | undefined } -export default function createBusinessesEndpoint(app: FastifyInstance) { +export default function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { + app.get('/regions/:region/businesses', + async (request) => { + let response = { + status: "ok", + date: Date.now(), + region: request.params.region, + businesses: [] + } + response.businesses = await dataLayer.getBusinessesByRegion(request.params.region); + return JSON.stringify(response); + } + ); + app.post( - '/businesses', - async (request) => { - let bc = firestore.collection("businesses"); - let businessRef = request.body.id ? bc.doc(request.body.id) : bc.doc(); - await businessRef.set(request.body); - await firestore.collection("years").doc(`${request.body.year_added}`).set({}); - await firestore.collection("regions").doc(request.body.region).set({}); - let response = { - status: "ok", - date: Date.now(), - businessId: businessRef.id - }; - return JSON.stringify(response); - }); + '/businesses', + async (request) => { + let businessRef = await dataLayer.setBusiness(request.body); + let response = { + status: "ok", + date: Date.now(), + businessId: businessRef.id + }; + return JSON.stringify(response); + }); + return app; } diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index f43b43e..b3ab987 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,5 +1,5 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {firestore} from "../database/firestore"; +import {DataLayer} from "../database/dataLayer"; interface GetFiltersRequest extends RequestGenericInterface { Params: { @@ -7,17 +7,16 @@ interface GetFiltersRequest extends RequestGenericInterface { } } -export default function createFiltersEndpoint(app: FastifyInstance) { +export default function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/filters', async () => { let response = { status: "ok", date: Date.now(), - years: [] + years: [] } - let yearsSnapshot = await firestore.collection("years").get(); - response.years = await yearsSnapshot.docs.map((b) => Number.parseInt(b.id)); - console.log(response); + + response.years = (await dataLayer.getFilters()).years; return JSON.stringify(response); } ); diff --git a/src/endpoints/regionBusinesses.ts b/src/endpoints/regionBusinesses.ts deleted file mode 100644 index e14a227..0000000 --- a/src/endpoints/regionBusinesses.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {firestore} from "../database/firestore"; - -interface GetRegionBusinessRequest extends RequestGenericInterface { - Params: { - regionId: number - } -} - -export default function createRegionBusinessesEndpoint(app: FastifyInstance) { - app.get('/regions/:regionId/businesses', - async (request) => { - let response = { - status: "ok", - date: Date.now(), - region: request.params.regionId, - businesses: [] - } - let businessSnapshot = await firestore.collection("businesses").where("region", "==", request.params.regionId).get(); - response.businesses = await businessSnapshot.docs.map((b) => ({'id': b.id, 'data': b.data()})); - console.log(response); - return JSON.stringify(response); - } - ); - - return app; -} diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 3ebc917..4777d6b 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -1,17 +1,21 @@ -import type { FastifyInstance } from 'fastify'; -import {firestore} from "../database/firestore"; +import type {FastifyInstance, RequestGenericInterface} from 'fastify'; +import {DataLayer, Region} from "../database/dataLayer"; -export default function createRegionsEndpoint(app: FastifyInstance) { - app.get('/regions', - async () => { +interface GetManagedRegionsRequest extends RequestGenericInterface { + Params: { + managerId: string + } +} + +export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer) { + app.get('/regions/:managerId', + async (request) => { let response = { status: "ok", date: Date.now(), - regions: [] + regions: [] } - let regionsSnapshot = await firestore.collection("regions").get(); - response.regions = await regionsSnapshot.docs.map((r) => ({'id': r.id, 'data': r.data()})); - console.log(response); + response.regions = await dataLayer.getRegionsManagedBy(request.params.managerId); return JSON.stringify(response); } ); diff --git a/src/endpoints/userRegions.ts b/src/endpoints/userRegions.ts deleted file mode 100644 index 52d2bc0..0000000 --- a/src/endpoints/userRegions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {firestore} from "../database/firestore"; - -interface UserRegionsRequest extends RequestGenericInterface { - Params: { - userId: string - } -} - -export default function createUserRegionsEndpoint(app: FastifyInstance) { - app.get('/users/:userId/regions', - async (request) => { - let userId = request.params.userId; - let response = { - status: "ok", - date: Date.now(), - user: userId, - regions: [] - } - let regionsSnapshot = await firestore.collection("regions").where("manager", "==", userId).get(); - response.regions = await regionsSnapshot.docs.map((b) => ({'id': b.id, 'data': b.data()})); - return JSON.stringify(response); - } - ); - - return app; -} diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index cf5a67e..7fc20ed 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,26 +1,25 @@ -import createRegionBusinessesEndpoint from "../src/endpoints/regionBusinesses"; import {fastify} from "fastify"; -import createBusinessesEndpoint from "../src/endpoints/businesses"; +import {dummyDataLayer} from "./utils/dummyDatalayer"; +import createBusinessesEndpoint, {Business} from "../src/endpoints/businesses"; test('creates and retrieves a valid business', async(done) => { - const app1 = createBusinessesEndpoint(fastify()); - const app2 = createRegionBusinessesEndpoint(fastify()); - const response1 = await app1.inject({ + const bizApp = createBusinessesEndpoint(fastify(), dummyDataLayer); + const response1 = await bizApp.inject({ method: 'POST', url: '/businesses', payload: {name: "DummyBiz", region: "DummyRegion", year_added: 2009} }); expect(response1.statusCode).toBe(200); - const response2 = await app2.inject({ + const response2 = await bizApp.inject({ method: 'GET', url: '/regions/DummyRegion/businesses' }); expect(response2.statusCode).toBe(200); - expect(JSON.parse(response2.payload).businesses).toEqual(expect.arrayContaining(expect.objectContaining({name: "DummyBiz"}))); + let businesses: Business[] = JSON.parse(response2.payload).businesses; + expect(businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: "DummyBiz"})])); - await app1.close(); - await app2.close(); + await bizApp.close(); done(); }) diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index e887917..5204b34 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -1,7 +1,41 @@ -import {dataLayer} from "../src/database/dataLayer"; +import {dataLayer, Region} from "../src/database/dataLayer"; +import {firestore} from "../src/database/firestore"; +import {Business} from "../src/endpoints/businesses"; -test("Gets expected data", () => { - let out = dataLayer.getBusinesses({businesses: [{name:"dummy"}]}); +afterEach(async() => { + let bizDocs = (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs; + bizDocs.forEach((d) => d.ref.delete()); + await firestore.collection("years").doc("2019").delete(); + await firestore.collection("regions").doc("DummyRegion").delete(); +}); + +test("Creates and retrieves businesses", async (done) => { + let biz : Business = { + employees: 21, + name: "DummyBiz", + region: "DummyRegion", + year_added: 2019 + }; + let id = (await dataLayer.setBusiness(biz)).id; + expect(id).toBeTruthy(); + + let out = await dataLayer.getBusinessesByRegion("DummyRegion"); + expect(out).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); + let filters = await dataLayer.getFilters(); + expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([2019])})) + done(); +}); + +test("Creates and retrieves regions", async (done) => { + let region: Region = { + id: "DummyRegion", + manager: "Dummy Manager" + }; + + let id = await dataLayer.setRegion(region); + expect(id).toBeTruthy(); - expect(out).toStrictEqual([{name:"dummy"}]); + let out = await dataLayer.getRegionsManagedBy("Dummy Manager"); + expect(out).toEqual(expect.arrayContaining([region])); + done(); }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index 42b5210..a9d1d84 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,13 +1,26 @@ import {fastify} from "fastify"; import createFiltersEndpoint from "../src/endpoints/filters"; +import createBusinessesEndpoint from "../src/endpoints/businesses"; +import {dummyDataLayer} from "./utils/dummyDatalayer"; -test('Returns the prebaked data', async (done) => { - const app = createFiltersEndpoint(fastify()); - const response = await app.inject({ method: 'GET', url: '/filters' }); +beforeAll(async (done) => { + const bizApp = createBusinessesEndpoint(fastify(), dummyDataLayer); + const response1 = await bizApp.inject({ + method: 'POST', + url: '/businesses', + payload: {name: "DummyBiz", region: "DummyRegion", year_added: 2019} + }); + expect(response1.statusCode).toBe(200); + done(); +}) + +test('Returns the filter data added previously', async (done) => { + const filterApp = createFiltersEndpoint(fastify(), dummyDataLayer); + const filterResponse = await filterApp.inject({ method: 'GET', url: '/filters' }); - expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).years).toContain(2019); - await app.close(); + expect(filterResponse.statusCode).toBe(200); + expect(JSON.parse(filterResponse.payload).years).toContain(2019); + await filterApp.close(); done(); }); diff --git a/tests/regionBusinesses.test.ts b/tests/regionBusinesses.test.ts deleted file mode 100644 index 9eaf7d0..0000000 --- a/tests/regionBusinesses.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import createRegionBusinessesEndpoint from "../src/endpoints/regionBusinesses"; -import {fastify} from "fastify"; - -test('Returns the prebaked data', async (done) => { - const app = createRegionBusinessesEndpoint(fastify()); - const response = await app.inject({ method: 'GET', url: '/regions/Bay%20Bulls/businesses' }); - - expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).businesses.length).toBe(1); - await app.close(); - done(); -}); - diff --git a/tests/regions.test.ts b/tests/regions.test.ts index dc2ee7a..57c4eb1 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,11 +1,19 @@ import createRegionsEndpoint from "../src/endpoints/regions"; import {fastify} from "fastify"; +import {dummyDataLayer} from "./utils/dummyDatalayer"; -test('returns a status code of 200', async () => { - const app = createRegionsEndpoint(fastify()); - const response = await app.inject({ method: 'GET', url: '/regions' }); +beforeEach(async (done) => { + await dummyDataLayer.setRegion({ + id: "DummyRegion", manager: "dummyManagerId" + }); + done(); +}); - expect(response.statusCode).toBe(200); +test('Successfully retrieves the region by manager ID', async () => { + const app = createRegionsEndpoint(fastify(), dummyDataLayer); + const response = await app.inject({ method: 'GET', url: '/regions/dummyManagerId' }); + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([{id:"DummyRegion", manager: "dummyManagerId"}])); await app.close(); }); diff --git a/tests/userRegions.test.ts b/tests/userRegions.test.ts deleted file mode 100644 index 03ac52c..0000000 --- a/tests/userRegions.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {fastify} from "fastify"; -import createUserRegionsEndpoint from "../src/endpoints/userRegions"; - - -test('returns a status code of 200', async () => { - const app = createUserRegionsEndpoint(fastify()); - const response = await app.inject({ method: 'GET', url: '/users/joe/regions' }); - console.log(response); - expect(response.statusCode).toBe(200); - const payload: { date: Date; regions: string[] } = JSON.parse( - response.payload - ); - expect(payload.regions.length).toBe(1); - - await app.close(); -}); diff --git a/tests/utils/dummyDatalayer.ts b/tests/utils/dummyDatalayer.ts new file mode 100644 index 0000000..e8c44e4 --- /dev/null +++ b/tests/utils/dummyDatalayer.ts @@ -0,0 +1,33 @@ +import {DataLayer, Filters, IdObject, Region} from "../../src/database/dataLayer"; +import {Business} from "../../src/endpoints/businesses"; + +class DummyDatalayer implements DataLayer { + businesses: Business[] = []; + regions: Region[] = []; + + getBusinessesByRegion(_:string): Promise { + return Promise.resolve(this.businesses); + } + + async setBusiness(business:Business): Promise { + this.businesses.push(business); + return {id:"1"}; + } + + async getFilters(): Promise { + return { + years: this.businesses.map((b) => b.year_added) + }; + } + + async setRegion(region: Region): Promise { + this.regions.push(region); + return {id: region.id}; + } + + async getRegionsManagedBy(managerId: string ): Promise { + return this.regions.filter((r) => managerId === r.manager); + } +} + +export const dummyDataLayer = new DummyDatalayer(); From f18fb7a549046be62d60336f2f888b44f7edd51b Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 27 Nov 2020 00:10:29 -0330 Subject: [PATCH 06/75] Adding some missing done() callbacks --- tests/dataLayer.test.ts | 3 ++- tests/ping.test.ts | 3 ++- tests/regions.test.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 5204b34..99a9f3d 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -2,11 +2,12 @@ import {dataLayer, Region} from "../src/database/dataLayer"; import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; -afterEach(async() => { +afterEach(async(done) => { let bizDocs = (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs; bizDocs.forEach((d) => d.ref.delete()); await firestore.collection("years").doc("2019").delete(); await firestore.collection("regions").doc("DummyRegion").delete(); + done(); }); test("Creates and retrieves businesses", async (done) => { diff --git a/tests/ping.test.ts b/tests/ping.test.ts index 5079532..9bec365 100644 --- a/tests/ping.test.ts +++ b/tests/ping.test.ts @@ -1,11 +1,12 @@ import fastify from 'fastify'; import createPingEndpoint from '../src/endpoints/ping'; -test('returns a status code of 200', async () => { +test('returns a status code of 200', async (done) => { const app = createPingEndpoint(fastify()); const response = await app.inject({ method: 'GET', url: '/ping' }); expect(response.statusCode).toBe(200); await app.close(); + done(); }); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index 57c4eb1..4c67e41 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -9,11 +9,12 @@ beforeEach(async (done) => { done(); }); -test('Successfully retrieves the region by manager ID', async () => { +test('Successfully retrieves the region by manager ID', async (done) => { const app = createRegionsEndpoint(fastify(), dummyDataLayer); const response = await app.inject({ method: 'GET', url: '/regions/dummyManagerId' }); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([{id:"DummyRegion", manager: "dummyManagerId"}])); await app.close(); + done(); }); From 8a6e4b4feb6176885eb69233ae1e578c483fd5e9 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 30 Nov 2020 02:37:49 -0330 Subject: [PATCH 07/75] A bunch of new stuff related to deployment. There'll be more to come, but this has at least gotten us up on a new endpoint. --- Dockerfile | 18 ++++++++++++++++++ deploy.sh | 6 ++++++ package.json | 4 ++-- .../{dataLayer.ts => productionDataLayer.ts} | 2 +- src/endpoints/businesses.ts | 2 +- src/endpoints/filters.ts | 2 +- src/endpoints/regions.ts | 2 +- src/index.ts | 9 ++++++++- tests/businesses.test.ts | 4 ++-- tests/dataLayer.test.ts | 12 ++++++------ tests/filters.test.ts | 6 +++--- tests/regions.test.ts | 6 +++--- .../{dummyDatalayer.ts => testDataLayer.ts} | 4 ++-- tsconfig.json | 3 ++- 14 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 Dockerfile create mode 100644 deploy.sh rename src/database/{dataLayer.ts => productionDataLayer.ts} (96%) rename tests/utils/{dummyDatalayer.ts => testDataLayer.ts} (91%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8492d9f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:15 + +# Create and change to the app directory. +WORKDIR /usr/src/app + +# Copy application dependency manifests to the container image. +# A wildcard is used to ensure both package.json AND package-lock.json are copied. +# Copying this separately prevents re-running npm install on every code change. +COPY package*.json ./ + +# Install production dependencies. +RUN npm install --only=production + +# Copy local code to the container image. +COPY . . + +# Run the web service on container startup. +CMD [ "npm", "start" ] diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..e04ae35 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,6 @@ +# Prerequisite Setup Steps: +# gcloud artifacts repositories create ranlab-api-mvp --repository-format docker --location northamerica-northeast1 # create the artifact repo +# gcloud auth configure-docker northamerica-northeast1-docker.pkg.dev +# docker tag ranlab-mvp-api:latest northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest +# docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest +gcloud run --platform=managed deploy ranlab-api-mvp --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest diff --git a/package.json b/package.json index e3d751c..247bc87 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@ranlab/api", - "version": "0.0.0", + "version": "0.1.0", "license": "UNLICENSED", "private": true, "description": "API for the RAnLab app", - "main": "src/index.js", + "main": "build/src/index.js", "scripts": { "build": "tsc --project .", "test": "dev/test", diff --git a/src/database/dataLayer.ts b/src/database/productionDataLayer.ts similarity index 96% rename from src/database/dataLayer.ts rename to src/database/productionDataLayer.ts index e8ec179..edde434 100644 --- a/src/database/dataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -53,4 +53,4 @@ export class ProductionDataLayer implements DataLayer { } } -export const dataLayer = new ProductionDataLayer(); +export const productionDataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 7caf471..6b9eb0b 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -1,5 +1,5 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {DataLayer} from "../database/dataLayer"; +import {DataLayer} from "../database/productionDataLayer"; import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index b3ab987..19b2d77 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,5 +1,5 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {DataLayer} from "../database/dataLayer"; +import {DataLayer} from "../database/productionDataLayer"; interface GetFiltersRequest extends RequestGenericInterface { Params: { diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 4777d6b..586989a 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -1,5 +1,5 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {DataLayer, Region} from "../database/dataLayer"; +import {DataLayer, Region} from "../database/productionDataLayer"; interface GetManagedRegionsRequest extends RequestGenericInterface { Params: { diff --git a/src/index.ts b/src/index.ts index fd23f71..8088be0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,18 @@ -import fastify from 'fastify'; +import fastify, {FastifyInstance} from 'fastify'; import createPingEndpoint from './endpoints/ping'; import { addRoutes } from './utils'; +import createRegionBusinessesEndpoint from "./endpoints/businesses"; +import createRegionsEndpoint from "./endpoints/regions"; +import {productionDataLayer} from "./database/productionDataLayer"; +import createFiltersEndpoint from "./endpoints/filters"; const port = Number(process.env.PORT || 8080); const server = addRoutes( fastify(), createPingEndpoint, + (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer), + (app: FastifyInstance) => createRegionBusinessesEndpoint(app, productionDataLayer), + (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer) ); server.listen(port, '::', (err, address) => { diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index 7fc20ed..862ee01 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,9 +1,9 @@ import {fastify} from "fastify"; -import {dummyDataLayer} from "./utils/dummyDatalayer"; +import {testDataLayer} from "./utils/testDataLayer"; import createBusinessesEndpoint, {Business} from "../src/endpoints/businesses"; test('creates and retrieves a valid business', async(done) => { - const bizApp = createBusinessesEndpoint(fastify(), dummyDataLayer); + const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); const response1 = await bizApp.inject({ method: 'POST', url: '/businesses', diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 99a9f3d..86813de 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -1,4 +1,4 @@ -import {dataLayer, Region} from "../src/database/dataLayer"; +import {productionDataLayer, Region} from "../src/database/productionDataLayer"; import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; @@ -17,12 +17,12 @@ test("Creates and retrieves businesses", async (done) => { region: "DummyRegion", year_added: 2019 }; - let id = (await dataLayer.setBusiness(biz)).id; + let id = (await productionDataLayer.setBusiness(biz)).id; expect(id).toBeTruthy(); - let out = await dataLayer.getBusinessesByRegion("DummyRegion"); + let out = await productionDataLayer.getBusinessesByRegion("DummyRegion"); expect(out).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); - let filters = await dataLayer.getFilters(); + let filters = await productionDataLayer.getFilters(); expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([2019])})) done(); }); @@ -33,10 +33,10 @@ test("Creates and retrieves regions", async (done) => { manager: "Dummy Manager" }; - let id = await dataLayer.setRegion(region); + let id = await productionDataLayer.setRegion(region); expect(id).toBeTruthy(); - let out = await dataLayer.getRegionsManagedBy("Dummy Manager"); + let out = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); expect(out).toEqual(expect.arrayContaining([region])); done(); }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index a9d1d84..9fb124c 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,10 +1,10 @@ import {fastify} from "fastify"; import createFiltersEndpoint from "../src/endpoints/filters"; import createBusinessesEndpoint from "../src/endpoints/businesses"; -import {dummyDataLayer} from "./utils/dummyDatalayer"; +import {testDataLayer} from "./utils/testDataLayer"; beforeAll(async (done) => { - const bizApp = createBusinessesEndpoint(fastify(), dummyDataLayer); + const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); const response1 = await bizApp.inject({ method: 'POST', url: '/businesses', @@ -15,7 +15,7 @@ beforeAll(async (done) => { }) test('Returns the filter data added previously', async (done) => { - const filterApp = createFiltersEndpoint(fastify(), dummyDataLayer); + const filterApp = createFiltersEndpoint(fastify(), testDataLayer); const filterResponse = await filterApp.inject({ method: 'GET', url: '/filters' }); expect(filterResponse.statusCode).toBe(200); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index 4c67e41..b107ecf 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,16 +1,16 @@ import createRegionsEndpoint from "../src/endpoints/regions"; import {fastify} from "fastify"; -import {dummyDataLayer} from "./utils/dummyDatalayer"; +import {testDataLayer} from "./utils/testDataLayer"; beforeEach(async (done) => { - await dummyDataLayer.setRegion({ + await testDataLayer.setRegion({ id: "DummyRegion", manager: "dummyManagerId" }); done(); }); test('Successfully retrieves the region by manager ID', async (done) => { - const app = createRegionsEndpoint(fastify(), dummyDataLayer); + const app = createRegionsEndpoint(fastify(), testDataLayer); const response = await app.inject({ method: 'GET', url: '/regions/dummyManagerId' }); expect(response.statusCode).toBe(200); diff --git a/tests/utils/dummyDatalayer.ts b/tests/utils/testDataLayer.ts similarity index 91% rename from tests/utils/dummyDatalayer.ts rename to tests/utils/testDataLayer.ts index e8c44e4..a2e9f18 100644 --- a/tests/utils/dummyDatalayer.ts +++ b/tests/utils/testDataLayer.ts @@ -1,4 +1,4 @@ -import {DataLayer, Filters, IdObject, Region} from "../../src/database/dataLayer"; +import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; import {Business} from "../../src/endpoints/businesses"; class DummyDatalayer implements DataLayer { @@ -30,4 +30,4 @@ class DummyDatalayer implements DataLayer { } } -export const dummyDataLayer = new DummyDatalayer(); +export const testDataLayer = new DummyDatalayer(); diff --git a/tsconfig.json b/tsconfig.json index c4a1207..7c0e8e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,9 @@ /* Basic Options */ "incremental": true, "target": "ES2017", - "module": "ES6", + "module": "commonjs", "declaration": true, + "outDir": "build", /* Strict Type-Checking Options */ "strict": true, From 2c8f5ccf762d7528140aa5291db1b140c96490fa Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Wed, 2 Dec 2020 22:58:53 -0330 Subject: [PATCH 08/75] Added CRUD operations and related tests for businesses and regions --- deploy.sh | 5 +-- src/database/productionDataLayer.ts | 18 ++++++++--- src/endpoints/businesses.ts | 25 ++++++++++++++- src/endpoints/regions.ts | 50 ++++++++++++++++++++++++++++- tests/businesses.test.ts | 38 ++++++++++++++-------- tests/dataLayer.test.ts | 50 +++++++++++++++++++++++++---- tests/filters.test.ts | 12 +++---- tests/regions.test.ts | 46 ++++++++++++++++++++++---- tests/utils/dummyData.ts | 27 ++++++++++++++++ tests/utils/testDataLayer.ts | 6 +++- 10 files changed, 234 insertions(+), 43 deletions(-) create mode 100644 tests/utils/dummyData.ts diff --git a/deploy.sh b/deploy.sh index e04ae35..ba41f41 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,7 @@ # Prerequisite Setup Steps: # gcloud artifacts repositories create ranlab-api-mvp --repository-format docker --location northamerica-northeast1 # create the artifact repo # gcloud auth configure-docker northamerica-northeast1-docker.pkg.dev -# docker tag ranlab-mvp-api:latest northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest -# docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest +docker build -t ranlab-mvp-api:latest . +docker tag ranlab-mvp-api:latest northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest +docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest gcloud run --platform=managed deploy ranlab-api-mvp --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index edde434..51b03e5 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -13,9 +13,10 @@ export interface Region { export interface DataLayer { setBusiness(business: Business) : Promise; getBusinessesByRegion(region: string): Promise; - getFilters() : Promise; + getFilters(regionId: string) : Promise; getRegionsManagedBy(managerId: string) : Promise; setRegion(region: Region): Promise; + deleteRegion(regionId: string): Promise; } export interface Filters { @@ -36,10 +37,11 @@ export class ProductionDataLayer implements DataLayer { return {id: businessRef.id}; } - async getFilters() : Promise{ + async getFilters(region: string) : Promise{ return { - years: [2019] - } + years: (await firestore.collection("businesses").where("region", "==", region).get()).docs + .map((b) => ((b.data()).year_added)) + }; } async getRegionsManagedBy(managerId: string): Promise { @@ -51,6 +53,14 @@ export class ProductionDataLayer implements DataLayer { await firestore.collection("regions").doc(region.id).set({"manager": region.manager}); return {id: region.id}; } + + async deleteRegion(id: string): Promise { + await firestore.collection("regions").doc(id).delete(); + } + + async deleteBusiness(id: string) { + await firestore.collection("businesses").doc(id).delete(); + } } export const productionDataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 6b9eb0b..13917cb 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -13,6 +13,13 @@ interface CreateBusinessRequest extends RequestGenericInterface { Body: Business } +interface UpdateBusinessRequest extends RequestGenericInterface { + Params: { + businessId: string + }, + Body: Business +} + export interface Business { id?: string | undefined; name: string; @@ -22,6 +29,7 @@ export interface Business { location?: GeoPoint | null | undefined } + export default function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/regions/:region/businesses', async (request) => { @@ -46,7 +54,22 @@ export default function createRegionBusinessesEndpoint(app: FastifyInstance, dat businessId: businessRef.id }; return JSON.stringify(response); - }); + } + ); + + app.post( + '/businesses/:businessId', + async (request) => { + let updatedBiz = {...request.body, id: request.params.businessId}; + await dataLayer.setBusiness(updatedBiz); + let response = { + status: "ok", + date: Date.now(), + business: updatedBiz + }; + return JSON.stringify(response); + } + ); return app; } diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 586989a..e020eda 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -7,18 +7,66 @@ interface GetManagedRegionsRequest extends RequestGenericInterface { } } +interface CreateRegionRequest extends RequestGenericInterface { + Body: Region +} + +interface DeleteRegionRequest extends RequestGenericInterface { + Params: { + regionId: string + } +} + +interface UpdateRegionRequest extends RequestGenericInterface { + Params: { + regionId: string + }, + Body: Region +} + export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer) { app.get('/regions/:managerId', async (request) => { let response = { + statusCode: 200, status: "ok", date: Date.now(), regions: [] } - response.regions = await dataLayer.getRegionsManagedBy(request.params.managerId); + response.regions.push(...(await dataLayer.getRegionsManagedBy(request.params.managerId))); return JSON.stringify(response); } ); + app.post('/regions', + async(request, reply) => { + let response = { + status: "ok", + region: request.body.id + }; + await dataLayer.setRegion(request.body); + reply.code(201); + return JSON.stringify(response); + } + ); + + app.post('/regions/:regionId', + async(request) => { + let UpdatedRegion: Region = {...request.body}; + let response = { + status: "ok", + region: UpdatedRegion + }; + await dataLayer.setRegion(UpdatedRegion); + return JSON.stringify(response); + } + ); + + app.delete('/regions/:regionId', + async(request, reply) => { + await dataLayer.deleteRegion(request.params.regionId); + reply.code(204); + } + ); return app; } diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index 862ee01..1554ec9 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,24 +1,36 @@ import {fastify} from "fastify"; import {testDataLayer} from "./utils/testDataLayer"; -import createBusinessesEndpoint, {Business} from "../src/endpoints/businesses"; +import createBusinessesEndpoint from "../src/endpoints/businesses"; +import {createDummyBusiness, DummyBiz, DummyRegion} from "./utils/dummyData"; -test('creates and retrieves a valid business', async(done) => { +it('Can create and retrieve a valid business', async(done) => { const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - const response1 = await bizApp.inject({ - method: 'POST', - url: '/businesses', - payload: {name: "DummyBiz", region: "DummyRegion", year_added: 2009} - }); - expect(response1.statusCode).toBe(200); + const createResponse = await createDummyBusiness(bizApp); + expect(createResponse.statusCode).toBe(200); - const response2 = await bizApp.inject({ + const getResponse = await bizApp.inject({ method: 'GET', - url: '/regions/DummyRegion/businesses' + url: `/regions/${DummyRegion.id}/businesses` }); - expect(response2.statusCode).toBe(200); - let businesses: Business[] = JSON.parse(response2.payload).businesses; - expect(businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: "DummyBiz"})])); + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: DummyBiz.name})])); + + await bizApp.close(); + done(); +}); + +it('Can update and retrieve a business', async(done) => { + const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); + const bizId = JSON.parse((await createDummyBusiness(bizApp)).payload).businessId; + const updatedBiz = {...DummyBiz, id: bizId, year_added: 2020, employees: 2}; + const updateResponse = await bizApp.inject({ + method: 'POST', + url: `/businesses/${bizId}`, + payload: updatedBiz + }); + expect(updateResponse.statusCode).toBe(200); + expect(JSON.parse(updateResponse.payload).business).toEqual(updatedBiz); await bizApp.close(); done(); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 86813de..2000451 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -10,9 +10,9 @@ afterEach(async(done) => { done(); }); -test("Creates and retrieves businesses", async (done) => { +it("Creates, retrieves, updates, and deletes businesses", async (done) => { let biz : Business = { - employees: 21, + employees: 1, name: "DummyBiz", region: "DummyRegion", year_added: 2019 @@ -20,14 +20,35 @@ test("Creates and retrieves businesses", async (done) => { let id = (await productionDataLayer.setBusiness(biz)).id; expect(id).toBeTruthy(); - let out = await productionDataLayer.getBusinessesByRegion("DummyRegion"); - expect(out).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); - let filters = await productionDataLayer.getFilters(); - expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([2019])})) + let bizData = await productionDataLayer.getBusinessesByRegion(biz.region); + expect(bizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); + + let filters = await productionDataLayer.getFilters(biz.region); + expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) + + biz.id = id; + biz.employees = 2; + biz.year_added = 2020; + let updatedId = (await productionDataLayer.setBusiness(biz)).id; + expect(updatedId).toEqual(id); + + let updatedBizData = await productionDataLayer.getBusinessesByRegion(biz.region); + expect(updatedBizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); + + let updatedFilters = await productionDataLayer.getFilters(biz.region); + expect(updatedFilters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) + + await productionDataLayer.deleteBusiness(biz.id); + let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.region); + expect(emptyBizData).toEqual([]); + + let emptyFilters = await productionDataLayer.getFilters(biz.region); + expect(emptyFilters.years).toEqual([]); + done(); }); -test("Creates and retrieves regions", async (done) => { +it("Creates, retrieves, updates, and deletes regions", async (done) => { let region: Region = { id: "DummyRegion", manager: "Dummy Manager" @@ -38,5 +59,20 @@ test("Creates and retrieves regions", async (done) => { let out = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); expect(out).toEqual(expect.arrayContaining([region])); + + region.manager = "New Manager"; + let updatedId = (await productionDataLayer.setRegion(region)); + expect(updatedId.id).toEqual(region.id); + + let oldManagerRegions = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); + expect(oldManagerRegions).toEqual(expect.arrayContaining([])); + + let newManagerRegions = await productionDataLayer.getRegionsManagedBy(region.manager); + expect(newManagerRegions).toEqual(expect.arrayContaining([region])); + + await productionDataLayer.deleteRegion(region.id); + let empty = await productionDataLayer.getRegionsManagedBy(region.manager); + expect(empty).toEqual([]); + done(); }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index 9fb124c..90b07d9 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -2,24 +2,20 @@ import {fastify} from "fastify"; import createFiltersEndpoint from "../src/endpoints/filters"; import createBusinessesEndpoint from "../src/endpoints/businesses"; import {testDataLayer} from "./utils/testDataLayer"; +import {createDummyBusiness, DummyBiz} from "./utils/dummyData"; beforeAll(async (done) => { const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - const response1 = await bizApp.inject({ - method: 'POST', - url: '/businesses', - payload: {name: "DummyBiz", region: "DummyRegion", year_added: 2019} - }); - expect(response1.statusCode).toBe(200); + await createDummyBusiness(bizApp) done(); }) -test('Returns the filter data added previously', async (done) => { +it('Returns the filter data added previously', async (done) => { const filterApp = createFiltersEndpoint(fastify(), testDataLayer); const filterResponse = await filterApp.inject({ method: 'GET', url: '/filters' }); expect(filterResponse.statusCode).toBe(200); - expect(JSON.parse(filterResponse.payload).years).toContain(2019); + expect(JSON.parse(filterResponse.payload).years).toContain(DummyBiz.year_added); await filterApp.close(); done(); }); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index b107ecf..d6616f6 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,20 +1,54 @@ import createRegionsEndpoint from "../src/endpoints/regions"; import {fastify} from "fastify"; import {testDataLayer} from "./utils/testDataLayer"; +import {DummyRegion, requestDummyManagedRegions} from "./utils/dummyData"; beforeEach(async (done) => { - await testDataLayer.setRegion({ - id: "DummyRegion", manager: "dummyManagerId" - }); + await testDataLayer.setRegion(DummyRegion); + done(); +}); + + +test('Can create and retrieve a region by manager ID', async (done) => { + const app = createRegionsEndpoint(fastify(), testDataLayer); + const response = await requestDummyManagedRegions(app); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); + await app.close(); done(); }); -test('Successfully retrieves the region by manager ID', async (done) => { +it('Can update and retrieve a region', async (done) => { const app = createRegionsEndpoint(fastify(), testDataLayer); - const response = await app.inject({ method: 'GET', url: '/regions/dummyManagerId' }); + const updatedRegion = { + id: "TestRegion", + manager: "TestManager" + }; + const response = await app.inject( { + method: 'POST', + url: `/regions/${DummyRegion.id}`, + payload: updatedRegion + }); expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([{id:"DummyRegion", manager: "dummyManagerId"}])); + expect(JSON.parse(response.payload).region).toEqual(updatedRegion); + await app.close(); + done(); +}); + +it('Can delete a region', async (done) => { + const app = createRegionsEndpoint(fastify(), testDataLayer); + const deleteResponse = await app.inject( { + method: 'DELETE', + url: '/regions/DummyRegion"', + }); + + expect(deleteResponse.statusCode).toBe(204); + + const getResponse = await requestDummyManagedRegions(app); + expect(JSON.parse(getResponse.payload).regions).toEqual([]); + await app.close(); done(); }); diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts new file mode 100644 index 0000000..3604dff --- /dev/null +++ b/tests/utils/dummyData.ts @@ -0,0 +1,27 @@ +import {FastifyInstance} from "fastify"; +import {Business} from "../../src/endpoints/businesses"; +import {Region} from "../../src/database/productionDataLayer"; + +export const DummyRegion: Region = { + id: "DummyRegion", + manager: "DummyManagerId" +}; + +export const DummyBiz: Business = { + name: "DummyBiz", + region: DummyRegion.id, + year_added: 2009, + employees: 1 +}; + +export async function createDummyBusiness(bizApp: FastifyInstance) { + return await bizApp.inject({ + method: 'POST', + url: '/businesses', + payload: DummyBiz + }); +} + +export async function requestDummyManagedRegions(app: FastifyInstance) { + return await app.inject({method: 'GET', url: `/regions/${DummyRegion.manager}`}); +} diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index a2e9f18..26dc91a 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -14,7 +14,7 @@ class DummyDatalayer implements DataLayer { return {id:"1"}; } - async getFilters(): Promise { + async getFilters(_: string): Promise { return { years: this.businesses.map((b) => b.year_added) }; @@ -28,6 +28,10 @@ class DummyDatalayer implements DataLayer { async getRegionsManagedBy(managerId: string ): Promise { return this.regions.filter((r) => managerId === r.manager); } + + async deleteRegion(regionId: string): Promise { + this.regions = this.regions.filter((r) => r.id == regionId); + } } export const testDataLayer = new DummyDatalayer(); From e7f4ae19c0b9661dad3d306fcdba9883ecc094c2 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Wed, 2 Dec 2020 23:41:59 -0330 Subject: [PATCH 09/75] Added docker login to the deploy script Added a simple test.sh to validate that the new endpoints are functioning. --- deploy.sh | 3 ++- test.sh | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 test.sh diff --git a/deploy.sh b/deploy.sh index ba41f41..e14b3dc 100644 --- a/deploy.sh +++ b/deploy.sh @@ -3,5 +3,6 @@ # gcloud auth configure-docker northamerica-northeast1-docker.pkg.dev docker build -t ranlab-mvp-api:latest . docker tag ranlab-mvp-api:latest northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest +gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://gcr.io docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest -gcloud run --platform=managed deploy ranlab-api-mvp --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest +gcloud run --platform=managed --region=northamerica-northeast1 deploy ranlab-api-mvp --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..70b2f5d --- /dev/null +++ b/test.sh @@ -0,0 +1,3 @@ +curl -X POST -H "Content-Type: application/json" \ + --data "{\"name\":\"DummyBiz\",\"year_added\":2020,\"region\":\"DummyRegion\",\"employees\":2}" \ + https://ranlab-api-mvp-xxvyt3l5wa-nn.a.run.app/businesses From 98cf744762956a4543c0f73ed2e9ce8287648177 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 22:09:11 -0330 Subject: [PATCH 10/75] Creating a deployment workflow Recreating deploy.sh as an action, we'll see how this goes. --- .github/workflows/deploy.yml | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..5b41811 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,43 @@ +name: Deploy to Google Cloud Run +on: + release +jobs: + build: + name: Build image + runs-on: ubuntu-latest + env: + HASH: $(git rev-parse --short "$GITHUB_SHA") + BRANCH: ${GITHUB_REF##*/} + SERVICE_NAME: ${{ secrets.SERVICE_NAME }} + PROJECT_ID: ${{ secrets.PROJECT_ID }} + steps: + - name: Checkout + uses: actions/checkout@v2 + + # Setup gcloud CLI + - uses: google-github-actions/github-actions/setup-gcloud@master + with: + service_account_key: ${{ secrets.GCR_DEVOPS_SERVICE_ACCOUNT_KEY }} + project_id: ${{ secrets.PROJECT_ID }} + export_default_credentials: true + + # Build docker image + - name: Build Docker Image + run: |- + docker build --build-arg -t northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest . + # Configure docker to use the gcloud command-line tool as a credential helper + - run: | + gcloud auth configure-docker -q + # Push image to Google Container Registry + - name: Push Image to GCR + run: |- + docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest + - name: Deploy Container + run: |- + gcloud run + deploy ranlab-api-mvp \ + --quiet + --platform=managed \ + --region=northamerica-northeast1 \ + --allow-unauthenticated \ + --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest From 05f6860d7f1926523a146b54e871cd1423300263 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 22:32:36 -0330 Subject: [PATCH 11/75] Updating deployment Will only trigger on published releases --- .github/workflows/deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5b41811..2dcae40 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,6 +1,7 @@ name: Deploy to Google Cloud Run on: - release + release: + types: [published] jobs: build: name: Build image From 10a0b1ce18c7a4fe0cc75f641b2bf4cc219852b5 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 22:37:04 -0330 Subject: [PATCH 12/75] Fixing the build-arg issue. Again. --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2dcae40..ec9b35d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,7 +25,7 @@ jobs: # Build docker image - name: Build Docker Image run: |- - docker build --build-arg -t northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest . + docker build -t northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest . # Configure docker to use the gcloud command-line tool as a credential helper - run: | gcloud auth configure-docker -q From 34a66a8e8d23bde060f595c622931b6a79cb7aea Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 23:05:12 -0330 Subject: [PATCH 13/75] debug code --- .github/workflows/deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec9b35d..fbbece9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,7 +21,8 @@ jobs: service_account_key: ${{ secrets.GCR_DEVOPS_SERVICE_ACCOUNT_KEY }} project_id: ${{ secrets.PROJECT_ID }} export_default_credentials: true - + - run: | + ls -la # Build docker image - name: Build Docker Image run: |- From 17672cb434248adbb26dccaf1b6e4dc52a4aea8d Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 23:29:22 -0330 Subject: [PATCH 14/75] Adding region to gcloud auth Supposedly Artifact Registry requires this --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fbbece9..880dbf3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: docker build -t northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest . # Configure docker to use the gcloud command-line tool as a credential helper - run: | - gcloud auth configure-docker -q + gcloud auth configure-docker -q northamerica-northeast1 # Push image to Google Container Registry - name: Push Image to GCR run: |- From 1e6450fb0b5c41bf3a3da31cbe9045cc2a5130c0 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 23:35:00 -0330 Subject: [PATCH 15/75] Fixing the repository name Should include the -docker.pkg.dev suffix --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 880dbf3..0d36879 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: docker build -t northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest . # Configure docker to use the gcloud command-line tool as a credential helper - run: | - gcloud auth configure-docker -q northamerica-northeast1 + gcloud auth configure-docker -q northamerica-northeast1-docker.pkg.dev # Push image to Google Container Registry - name: Push Image to GCR run: |- From 32976c91c4efd5143f04c7d16a3aa6b0846486f9 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 23:38:21 -0330 Subject: [PATCH 16/75] Fixing gcloud run command Stupid line continuations --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0d36879..b222997 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,7 +36,7 @@ jobs: docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest - name: Deploy Container run: |- - gcloud run + gcloud run \ deploy ranlab-api-mvp \ --quiet --platform=managed \ From a4e9e4514c76527db5f4addad7db640e0947e75b Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Thu, 3 Dec 2020 23:47:49 -0330 Subject: [PATCH 17/75] Fixing line continuations, part 2 --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b222997..3c8f985 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,7 +38,7 @@ jobs: run: |- gcloud run \ deploy ranlab-api-mvp \ - --quiet + --quiet \ --platform=managed \ --region=northamerica-northeast1 \ --allow-unauthenticated \ From 41c307dfa1fed6ebde662a0da47f47a1a15f1d64 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 3 Dec 2020 23:56:49 -0330 Subject: [PATCH 18/75] Fixing compilation error and adding TypeScript compilation to the deploy action --- .github/workflows/deploy.yml | 4 ++-- deploy.sh | 2 ++ src/endpoints/filters.ts | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fbbece9..809e361 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - + - run: yarn build # Setup gcloud CLI - uses: google-github-actions/github-actions/setup-gcloud@master with: @@ -36,7 +36,7 @@ jobs: docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest - name: Deploy Container run: |- - gcloud run + gcloud run deploy ranlab-api-mvp \ --quiet --platform=managed \ diff --git a/deploy.sh b/deploy.sh index e14b3dc..9f87d4a 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,8 +1,10 @@ # Prerequisite Setup Steps: # gcloud artifacts repositories create ranlab-api-mvp --repository-format docker --location northamerica-northeast1 # create the artifact repo # gcloud auth configure-docker northamerica-northeast1-docker.pkg.dev + docker build -t ranlab-mvp-api:latest . docker tag ranlab-mvp-api:latest northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://gcr.io docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest gcloud run --platform=managed --region=northamerica-northeast1 deploy ranlab-api-mvp --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest + diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index 19b2d77..e56bbdb 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -3,20 +3,20 @@ import {DataLayer} from "../database/productionDataLayer"; interface GetFiltersRequest extends RequestGenericInterface { Params: { - regionId: number + regionId: string } } export default function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/filters', - async () => { + async (request) => { let response = { status: "ok", date: Date.now(), years: [] } - response.years = (await dataLayer.getFilters()).years; + response.years = (await dataLayer.getFilters(request.params.regionId)).years; return JSON.stringify(response); } ); From 3af63a73eb366e1a0c25ae8ff39f284a21a2efbe Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 4 Dec 2020 00:08:04 -0330 Subject: [PATCH 19/75] Dockerfile wasn't actually building the project, which was breaking the GH Actions deployment. --- .github/workflows/deploy.yml | 1 - Dockerfile | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index da233b4..99b2947 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - run: yarn build # Setup gcloud CLI - uses: google-github-actions/github-actions/setup-gcloud@master with: diff --git a/Dockerfile b/Dockerfile index 8492d9f..fce382d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,10 +9,12 @@ WORKDIR /usr/src/app COPY package*.json ./ # Install production dependencies. -RUN npm install --only=production +RUN yarn install --only=production # Copy local code to the container image. COPY . . +RUN yarn build + # Run the web service on container startup. CMD [ "npm", "start" ] From e35da224e07649dea6a6a25f4d9a30e0e433a513 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 20 Dec 2020 23:41:44 -0330 Subject: [PATCH 20/75] Implemented auth0 and related tests --- .env | 2 + babel.config.js | 6 + jest.config.js | 5 + package-lock.json | 358 ------------------ package.json | 7 +- src/auth0.ts | 20 + src/declarations/fastify-authz-jwks.d.ts | 7 + src/endpoints/businesses.ts | 4 +- src/endpoints/regions.ts | 16 +- src/index.ts | 3 + tests/regions.test.ts | 17 +- tests/utils/dummyData.ts | 8 +- tests/utils/testDataLayer.ts | 6 +- tests/utils/testify.ts | 25 ++ yarn.lock | 454 ++++++++++++++++++++++- 15 files changed, 555 insertions(+), 383 deletions(-) create mode 100644 .env create mode 100644 babel.config.js create mode 100644 jest.config.js delete mode 100644 package-lock.json create mode 100644 src/auth0.ts create mode 100644 src/declarations/fastify-authz-jwks.d.ts create mode 100644 tests/utils/testify.ts diff --git a/.env b/.env new file mode 100644 index 0000000..d60d5c9 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +AUTH0_DOMAIN=lesleychard.auth0.com +AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..db20d00 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', {targets: {node: 'current'}}], + + '@babel/preset-typescript', + ], +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..cabb177 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ["/node_modules/","/build/"] +}; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a031c5c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,358 +0,0 @@ -{ - "name": "@ranlab/api", - "version": "0.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@types/node": { - "version": "12.19.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.4.tgz", - "integrity": "sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w==", - "dev": true - }, - "abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" - }, - "atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" - }, - "avvio": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.0.tgz", - "integrity": "sha512-KtC63UyZARidAoIV8wXutAZnDIbZcXBqLjTAhZOX+mdMZBQCh5il/15MvCvma1178nhTwvN2D0TOAdiKG1MpUA==", - "requires": { - "archy": "^1.0.0", - "debug": "^4.0.0", - "fastq": "^1.6.1", - "queue-microtask": "^1.1.2" - } - }, - "barecolor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/barecolor/-/barecolor-1.0.1.tgz", - "integrity": "sha512-ncJ680U+r1CGBt73L3O6V9GIAPy3hbDmWODEQajwEnDmmzeStvc4UYhapUSxUpS76+MHxyRihzZfwhyl122Zdw==", - "dev": true - }, - "baretest": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/baretest/-/baretest-2.0.0.tgz", - "integrity": "sha512-hRmYnBojeijT3jH0GtqLoHus+adPoeYh2NmcNT3wBPH903AUphcFqs1gJ64fBovDXql51Df24g9D9jcXRZd4vA==", - "dev": true, - "requires": { - "barecolor": "1.0.1" - } - }, - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" - }, - "fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-json-stringify": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.2.9.tgz", - "integrity": "sha512-O8YmNoc7LnfSafVaTfa1yXVFT4UMsi/N7cYcNZw6w5D5tltyu6XGXvH45mvWfsrcFoSK+H0q0exGXsUqC18z/g==", - "requires": { - "ajv": "^6.11.0", - "deepmerge": "^4.2.2", - "string-similarity": "^4.0.1" - } - }, - "fast-redact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.0.0.tgz", - "integrity": "sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w==" - }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "fastify": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.8.0.tgz", - "integrity": "sha512-w57/uvyQWzF/KSr9CbWQ5nfTqSSfYcmrems9Lc3VvtrAF7EsLbfZQBQZul6xwvE1uEfxA4nGdoUKqpU7xiv7cw==", - "requires": { - "abstract-logging": "^2.0.0", - "ajv": "^6.12.2", - "avvio": "^7.1.2", - "fast-json-stringify": "^2.2.1", - "fastify-error": "^0.2.0", - "fastify-warning": "^0.2.0", - "find-my-way": "^3.0.5", - "flatstr": "^1.0.12", - "light-my-request": "^4.2.0", - "pino": "^6.2.1", - "proxy-addr": "^2.0.5", - "readable-stream": "^3.4.0", - "rfdc": "^1.1.4", - "secure-json-parse": "^2.0.0", - "semver": "^7.3.2", - "tiny-lru": "^7.0.0" - } - }, - "fastify-error": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.2.0.tgz", - "integrity": "sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA==" - }, - "fastify-warning": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", - "integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==" - }, - "fastq": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", - "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", - "requires": { - "reusify": "^1.0.4" - } - }, - "find-my-way": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-3.0.5.tgz", - "integrity": "sha512-FweGg0cv1sBX8z7WhvBX5B5AECW4Zdh/NiB38Oa0qwSNIyPgRBCl/YjxuZn/rz38E/MMBHeVKJ22i7W3c626Gg==", - "requires": { - "fast-decode-uri-component": "^1.0.1", - "safe-regex2": "^2.0.0", - "semver-store": "^0.3.0" - } - }, - "flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "light-my-request": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.3.0.tgz", - "integrity": "sha512-WrEvI7V41ZbEUe0bsfuS170QrYSVADKA0JiWyK/lVtm4Ra26pl9CYKBdlr823/s37N2wMJze8YNkHbg11aZWAw==", - "requires": { - "ajv": "^6.12.2", - "cookie": "^0.4.0", - "fastify-warning": "^0.2.0", - "readable-stream": "^3.6.0", - "set-cookie-parser": "^2.4.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "pino": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.7.0.tgz", - "integrity": "sha512-vPXJ4P9rWCwzlTJt+f0Ni4THc3DWyt8iDDCO4edQ8narTu6hnpzdXu8FqeSJCGndl1W6lfbYQUQihUO54y66Lw==", - "requires": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.7", - "flatstr": "^1.0.12", - "pino-std-serializers": "^2.4.2", - "quick-format-unescaped": "^4.0.1", - "sonic-boom": "^1.0.2" - } - }, - "pino-std-serializers": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz", - "integrity": "sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg==" - }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "queue-microtask": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.0.tgz", - "integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w==" - }, - "quick-format-unescaped": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz", - "integrity": "sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A==" - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, - "rfdc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", - "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", - "requires": { - "ret": "~0.2.0" - } - }, - "secure-json-parse": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.1.0.tgz", - "integrity": "sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA==" - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" - }, - "semver-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", - "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==" - }, - "set-cookie-parser": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz", - "integrity": "sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg==" - }, - "sonic-boom": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.3.0.tgz", - "integrity": "sha512-4nX6OYvOYr6R76xfQKi6cZpTO3YSWe/vd+QdIfoH0lBy0MnPkeAbb2rRWgmgADkXUeCKPwO1FZAKlAVWAadELw==", - "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, - "string-similarity": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.3.tgz", - "integrity": "sha512-QEwJzNFCqq+5AGImk5z4vbsEPTN/+gtyKfXBVLBcbPBRPNganZGfQnIuf9yJ+GiwSnD65sT8xrw/uwU1Q1WmfQ==" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "tiny-lru": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", - "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==" - }, - "typescript": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", - "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", - "dev": true - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - } - } -} diff --git a/package.json b/package.json index 247bc87..6a8daf2 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,12 @@ "node": ">=12" }, "dependencies": { - "fastify": "^3.5.1" + "body-parser": "^1.19.0", + "fastify": "^3.5.1", + "fastify-authz-jwks": "^1.1.11", + "fastify-jwt": "^2.1.3", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^1.12.0" }, "devDependencies": { "@types/jest": "^26.0.15", diff --git a/src/auth0.ts b/src/auth0.ts new file mode 100644 index 0000000..69b2c14 --- /dev/null +++ b/src/auth0.ts @@ -0,0 +1,20 @@ +import {FastifyInstance} from "fastify"; +import fastifySecretProvider from 'fastify-authz-jwks'; +import fastifyJwt, {FastifyJWTOptions} from 'fastify-jwt'; + +export function registerAuth0(fastify: FastifyInstance) { + const faSecretProvider = fastifySecretProvider({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: 'http://__tenant__.auth0.com/.well-known/jwks.json' + }); + + fastify.register(fastifyJwt, { + secret: faSecretProvider, + audience: 'https://api.example.com', + issuer: 'https://__tenant__.auth0.com/', + algorithms: ['RS256'], + decode: { complete: true }, + }); +} diff --git a/src/declarations/fastify-authz-jwks.d.ts b/src/declarations/fastify-authz-jwks.d.ts new file mode 100644 index 0000000..3d49243 --- /dev/null +++ b/src/declarations/fastify-authz-jwks.d.ts @@ -0,0 +1,7 @@ +declare module "fastify-authz-jwks" { + import {FastifyReply, FastifyRequest} from "fastify"; + export type SecretCB = (e: Error | null, secret: string | undefined) => void; + export default function fastifyJwtSecret( + options: {} + ): (request : FastifyRequest, token : FastifyReply, cb: SecretCB) => void; +}; diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 13917cb..4622919 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -6,6 +6,9 @@ import GeoPoint = firebase.firestore.GeoPoint; interface GetRegionBusinessRequest extends RequestGenericInterface { Params: { region: string + }, + Headers: { + access_token: string } } @@ -29,7 +32,6 @@ export interface Business { location?: GeoPoint | null | undefined } - export default function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/regions/:region/businesses', async (request) => { diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index e020eda..aa28c41 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -25,15 +25,16 @@ interface UpdateRegionRequest extends RequestGenericInterface { } export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer) { - app.get('/regions/:managerId', + app.get('/regions', async (request) => { + let {userId} = <{userId:string}>await request.jwtVerify(); let response = { statusCode: 200, status: "ok", date: Date.now(), regions: [] } - response.regions.push(...(await dataLayer.getRegionsManagedBy(request.params.managerId))); + response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); return JSON.stringify(response); } ); @@ -63,9 +64,14 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : ); app.delete('/regions/:regionId', - async(request, reply) => { - await dataLayer.deleteRegion(request.params.regionId); - reply.code(204); + async (request, reply) => { + let {userId} = <{userId:string}>await request.jwtVerify(); + if((await dataLayer.getRegionsManagedBy(userId)).find((r) => r.id === request.params.regionId)) { + await dataLayer.deleteRegion(request.params.regionId); + reply.code(204); + } else { + reply.code(401); + } } ); return app; diff --git a/src/index.ts b/src/index.ts index 8088be0..6f607c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import createRegionBusinessesEndpoint from "./endpoints/businesses"; import createRegionsEndpoint from "./endpoints/regions"; import {productionDataLayer} from "./database/productionDataLayer"; import createFiltersEndpoint from "./endpoints/filters"; +import {registerAuth0} from "./auth0"; const port = Number(process.env.PORT || 8080); const server = addRoutes( @@ -15,6 +16,8 @@ const server = addRoutes( (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer) ); +registerAuth0(server); + server.listen(port, '::', (err, address) => { if (err) { console.error(err); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index d6616f6..d3dbf30 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,18 +1,20 @@ import createRegionsEndpoint from "../src/endpoints/regions"; -import {fastify} from "fastify"; +import {testify, mockAuth0Return} from "./utils/testify"; import {testDataLayer} from "./utils/testDataLayer"; -import {DummyRegion, requestDummyManagedRegions} from "./utils/dummyData"; +import {DummyRegion, dummyToken, requestDummyManagedRegions} from "./utils/dummyData"; beforeEach(async (done) => { + testDataLayer.clearRegions(); await testDataLayer.setRegion(DummyRegion); done(); }); - test('Can create and retrieve a region by manager ID', async (done) => { - const app = createRegionsEndpoint(fastify(), testDataLayer); + const app = createRegionsEndpoint(testify(), testDataLayer); + mockAuth0Return.user = "DummyUser"; const response = await requestDummyManagedRegions(app); + expect(mockAuth0Return.callCount).toBe(1); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); await app.close(); @@ -20,7 +22,7 @@ test('Can create and retrieve a region by manager ID', async (done) => { }); it('Can update and retrieve a region', async (done) => { - const app = createRegionsEndpoint(fastify(), testDataLayer); + const app = createRegionsEndpoint(testify(), testDataLayer); const updatedRegion = { id: "TestRegion", manager: "TestManager" @@ -38,10 +40,11 @@ it('Can update and retrieve a region', async (done) => { }); it('Can delete a region', async (done) => { - const app = createRegionsEndpoint(fastify(), testDataLayer); + const app = createRegionsEndpoint(await testify(), testDataLayer); const deleteResponse = await app.inject( { method: 'DELETE', - url: '/regions/DummyRegion"', + url: `/regions/${DummyRegion.id}`, + headers: {authorization: `Bearer ${dummyToken}`} }); expect(deleteResponse.statusCode).toBe(204); diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index 3604dff..a4e3f35 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -1,10 +1,14 @@ import {FastifyInstance} from "fastify"; import {Business} from "../../src/endpoints/businesses"; import {Region} from "../../src/database/productionDataLayer"; +import {getMockToken} from "./testify"; + +const dummyManager = "DummyManagerId"; +export const dummyToken = getMockToken({userId: dummyManager}) export const DummyRegion: Region = { id: "DummyRegion", - manager: "DummyManagerId" + manager: dummyManager }; export const DummyBiz: Business = { @@ -23,5 +27,5 @@ export async function createDummyBusiness(bizApp: FastifyInstance) { } export async function requestDummyManagedRegions(app: FastifyInstance) { - return await app.inject({method: 'GET', url: `/regions/${DummyRegion.manager}`}); + return await app.inject({method: 'GET', headers: {authorization: `Bearer ${dummyToken}`}, url: `/regions`}); } diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index 26dc91a..770301a 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -30,7 +30,11 @@ class DummyDatalayer implements DataLayer { } async deleteRegion(regionId: string): Promise { - this.regions = this.regions.filter((r) => r.id == regionId); + this.regions = this.regions.filter((r) => r.id !== regionId); + } + + clearRegions() { + this.regions = []; } } diff --git a/tests/utils/testify.ts b/tests/utils/testify.ts new file mode 100644 index 0000000..0251593 --- /dev/null +++ b/tests/utils/testify.ts @@ -0,0 +1,25 @@ +import fastify from "fastify"; +import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; +import jwt from "jsonwebtoken"; + +export const mockSecret = 'dummy'; +export const mockAuth0Return = { + user: null, + callCount: 0 +}; + +export function getMockToken(payload: object) { + return jwt.sign(payload, mockSecret) +} + +export const testify = () => { + const f = fastify(); + f.register(fastifyJWT, { + secret: (_request, _reply, _provider) => { mockAuth0Return.callCount++; _provider(null, mockSecret);}, + audience: 'https://localhost', + issuer: 'https://localhost/', + algorithms: ['RS256'], + decode: { complete: true }, + }); + return f; +}; diff --git a/yarn.lock b/yarn.lock index 662a798..ea2336f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -774,6 +774,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" @@ -807,6 +812,55 @@ dependencies: "@babel/types" "^7.3.0" +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.34" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" + integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== + dependencies: + "@types/node" "*" + +"@types/express-jwt@0.0.42": + version "0.0.42" + resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz#4f04e1fadf9d18725950dc041808a4a4adf7f5ae" + integrity sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag== + dependencies: + "@types/express" "*" + "@types/express-unless" "*" + +"@types/express-serve-static-core@*": + version "4.17.15" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.15.tgz#7c3d37829a991da9a507c1efd44d97532e8909e3" + integrity sha512-pb71P0BrBAx7cQE+/7QnA1HTQUkdBKMlkPY7lHUMn0YvPJkL2UA+KW3BdWQ309IT+i9En/qm45ZxpjIcpgEhNQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express-unless@*": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.1.tgz#4f440b905e42bbf53382b8207bc337dc5ff9fd1f" + integrity sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw== + dependencies: + "@types/express" "*" + +"@types/express@*": + version "4.17.9" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.9.tgz#f5f2df6add703ff28428add52bdec8a1091b0a78" + integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.2": version "4.1.4" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.4.tgz#4ff9f641a7c6d1a3508ff88bc3141b152772e753" @@ -841,11 +895,23 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/jsonwebtoken@^8.3.2": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" + integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== + dependencies: + "@types/node" "*" + "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + "@types/node@*": version "14.14.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" @@ -876,6 +942,24 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46" + integrity sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA== + dependencies: + "@types/mime" "*" + "@types/node" "*" + "@types/stack-utils@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" @@ -1086,6 +1170,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + babel-jest@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" @@ -1194,6 +1285,22 @@ bignumber.js@^9.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== +body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1254,6 +1361,11 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1406,6 +1518,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -1486,6 +1603,13 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" @@ -1493,10 +1617,10 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" @@ -1552,6 +1676,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -1589,6 +1718,11 @@ ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: dependencies: safe-buffer "^5.0.1" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + emittery@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" @@ -1806,11 +1940,43 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== +fastfall@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94" + integrity sha1-P+4DMxpJ0dObPN96XpzWb0dee5Q= + dependencies: + reusify "^1.0.0" + +fastify-authz-jwks@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/fastify-authz-jwks/-/fastify-authz-jwks-1.1.11.tgz#1b0b8d18962e4f1a4e6033a687eaf5325a6b8351" + integrity sha512-8puiTBrphRrQy0fpJQLpwYKIyyeAWDkdp9/EDGVWbGOypvA/zc8RXaTCtDQxQ9JOxx0X0/g8IEYQv/37yJtCPQ== + dependencies: + jwks-rsa "^1.6.0" + fastify-error@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== +fastify-jwt@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.1.3.tgz#d016a2f6a810299edc72ad79ce2949fb88a20604" + integrity sha512-8732zt+7UA9JzeRebJFCH+56laMCAxq/Wyou6pzXZzEWcPGPLcRqCk+R0CcgwjjVToo6wLIeNNKHFegyemyHug== + dependencies: + "@types/jsonwebtoken" "^8.3.2" + fastify-plugin "^2.0.0" + http-errors "^1.7.1" + jsonwebtoken "^8.3.0" + steed "^1.1.3" + +fastify-plugin@^2.0.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz#b17abdc36a97877d88101fb86ad8a07f2c07de87" + integrity sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ== + dependencies: + semver "^7.3.2" + fastify-warning@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" @@ -1838,6 +2004,21 @@ fastify@^3.5.1: semver "^7.3.2" tiny-lru "^7.0.0" +fastparallel@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688" + integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ== + dependencies: + reusify "^1.0.4" + xtend "^4.0.2" + +fastq@^1.3.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" + integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== + dependencies: + reusify "^1.0.4" + fastq@^1.6.1: version "1.8.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" @@ -1845,6 +2026,14 @@ fastq@^1.6.1: dependencies: reusify "^1.0.4" +fastseries@^1.7.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b" + integrity sha1-0izhO5Qz3/M4jZHb1ri9qbIaD0s= + dependencies: + reusify "^1.0.0" + xtend "^4.0.0" + faye-websocket@0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" @@ -1918,6 +2107,13 @@ flatstr@^1.0.12: resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -2161,11 +2357,42 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@^1.7.1: + version "1.8.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" + integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-parser-js@>=0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2221,11 +2448,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -2914,6 +3146,22 @@ json5@2.x, json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -2924,6 +3172,15 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jwa@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" @@ -2933,6 +3190,30 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwks-rsa@^1.12.0, jwks-rsa@^1.6.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.12.0.tgz#6bfa20c6673abf15c2ff0806edc0a15b3f2f5969" + integrity sha512-6zKwlhnGQo+KivArZQThGoxrliRZgyC8s6yXDFSNRiMEVm+4jv07nBkVPFoaN/rEX0b47GKm5MSyBmuzGjj6wg== + dependencies: + "@types/express-jwt" "0.0.42" + axios "^0.19.2" + debug "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + jsonwebtoken "^8.5.1" + limiter "^1.1.5" + lru-memoizer "^2.1.2" + ms "^2.1.2" + proxy-from-env "^1.1.0" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + jws@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" @@ -2994,6 +3275,11 @@ light-my-request@^4.0.2: readable-stream "^3.6.0" set-cookie-parser "^2.4.1" +limiter@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -3011,11 +3297,51 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -3038,6 +3364,22 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@~4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + integrity sha1-HRdnnAac2l0ECZGgnbwsDbN35V4= + dependencies: + pseudomap "^1.0.1" + yallist "^2.0.0" + +lru-memoizer@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.1.3.tgz#a16a14248e930658dafbd0998af82a747a230235" + integrity sha512-DcAptVUrKHbyKfSpvthwHwD42bFBLSAhTXJf5PQunu4F0/Hzy41WTamvavUWqsOPps26D0l5534aFvcwEcYzDw== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "~4.0.0" + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -3069,6 +3411,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -3106,7 +3453,7 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -3158,6 +3505,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1, ms@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -3286,6 +3638,13 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3487,6 +3846,16 @@ proxy-addr@^2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pseudomap@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -3505,6 +3874,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -3520,6 +3894,16 @@ quick-format-unescaped@^4.0.1: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701" integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" @@ -3663,7 +4047,7 @@ ret@~0.2.0: resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== -reusify@^1.0.4: +reusify@^1.0.0, reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== @@ -3746,7 +4130,7 @@ semver-store@^0.3.0: resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -3781,6 +4165,16 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -3970,11 +4364,27 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +"statuses@>= 1.5.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +steed@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5" + integrity sha1-8VJd1a2xLrIb90dJU3Zo1iW5q8U= + dependencies: + fastfall "^1.5.0" + fastparallel "^2.2.0" + fastq "^1.3.0" + fastseries "^1.7.0" + reusify "^1.0.0" + string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" @@ -4122,6 +4532,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -4207,6 +4622,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -4229,6 +4652,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -4434,11 +4862,21 @@ xmlhttprequest@1.8.0: resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= +xtend@^4.0.0, xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +yallist@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 8c1a25fa8c6145e5e1d61b267d695bd4245dfdd1 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 20 Dec 2020 23:50:04 -0330 Subject: [PATCH 21/75] Moved the data clearing for the data layer tests to happen before the tests rather than after to avoid issues with dirty databases. --- tests/dataLayer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 2000451..c3ff80a 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -2,7 +2,7 @@ import {productionDataLayer, Region} from "../src/database/productionDataLayer"; import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; -afterEach(async(done) => { +beforeEach(async(done) => { let bizDocs = (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs; bizDocs.forEach((d) => d.ref.delete()); await firestore.collection("years").doc("2019").delete(); From 38d12db94ddb1a2433a13882e82fb882546580de Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 21 Dec 2020 00:24:20 -0330 Subject: [PATCH 22/75] Added admin region endpoint and data layer functionality --- src/database/productionDataLayer.ts | 6 ++ src/endpoints/regions.ts | 8 +- tests/businesses.test.ts | 60 ++++++----- tests/dataLayer.test.ts | 157 +++++++++++++++------------- tests/filters.test.ts | 32 +++--- tests/regions.test.ts | 125 +++++++++++++--------- tests/utils/dummyData.ts | 4 +- tests/utils/testDataLayer.ts | 8 +- tests/utils/testify.ts | 14 ++- 9 files changed, 239 insertions(+), 175 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 51b03e5..01b20f6 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -17,6 +17,7 @@ export interface DataLayer { getRegionsManagedBy(managerId: string) : Promise; setRegion(region: Region): Promise; deleteRegion(regionId: string): Promise; + getAllRegions(): Promise; } export interface Filters { @@ -44,6 +45,11 @@ export class ProductionDataLayer implements DataLayer { }; } + async getAllRegions() : Promise { + let regionsSnapshot = await firestore.collection("regions").get(); + return regionsSnapshot.docs.map((r) => ({id: r.id, manager: r.data().manager})); + } + async getRegionsManagedBy(managerId: string): Promise { let regionsSnapshot = await firestore.collection("regions").where("manager", "==", managerId).get(); return regionsSnapshot.docs.map((r) => ({id: r.id, manager: r.data().manager})); diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index aa28c41..9bc57cf 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -27,14 +27,18 @@ interface UpdateRegionRequest extends RequestGenericInterface { export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer) { app.get('/regions', async (request) => { - let {userId} = <{userId:string}>await request.jwtVerify(); + let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); let response = { statusCode: 200, status: "ok", date: Date.now(), regions: [] } - response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); + if(admin) { + response.regions.push(...(await dataLayer.getAllRegions())); + } else { + response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); + } return JSON.stringify(response); } ); diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index 1554ec9..a5b8a18 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,37 +1,45 @@ import {fastify} from "fastify"; -import {testDataLayer} from "./utils/testDataLayer"; +import {DummyDatalayer} from "./utils/testDataLayer"; import createBusinessesEndpoint from "../src/endpoints/businesses"; import {createDummyBusiness, DummyBiz, DummyRegion} from "./utils/dummyData"; -it('Can create and retrieve a valid business', async(done) => { - const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - const createResponse = await createDummyBusiness(bizApp); - expect(createResponse.statusCode).toBe(200); +describe("Business Endpoint Tests", () => { + let testDataLayer: DummyDatalayer; - const getResponse = await bizApp.inject({ - method: 'GET', - url: `/regions/${DummyRegion.id}/businesses` + beforeEach(() => { + testDataLayer = new DummyDatalayer(); }); + it('Can create and retrieve a valid business', async(done) => { + const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); + const createResponse = await createDummyBusiness(bizApp); + expect(createResponse.statusCode).toBe(200); - expect(getResponse.statusCode).toBe(200); - expect(JSON.parse(getResponse.payload).businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: DummyBiz.name})])); + const getResponse = await bizApp.inject({ + method: 'GET', + url: `/regions/${DummyRegion.id}/businesses` + }); - await bizApp.close(); - done(); -}); + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: DummyBiz.name})])); -it('Can update and retrieve a business', async(done) => { - const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - const bizId = JSON.parse((await createDummyBusiness(bizApp)).payload).businessId; - const updatedBiz = {...DummyBiz, id: bizId, year_added: 2020, employees: 2}; - const updateResponse = await bizApp.inject({ - method: 'POST', - url: `/businesses/${bizId}`, - payload: updatedBiz + await bizApp.close(); + done(); }); - expect(updateResponse.statusCode).toBe(200); - expect(JSON.parse(updateResponse.payload).business).toEqual(updatedBiz); - await bizApp.close(); - done(); -}) + it('Can update and retrieve a business', async(done) => { + const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); + const bizId = JSON.parse((await createDummyBusiness(bizApp)).payload).businessId; + const updatedBiz = {...DummyBiz, id: bizId, year_added: 2020, employees: 2}; + const updateResponse = await bizApp.inject({ + method: 'POST', + url: `/businesses/${bizId}`, + payload: updatedBiz + }); + expect(updateResponse.statusCode).toBe(200); + expect(JSON.parse(updateResponse.payload).business).toEqual(updatedBiz); + + await bizApp.close(); + done(); + }); + +}); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index c3ff80a..38023d3 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -2,77 +2,88 @@ import {productionDataLayer, Region} from "../src/database/productionDataLayer"; import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; -beforeEach(async(done) => { - let bizDocs = (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs; - bizDocs.forEach((d) => d.ref.delete()); - await firestore.collection("years").doc("2019").delete(); - await firestore.collection("regions").doc("DummyRegion").delete(); - done(); -}); - -it("Creates, retrieves, updates, and deletes businesses", async (done) => { - let biz : Business = { - employees: 1, - name: "DummyBiz", - region: "DummyRegion", - year_added: 2019 - }; - let id = (await productionDataLayer.setBusiness(biz)).id; - expect(id).toBeTruthy(); - - let bizData = await productionDataLayer.getBusinessesByRegion(biz.region); - expect(bizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); - - let filters = await productionDataLayer.getFilters(biz.region); - expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) - - biz.id = id; - biz.employees = 2; - biz.year_added = 2020; - let updatedId = (await productionDataLayer.setBusiness(biz)).id; - expect(updatedId).toEqual(id); - - let updatedBizData = await productionDataLayer.getBusinessesByRegion(biz.region); - expect(updatedBizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); - - let updatedFilters = await productionDataLayer.getFilters(biz.region); - expect(updatedFilters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) - - await productionDataLayer.deleteBusiness(biz.id); - let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.region); - expect(emptyBizData).toEqual([]); - - let emptyFilters = await productionDataLayer.getFilters(biz.region); - expect(emptyFilters.years).toEqual([]); - - done(); -}); - -it("Creates, retrieves, updates, and deletes regions", async (done) => { - let region: Region = { - id: "DummyRegion", - manager: "Dummy Manager" - }; - - let id = await productionDataLayer.setRegion(region); - expect(id).toBeTruthy(); - - let out = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); - expect(out).toEqual(expect.arrayContaining([region])); - - region.manager = "New Manager"; - let updatedId = (await productionDataLayer.setRegion(region)); - expect(updatedId.id).toEqual(region.id); - - let oldManagerRegions = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); - expect(oldManagerRegions).toEqual(expect.arrayContaining([])); - - let newManagerRegions = await productionDataLayer.getRegionsManagedBy(region.manager); - expect(newManagerRegions).toEqual(expect.arrayContaining([region])); - - await productionDataLayer.deleteRegion(region.id); - let empty = await productionDataLayer.getRegionsManagedBy(region.manager); - expect(empty).toEqual([]); - - done(); +describe("Production Data Layer Tests", () => { + beforeEach(async(done) => { + let bizDocs = (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs; + bizDocs.forEach((d) => d.ref.delete()); + await firestore.collection("years").doc("2019").delete(); + await firestore.collection("regions").doc("DummyRegion").delete(); + await firestore.collection("regions").doc("DummyRegion2").delete(); + done(); + }); + + it("Creates, retrieves, updates, and deletes businesses", async (done) => { + let biz : Business = { + employees: 1, + name: "DummyBiz", + region: "DummyRegion", + year_added: 2019 + }; + let id = (await productionDataLayer.setBusiness(biz)).id; + expect(id).toBeTruthy(); + + let bizData = await productionDataLayer.getBusinessesByRegion(biz.region); + expect(bizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); + + let filters = await productionDataLayer.getFilters(biz.region); + expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) + + biz.id = id; + biz.employees = 2; + biz.year_added = 2020; + let updatedId = (await productionDataLayer.setBusiness(biz)).id; + expect(updatedId).toEqual(id); + + let updatedBizData = await productionDataLayer.getBusinessesByRegion(biz.region); + expect(updatedBizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); + + let updatedFilters = await productionDataLayer.getFilters(biz.region); + expect(updatedFilters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) + + await productionDataLayer.deleteBusiness(biz.id); + let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.region); + expect(emptyBizData).toEqual([]); + + let emptyFilters = await productionDataLayer.getFilters(biz.region); + expect(emptyFilters.years).toEqual([]); + + done(); + }); + + it("Creates, retrieves, updates, and deletes regions", async (done) => { + let region: Region = { + id: "DummyRegion", + manager: "Dummy Manager" + }; + + let id = await productionDataLayer.setRegion(region); + expect(id).toBeTruthy(); + + let out = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); + expect(out).toEqual(expect.arrayContaining([region])); + + region.manager = "New Manager"; + let updatedId = (await productionDataLayer.setRegion(region)); + expect(updatedId.id).toEqual(region.id); + + let oldManagerRegions = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); + expect(oldManagerRegions).toEqual(expect.arrayContaining([])); + + let newManagerRegions = await productionDataLayer.getRegionsManagedBy(region.manager); + expect(newManagerRegions).toEqual(expect.arrayContaining([region])); + + let region2: Region = { + id: "DummyRegion2", + manager: "Manager2" + }; + await productionDataLayer.setRegion(region2); + let adminRegions = await productionDataLayer.getAllRegions(); + expect(adminRegions).toEqual(expect.arrayContaining([region, region2])); + + await productionDataLayer.deleteRegion(region.id); + let empty = await productionDataLayer.getRegionsManagedBy(region.manager); + expect(empty).toEqual([]); + + done(); + }); }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index 90b07d9..eb0c85b 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,22 +1,26 @@ import {fastify} from "fastify"; import createFiltersEndpoint from "../src/endpoints/filters"; import createBusinessesEndpoint from "../src/endpoints/businesses"; -import {testDataLayer} from "./utils/testDataLayer"; +import {DummyDatalayer} from "./utils/testDataLayer"; import {createDummyBusiness, DummyBiz} from "./utils/dummyData"; -beforeAll(async (done) => { - const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - await createDummyBusiness(bizApp) - done(); -}) +describe("Filter Endpoint Tests", () => { + let testDataLayer: DummyDatalayer; -it('Returns the filter data added previously', async (done) => { - const filterApp = createFiltersEndpoint(fastify(), testDataLayer); - const filterResponse = await filterApp.inject({ method: 'GET', url: '/filters' }); + beforeAll(async (done) => { + testDataLayer = new DummyDatalayer(); + const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); + await createDummyBusiness(bizApp) + done(); + }); - expect(filterResponse.statusCode).toBe(200); - expect(JSON.parse(filterResponse.payload).years).toContain(DummyBiz.year_added); - await filterApp.close(); - done(); -}); + it('Returns the filter data added previously', async (done) => { + const filterApp = createFiltersEndpoint(fastify(), testDataLayer); + const filterResponse = await filterApp.inject({ method: 'GET', url: '/filters' }); + expect(filterResponse.statusCode).toBe(200); + expect(JSON.parse(filterResponse.payload).years).toContain(DummyBiz.year_added); + await filterApp.close(); + done(); + }); +}); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index d3dbf30..3cfbd57 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,57 +1,82 @@ import createRegionsEndpoint from "../src/endpoints/regions"; -import {testify, mockAuth0Return} from "./utils/testify"; -import {testDataLayer} from "./utils/testDataLayer"; -import {DummyRegion, dummyToken, requestDummyManagedRegions} from "./utils/dummyData"; - -beforeEach(async (done) => { - testDataLayer.clearRegions(); - await testDataLayer.setRegion(DummyRegion); - done(); -}); - -test('Can create and retrieve a region by manager ID', async (done) => { - const app = createRegionsEndpoint(testify(), testDataLayer); - mockAuth0Return.user = "DummyUser"; - const response = await requestDummyManagedRegions(app); - - expect(mockAuth0Return.callCount).toBe(1); - expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); - await app.close(); - done(); -}); - -it('Can update and retrieve a region', async (done) => { - const app = createRegionsEndpoint(testify(), testDataLayer); - const updatedRegion = { - id: "TestRegion", - manager: "TestManager" - }; - const response = await app.inject( { - method: 'POST', - url: `/regions/${DummyRegion.id}`, - payload: updatedRegion +import {testify, getMockToken, MockAuth0Return} from "./utils/testify"; +import {DummyDatalayer} from "./utils/testDataLayer"; +import {DummyRegion, dummyToken, getDummyRegions} from "./utils/dummyData"; +import {Region} from "../src/database/productionDataLayer"; +import {FastifyInstance} from "fastify"; + + +describe("Region Endpoint Tests", () => { + let testDataLayer: DummyDatalayer; + let mockAuth0Return: MockAuth0Return; + let testApp: FastifyInstance; + + beforeEach(async (done) => { + testDataLayer= new DummyDatalayer() + mockAuth0Return = new MockAuth0Return(); + testApp = testify(mockAuth0Return); + await testDataLayer.setRegion(DummyRegion); + done(); + }); + + test('Can create and retrieve all regions as SysAdmin', async (done) => { + const app = createRegionsEndpoint(testApp, testDataLayer); + let testRegions: Region[] = [{id: "region1", manager: "manager1"}, {id: "region2", manager: "manager2"}]; + testRegions.forEach((r) => testDataLayer.setRegion(r)); + mockAuth0Return.user = "DummyUser"; + const response = await getDummyRegions(app, getMockToken({userId: 'admin', admin: true})); + + expect(mockAuth0Return.callCount).toBe(1); + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions, DummyRegion])); + await app.close(); + done(); }); - expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).region).toEqual(updatedRegion); - await app.close(); - done(); -}); - -it('Can delete a region', async (done) => { - const app = createRegionsEndpoint(await testify(), testDataLayer); - const deleteResponse = await app.inject( { - method: 'DELETE', - url: `/regions/${DummyRegion.id}`, - headers: {authorization: `Bearer ${dummyToken}`} + test('Can create and retrieve a region as Region Manager', async (done) => { + const app = createRegionsEndpoint(testApp, testDataLayer); + mockAuth0Return.user = "DummyUser"; + const response = await getDummyRegions(app); + + expect(mockAuth0Return.callCount).toBe(1); + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); + await app.close(); + done(); + }); + + it('Can update and retrieve a region', async (done) => { + const app = createRegionsEndpoint(testApp, testDataLayer); + const updatedRegion = { + id: "TestRegion", + manager: "TestManager" + }; + const response = await app.inject( { + method: 'POST', + url: `/regions/${DummyRegion.id}`, + payload: updatedRegion + }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).region).toEqual(updatedRegion); + await app.close(); + done(); }); - expect(deleteResponse.statusCode).toBe(204); + it('Can delete a region', async (done) => { + const app = createRegionsEndpoint(testApp, testDataLayer); + const deleteResponse = await app.inject( { + method: 'DELETE', + url: `/regions/${DummyRegion.id}`, + headers: {authorization: `Bearer ${dummyToken}`} + }); + + expect(deleteResponse.statusCode).toBe(204); - const getResponse = await requestDummyManagedRegions(app); - expect(JSON.parse(getResponse.payload).regions).toEqual([]); + const getResponse = await getDummyRegions(app); + expect(JSON.parse(getResponse.payload).regions).toEqual([]); - await app.close(); - done(); -}); + await app.close(); + done(); + }); +}) diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index a4e3f35..5bde6bf 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -26,6 +26,6 @@ export async function createDummyBusiness(bizApp: FastifyInstance) { }); } -export async function requestDummyManagedRegions(app: FastifyInstance) { - return await app.inject({method: 'GET', headers: {authorization: `Bearer ${dummyToken}`}, url: `/regions`}); +export async function getDummyRegions(app: FastifyInstance, token: string = dummyToken) { + return await app.inject({method: 'GET', headers: {authorization: `Bearer ${token}`}, url: `/regions`}); } diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index 770301a..da34e9f 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -1,7 +1,7 @@ import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; import {Business} from "../../src/endpoints/businesses"; -class DummyDatalayer implements DataLayer { +export class DummyDatalayer implements DataLayer { businesses: Business[] = []; regions: Region[] = []; @@ -25,6 +25,10 @@ class DummyDatalayer implements DataLayer { return {id: region.id}; } + async getAllRegions() : Promise { + return this.regions; + } + async getRegionsManagedBy(managerId: string ): Promise { return this.regions.filter((r) => managerId === r.manager); } @@ -37,5 +41,3 @@ class DummyDatalayer implements DataLayer { this.regions = []; } } - -export const testDataLayer = new DummyDatalayer(); diff --git a/tests/utils/testify.ts b/tests/utils/testify.ts index 0251593..c635c86 100644 --- a/tests/utils/testify.ts +++ b/tests/utils/testify.ts @@ -3,16 +3,20 @@ import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; import jwt from "jsonwebtoken"; export const mockSecret = 'dummy'; -export const mockAuth0Return = { - user: null, - callCount: 0 -}; +export class MockAuth0Return { + user: string|null = null; + callCount: number = 0; + reset() { + this.callCount = 0; + this.user = null + } +} export function getMockToken(payload: object) { return jwt.sign(payload, mockSecret) } -export const testify = () => { +export const testify = (mockAuth0Return : MockAuth0Return) => { const f = fastify(); f.register(fastifyJWT, { secret: (_request, _reply, _provider) => { mockAuth0Return.callCount++; _provider(null, mockSecret);}, From 0e46d6e03f96b41504e5761b0c4da069f65607e1 Mon Sep 17 00:00:00 2001 From: "Mike Burton (Celtx)" Date: Wed, 30 Dec 2020 18:54:26 -0330 Subject: [PATCH 23/75] Started the paging for businesses --- src/auth0.ts | 4 ++-- src/endpoints/businesses.ts | 11 ++++++++--- src/index.ts | 7 ++++--- tests/businesses.test.ts | 31 ++++++++++++++++++++++++++++--- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/auth0.ts b/src/auth0.ts index 69b2c14..4ce9d8a 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -7,13 +7,13 @@ export function registerAuth0(fastify: FastifyInstance) { cache: true, rateLimit: true, jwksRequestsPerMinute: 5, - jwksUri: 'http://__tenant__.auth0.com/.well-known/jwks.json' + jwksUri: 'http://lesleychard.auth0.com/.well-known/jwks.json' }); fastify.register(fastifyJwt, { secret: faSecretProvider, audience: 'https://api.example.com', - issuer: 'https://__tenant__.auth0.com/', + issuer: 'https://lesleychard.auth0.com/', algorithms: ['RS256'], decode: { complete: true }, }); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 4622919..a547cfc 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -32,20 +32,25 @@ export interface Business { location?: GeoPoint | null | undefined } -export default function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { +export function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/regions/:region/businesses', async (request) => { let response = { status: "ok", date: Date.now(), region: request.params.region, - businesses: [] + businesses: [], + pageStart: "1", + pageEnd: "2" } - response.businesses = await dataLayer.getBusinessesByRegion(request.params.region); + response.businesses = (await dataLayer.getBusinessesByRegion(request.params.region)).slice(0, 10); return JSON.stringify(response); } ); + return app; +} +export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.post( '/businesses', async (request) => { diff --git a/src/index.ts b/src/index.ts index 6f607c6..89faf83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import fastify, {FastifyInstance} from 'fastify'; import createPingEndpoint from './endpoints/ping'; import { addRoutes } from './utils'; -import createRegionBusinessesEndpoint from "./endpoints/businesses"; +import {createRegionBusinessesEndpoint, createBusinessesEndpoint} from "./endpoints/businesses"; import createRegionsEndpoint from "./endpoints/regions"; import {productionDataLayer} from "./database/productionDataLayer"; import createFiltersEndpoint from "./endpoints/filters"; @@ -12,8 +12,9 @@ const server = addRoutes( fastify(), createPingEndpoint, (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer), - (app: FastifyInstance) => createRegionBusinessesEndpoint(app, productionDataLayer), - (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer) + (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer), + (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer), + (app: FastifyInstance) => createRegionBusinessesEndpoint(app, productionDataLayer) ); registerAuth0(server); diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index a5b8a18..f9a4ac8 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,7 +1,8 @@ import {fastify} from "fastify"; import {DummyDatalayer} from "./utils/testDataLayer"; -import createBusinessesEndpoint from "../src/endpoints/businesses"; +import {createBusinessesEndpoint, createRegionBusinessesEndpoint} from "../src/endpoints/businesses"; import {createDummyBusiness, DummyBiz, DummyRegion} from "./utils/dummyData"; +import {MockAuth0Return, testify} from "./utils/testify"; describe("Business Endpoint Tests", () => { let testDataLayer: DummyDatalayer; @@ -10,11 +11,13 @@ describe("Business Endpoint Tests", () => { testDataLayer = new DummyDatalayer(); }); it('Can create and retrieve a valid business', async(done) => { - const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); + const server = testify(new MockAuth0Return()); + const bizApp = createBusinessesEndpoint(server, testDataLayer) + const regionBizApp = createRegionBusinessesEndpoint(server, testDataLayer); const createResponse = await createDummyBusiness(bizApp); expect(createResponse.statusCode).toBe(200); - const getResponse = await bizApp.inject({ + const getResponse = await regionBizApp.inject({ method: 'GET', url: `/regions/${DummyRegion.id}/businesses` }); @@ -26,6 +29,28 @@ describe("Business Endpoint Tests", () => { done(); }); + it('Can paginate businesses', async(done) => { + const server = testify(new MockAuth0Return()); + const bizApp = createBusinessesEndpoint(server, testDataLayer); + const regionBizApp = createRegionBusinessesEndpoint(server, testDataLayer); + for(let i = 0; i < 20; i++) { + await createDummyBusiness(bizApp); + } + const pagedResponse = await regionBizApp.inject({ + method: "GET", + url: `/regions/${DummyRegion.id}/businesses` + }); + expect(pagedResponse.statusCode).toBe(200); + const pagedPayload = JSON.parse(pagedResponse.payload); + expect(pagedPayload).toStrictEqual(expect.objectContaining({ + businesses: expect.arrayContaining([expect.anything()]), + pageStart: expect.stringMatching("[a-z0-9]*"), + pageEnd: expect.stringMatching("[a-z0-9]*") + })); + expect(pagedPayload.businesses).toHaveLength(10); + done(); + }); + it('Can update and retrieve a business', async(done) => { const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); const bizId = JSON.parse((await createDummyBusiness(bizApp)).payload).businessId; From fdd2478ec1a8e9d6a4f48ae2ab576aecfdd92565 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Wed, 30 Dec 2020 19:21:34 -0330 Subject: [PATCH 24/75] Resolved merge conflicts between two Christmas worksets (filters/pagination), made endpoint export style consistent, fixed a couple of resultant errors in tests. --- src/database/productionDataLayer.ts | 102 +++++++++++++++++++++++++--- src/endpoints/businesses.ts | 10 ++- src/endpoints/filters.ts | 2 +- tests/businesses.test.ts | 5 +- tests/dataLayer.test.ts | 7 +- tests/filters.test.ts | 4 +- tests/utils/dummyData.ts | 3 +- tests/utils/testDataLayer.ts | 3 +- 8 files changed, 114 insertions(+), 22 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 01b20f6..b87a939 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,5 +1,6 @@ import {Business} from "../endpoints/businesses"; import {firestore} from "./firestore"; +import firebase from "firebase"; export interface IdObject { id: string @@ -7,7 +8,9 @@ export interface IdObject { export interface Region { id: string, - manager: string + manager: string, + years?: {year: number, count: number}[] | undefined, + industries?: {industry: string, count: number}[] | undefined, } export interface DataLayer { @@ -21,7 +24,8 @@ export interface DataLayer { } export interface Filters { - years?: number[] | undefined + years?: number[] | undefined, + industries?: string[] } export class ProductionDataLayer implements DataLayer { @@ -30,18 +34,56 @@ export class ProductionDataLayer implements DataLayer { return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); } - async setBusiness(business: Business) : Promise { + async setBusiness(newBusinessData: Business) : Promise { const bc = firestore.collection("businesses"); - let businessRef = business.id ? bc.doc(business.id) : bc.doc(); - await businessRef.set(business); - await firestore.collection("years").doc(`${business.year_added}`).set({}); - return {id: businessRef.id}; + let businessRef = newBusinessData.id ? bc.doc(newBusinessData.id) : bc.doc(); + let regionRef = firestore.collection("regions").doc(newBusinessData.region); + return firestore.runTransaction(async transaction => { + let regionDoc = await transaction.get(regionRef); + if(!regionDoc.exists) { + throw "Bad Region"; + } + let {years, industries} = this.calculateNewFilters(regionDoc, newBusinessData, 1); + + let businessDoc = await transaction.get(businessRef); + if(businessDoc.exists) { + let existingBusinessData = businessDoc.data(); + if(!!existingBusinessData && !!existingBusinessData.year_added) { + // @ts-ignore I don't know why, but it thinks existingBusinessData could be null here, despite the enclosing if statement + let oldYearIndex = years.findIndex((y) => y.year === existingBusinessData.year_added); + if(oldYearIndex >= 0) { + if (years[oldYearIndex].count > 1) { + years[oldYearIndex].count -= 1; + } else { + years.splice(oldYearIndex, 1); + } + } + } + if (!!existingBusinessData && !!existingBusinessData.industry) { + // @ts-ignore I don't know why, but it thinks existingBusinessData could be null here, despite the enclosing if statement + let existingIndustryIndex = industries.findIndex(i => i.industry === existingBusinessData.industry); + if(existingIndustryIndex >= 0) { + if (industries[existingIndustryIndex].count > 1) { + industries[existingIndustryIndex].count -= 1; + } else { + industries.splice(existingIndustryIndex, 1); + } + } + } + } + let update = {years: years, industries: industries}; + + await transaction.update(regionRef, update) + await transaction.set(businessRef, newBusinessData); + }).then(() => {id: businessRef.id}); } async getFilters(region: string) : Promise{ + let regionData = (await firestore.collection("regions").doc(region).get()).data(); + regionData = !!regionData ? regionData : {}; return { - years: (await firestore.collection("businesses").where("region", "==", region).get()).docs - .map((b) => ((b.data()).year_added)) + years: regionData.years ? regionData.years : [], + industries: regionData.industries ? regionData.industries : [] }; } @@ -65,7 +107,47 @@ export class ProductionDataLayer implements DataLayer { } async deleteBusiness(id: string) { - await firestore.collection("businesses").doc(id).delete(); + await firestore.runTransaction( + async transaction => { + let businessRef = firestore.collection("businesses").doc(id); + let businessDoc = await transaction.get(businessRef); + let businessData = businessDoc.data(); + if (!!businessData && !!businessData.region) { + let regionRef = firestore.collection("regions").doc(businessData.region); + let regionDoc = await transaction.get(regionRef); + let {years, industries} = this.calculateNewFilters(regionDoc, businessData, -1); + + let update = {years: years, industries: industries}; + + await transaction.update(regionRef, update) + await transaction.delete(businessRef) + } + } + ); + } + + private calculateNewFilters(regionDoc: firebase.firestore.DocumentSnapshot, businessData: firebase.firestore.DocumentData , change: number) { + let regionData = regionDoc.data(); + let years: { year: number, count: number }[] = !!regionData && !!regionData.years ? regionData.years : []; + let yearEntryIndex = years.findIndex(y => y.year === businessData.year_added); + if (yearEntryIndex < 0 && change > 0) { + years.push({year: businessData.year_added, count: change}); + } else if (years[yearEntryIndex].count + change > 0) { + years[yearEntryIndex].count += change; + } else { + years.splice(yearEntryIndex, 1); + } + + let industries: { industry: string, count: number }[] = !!regionData && !!regionData.industries ? regionData.industries : []; + let industryEntryIndex = industries.findIndex(i => i.industry === businessData.industry); + if (industryEntryIndex < 0 && change > 0) { + industries.push({industry: businessData.industry, count: change}); + } else if (industries[industryEntryIndex].count + change > 0) { + industries[industryEntryIndex].count += change; + } else { + industries.splice(industryEntryIndex, 1); + } + return {years, industries}; } } diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index a547cfc..91c642c 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -1,5 +1,5 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {DataLayer} from "../database/productionDataLayer"; +import {DataLayer, Filters} from "../database/productionDataLayer"; import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; @@ -28,6 +28,7 @@ export interface Business { name: string; employees: number; region: string; + industry: string; year_added: number; location?: GeoPoint | null | undefined } @@ -41,9 +42,12 @@ export function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: region: request.params.region, businesses: [], pageStart: "1", - pageEnd: "2" - } + pageEnd: "2", + filters: {} + }; + response.businesses = (await dataLayer.getBusinessesByRegion(request.params.region)).slice(0, 10); + response.filters = await dataLayer.getFilters(request.params.region); return JSON.stringify(response); } ); diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index e56bbdb..42eb8ed 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -7,7 +7,7 @@ interface GetFiltersRequest extends RequestGenericInterface { } } -export default function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { +export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/filters', async (request) => { let response = { diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index f9a4ac8..ed0b830 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -23,8 +23,9 @@ describe("Business Endpoint Tests", () => { }); expect(getResponse.statusCode).toBe(200); - expect(JSON.parse(getResponse.payload).businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: DummyBiz.name})])); - + let {businesses, filters} = JSON.parse(getResponse.payload); + expect(businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: DummyBiz.name})])); + expect(filters).toEqual(expect.objectContaining({years: [DummyBiz.year_added], industries: [DummyBiz.industry]})) await bizApp.close(); done(); }); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 38023d3..4639518 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -17,8 +17,11 @@ describe("Production Data Layer Tests", () => { employees: 1, name: "DummyBiz", region: "DummyRegion", + industry: "DummyIndustry", year_added: 2019 }; + await productionDataLayer.setRegion({id: "DummyRegion", manager: "Dummy"}) + let id = (await productionDataLayer.setBusiness(biz)).id; expect(id).toBeTruthy(); @@ -26,7 +29,7 @@ describe("Production Data Layer Tests", () => { expect(bizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); let filters = await productionDataLayer.getFilters(biz.region); - expect(filters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) + expect(filters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})) biz.id = id; biz.employees = 2; @@ -38,7 +41,7 @@ describe("Production Data Layer Tests", () => { expect(updatedBizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); let updatedFilters = await productionDataLayer.getFilters(biz.region); - expect(updatedFilters).toEqual(expect.objectContaining({years: expect.arrayContaining([biz.year_added])})) + expect(updatedFilters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})); await productionDataLayer.deleteBusiness(biz.id); let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.region); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index eb0c85b..b40a1ba 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,6 +1,6 @@ import {fastify} from "fastify"; -import createFiltersEndpoint from "../src/endpoints/filters"; -import createBusinessesEndpoint from "../src/endpoints/businesses"; +import {createFiltersEndpoint} from "../src/endpoints/filters"; +import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import {DummyDatalayer} from "./utils/testDataLayer"; import {createDummyBusiness, DummyBiz} from "./utils/dummyData"; diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index 5bde6bf..c1aca0c 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -15,7 +15,8 @@ export const DummyBiz: Business = { name: "DummyBiz", region: DummyRegion.id, year_added: 2009, - employees: 1 + employees: 1, + industry: "DummyIndustry" }; export async function createDummyBusiness(bizApp: FastifyInstance) { diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index da34e9f..7d8a550 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -16,7 +16,8 @@ export class DummyDatalayer implements DataLayer { async getFilters(_: string): Promise { return { - years: this.businesses.map((b) => b.year_added) + years: this.businesses.map((b) => b.year_added), + industries: this.businesses.map((b) => b.industry) }; } From ebdc27fd11a0db80f9fdb4e066bfd664cd52bf8c Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 14 Jan 2021 03:49:44 -0330 Subject: [PATCH 25/75] Adding single region GET endpoint + test --- src/endpoints/regions.ts | 28 ++++++++++++++++++++++++++++ tests/regions.test.ts | 30 +++++++++++++++++++++++++++--- tests/utils/dummyData.ts | 1 + 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 9bc57cf..4ee3a0d 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -7,6 +7,13 @@ interface GetManagedRegionsRequest extends RequestGenericInterface { } } +interface GetSingleRegionRequest extends RequestGenericInterface { + Params: { + regionId: string + } +} + + interface CreateRegionRequest extends RequestGenericInterface { Body: Region } @@ -55,6 +62,27 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : } ); + app.get('/regions/:regionId', + async(request ) => { + let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); + let response = { + statusCode: 200, + status: "ok", + date: Date.now(), + region: null + } + let regions : Region[] ; + if(admin) { + regions = (await dataLayer.getAllRegions()); + } else { + regions = (await dataLayer.getRegionsManagedBy(userId)); + } + let region: Region | undefined = regions.find((r => r.id == request.params.regionId)) + response.region = !!region ? region : null; + return JSON.stringify(response); + } + ); + app.post('/regions/:regionId', async(request) => { let UpdatedRegion: Region = {...request.body}; diff --git a/tests/regions.test.ts b/tests/regions.test.ts index 3cfbd57..f24e585 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,7 +1,7 @@ import createRegionsEndpoint from "../src/endpoints/regions"; import {testify, getMockToken, MockAuth0Return} from "./utils/testify"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {DummyRegion, dummyToken, getDummyRegions} from "./utils/dummyData"; +import {dummyAdminToken, DummyRegion, dummyToken, getDummyRegions} from "./utils/dummyData"; import {Region} from "../src/database/productionDataLayer"; import {FastifyInstance} from "fastify"; @@ -19,7 +19,7 @@ describe("Region Endpoint Tests", () => { done(); }); - test('Can create and retrieve all regions as SysAdmin', async (done) => { + it('Can create and retrieve all regions as SysAdmin', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer); let testRegions: Region[] = [{id: "region1", manager: "manager1"}, {id: "region2", manager: "manager2"}]; testRegions.forEach((r) => testDataLayer.setRegion(r)); @@ -33,7 +33,7 @@ describe("Region Endpoint Tests", () => { done(); }); - test('Can create and retrieve a region as Region Manager', async (done) => { + it('Can create and retrieve a region as Region Manager', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer); mockAuth0Return.user = "DummyUser"; const response = await getDummyRegions(app); @@ -79,4 +79,28 @@ describe("Region Endpoint Tests", () => { await app.close(); done(); }); + + it("Can retrieve a single region as either region admin or system admin", async(done) => { + const app = createRegionsEndpoint(testApp, testDataLayer); + const getRegionAdminResponse = await app.inject({ + method: 'GET', + url: `/regions/${DummyRegion.id}`, + headers: {authorization: `Bearer ${dummyToken}`} + }); + + expect(getRegionAdminResponse.statusCode).toBe(200); + expect(JSON.parse(getRegionAdminResponse.payload).region).toStrictEqual(DummyRegion); + + const getSysAdminResponse = await app.inject({ + method: 'GET', + url: `/regions/${DummyRegion.id}`, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + + expect(getSysAdminResponse.statusCode).toBe(200); + expect(JSON.parse(getSysAdminResponse.payload).region).toStrictEqual(DummyRegion); + + await app.close(); + done(); + }); }) diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index c1aca0c..0698891 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -5,6 +5,7 @@ import {getMockToken} from "./testify"; const dummyManager = "DummyManagerId"; export const dummyToken = getMockToken({userId: dummyManager}) +export const dummyAdminToken = getMockToken({userId: "", admin: true}); export const DummyRegion: Region = { id: "DummyRegion", From 5307ddfc7c314c4b76e59c892bee65e060550175 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 16 Jan 2021 16:01:19 -0330 Subject: [PATCH 26/75] Fixing up filters --- src/endpoints/filters.ts | 9 ++++----- tests/filters.test.ts | 23 ++++++++++++++++++----- tests/utils/testDataLayer.ts | 6 +++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index 42eb8ed..89c0ffc 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,5 +1,5 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; -import {DataLayer} from "../database/productionDataLayer"; +import {DataLayer, Filters} from "../database/productionDataLayer"; interface GetFiltersRequest extends RequestGenericInterface { Params: { @@ -8,15 +8,14 @@ interface GetFiltersRequest extends RequestGenericInterface { } export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { - app.get('/filters', + app.get('/filters/:regionId', async (request) => { let response = { status: "ok", date: Date.now(), - years: [] + filters: null } - - response.years = (await dataLayer.getFilters(request.params.regionId)).years; + response.filters = await dataLayer.getFilters(request.params.regionId); return JSON.stringify(response); } ); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index b40a1ba..f33a493 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,8 +1,8 @@ import {fastify} from "fastify"; import {createFiltersEndpoint} from "../src/endpoints/filters"; -import {createBusinessesEndpoint} from "../src/endpoints/businesses"; +import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {createDummyBusiness, DummyBiz} from "./utils/dummyData"; +import {createDummyBusiness, DummyBiz, DummyRegion} from "./utils/dummyData"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; @@ -11,15 +11,28 @@ describe("Filter Endpoint Tests", () => { testDataLayer = new DummyDatalayer(); const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); await createDummyBusiness(bizApp) + await createDummyBusiness(bizApp, { + name: `Not ${DummyBiz.name}`, + employees: 1, + region: `Not ${DummyRegion.id}`, + industry: `Not ${DummyBiz.industry}`, + year_added: DummyBiz.year_added + 1 + }); done(); }); - it('Returns the filter data added previously', async (done) => { + it('Returns only the region-specific filter data', async (done) => { const filterApp = createFiltersEndpoint(fastify(), testDataLayer); - const filterResponse = await filterApp.inject({ method: 'GET', url: '/filters' }); + const filterResponse = await filterApp.inject({ method: 'GET', url: `/filters/${DummyRegion.id}` }); expect(filterResponse.statusCode).toBe(200); - expect(JSON.parse(filterResponse.payload).years).toContain(DummyBiz.year_added); + expect(JSON.parse(filterResponse.payload).filters).toStrictEqual( + expect.objectContaining({ + years: [DummyBiz.year_added], + industries: [DummyBiz.industry] + }) + ); + await filterApp.close(); done(); }); diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index 7d8a550..e6f8829 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -14,10 +14,10 @@ export class DummyDatalayer implements DataLayer { return {id:"1"}; } - async getFilters(_: string): Promise { + async getFilters(regionId: string): Promise { return { - years: this.businesses.map((b) => b.year_added), - industries: this.businesses.map((b) => b.industry) + years: this.businesses.filter(b => b.region=== regionId).map((b) => b.year_added), + industries: this.businesses.filter(b => b.region=== regionId).map((b) => b.industry) }; } From 82a571cfa7547de488086447ad0dd08933161313 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 16 Jan 2021 20:38:40 -0330 Subject: [PATCH 27/75] Adding CORS handling and tests --- package.json | 1 + src/cors.ts | 10 + src/index.ts | 5 +- tests/cors.test.ts | 35 ++ yarn.lock | 1383 ++++++++++++++++++++++---------------------- 5 files changed, 746 insertions(+), 688 deletions(-) create mode 100644 src/cors.ts create mode 100644 tests/cors.test.ts diff --git a/package.json b/package.json index 6a8daf2..a801c30 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "body-parser": "^1.19.0", "fastify": "^3.5.1", "fastify-authz-jwks": "^1.1.11", + "fastify-cors": "^5.1.0", "fastify-jwt": "^2.1.3", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0" diff --git a/src/cors.ts b/src/cors.ts new file mode 100644 index 0000000..1e4622b --- /dev/null +++ b/src/cors.ts @@ -0,0 +1,10 @@ +import {FastifyInstance} from "fastify"; +import fastifyCors from "fastify-cors"; + +export function registerCorsHandler(server: FastifyInstance) { + server.register(fastifyCors, { + origin: ["https://localhost", "http://localhost", "https://ranlab-app-phzez.ondigitalocean.app/"], + credentials: true, + strictPreflight: true + }); +} diff --git a/src/index.ts b/src/index.ts index 89faf83..3a25d31 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,10 @@ import { addRoutes } from './utils'; import {createRegionBusinessesEndpoint, createBusinessesEndpoint} from "./endpoints/businesses"; import createRegionsEndpoint from "./endpoints/regions"; import {productionDataLayer} from "./database/productionDataLayer"; -import createFiltersEndpoint from "./endpoints/filters"; +import {createFiltersEndpoint} from "./endpoints/filters"; import {registerAuth0} from "./auth0"; +import {registerCorsHandler} from "./cors"; + const port = Number(process.env.PORT || 8080); const server = addRoutes( @@ -18,6 +20,7 @@ const server = addRoutes( ); registerAuth0(server); +registerCorsHandler(server); server.listen(port, '::', (err, address) => { if (err) { diff --git a/tests/cors.test.ts b/tests/cors.test.ts new file mode 100644 index 0000000..6317470 --- /dev/null +++ b/tests/cors.test.ts @@ -0,0 +1,35 @@ +import {registerCorsHandler} from "../src/cors"; +import {MockAuth0Return, testify} from "./utils/testify"; +import {DummyRegion} from "./utils/dummyData"; + + +describe('CORS Handler Tests', function () { + async function testCorsRequest(origin: string, originResponse: string | boolean) { + let mockReturn = new MockAuth0Return(); + let testApp = testify(mockReturn); + mockReturn.user = "Dummy"; + registerCorsHandler(testApp); + let response = await testApp.inject({ + method: 'OPTIONS', + url: `/regions/${DummyRegion.id}/businesses`, + headers: { + 'authorization': 'Bearer abc123', + 'access-control-request-headers': 'x-requested-with', + 'access-control-request-method': 'GET', + origin: origin + } + }); + + expect(response.headers["access-control-allow-origin"]).toBe(originResponse); + await testApp.close(); + } + + it("Accepts localhost requests", async (done) => { + await testCorsRequest("http://localhost", "http://localhost"); + done(); + }); + it("Does not accept requests from unapproved origin", async (done) => { + await testCorsRequest('http://unapproved.com', false); + done(); + }); +}); diff --git a/yarn.lock b/yarn.lock index ea2336f..a44d86f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4,14 +4,14 @@ "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: "@babel/highlight" "^7.10.4" "@babel/core@^7.1.0", "@babel/core@^7.7.5": version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz" integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== dependencies: "@babel/code-frame" "^7.10.4" @@ -33,7 +33,7 @@ "@babel/generator@^7.12.1", "@babel/generator@^7.12.5": version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz" integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== dependencies: "@babel/types" "^7.12.5" @@ -42,7 +42,7 @@ "@babel/helper-function-name@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz" integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== dependencies: "@babel/helper-get-function-arity" "^7.10.4" @@ -51,28 +51,28 @@ "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz" integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== dependencies: "@babel/types" "^7.10.4" "@babel/helper-member-expression-to-functions@^7.12.1": version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz" integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== dependencies: "@babel/types" "^7.12.1" "@babel/helper-module-imports@^7.12.1": version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz" integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== dependencies: "@babel/types" "^7.12.5" "@babel/helper-module-transforms@^7.12.1": version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz" integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== dependencies: "@babel/helper-module-imports" "^7.12.1" @@ -87,19 +87,19 @@ "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz" integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== dependencies: "@babel/types" "^7.10.4" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== "@babel/helper-replace-supers@^7.12.1": version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz" integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA== dependencies: "@babel/helper-member-expression-to-functions" "^7.12.1" @@ -109,26 +109,26 @@ "@babel/helper-simple-access@^7.12.1": version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz" integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== dependencies: "@babel/types" "^7.12.1" "@babel/helper-split-export-declaration@^7.11.0": version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz" integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== dependencies: "@babel/types" "^7.11.0" "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== "@babel/helpers@^7.12.1": version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz" integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== dependencies: "@babel/template" "^7.10.4" @@ -137,7 +137,7 @@ "@babel/highlight@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz" integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== dependencies: "@babel/helper-validator-identifier" "^7.10.4" @@ -146,96 +146,96 @@ "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.12.3", "@babel/parser@^7.12.5": version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz" integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.8.3": version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz" integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-top-level-await@^7.8.3": version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz" integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/template@^7.10.4", "@babel/template@^7.3.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== dependencies: "@babel/code-frame" "^7.10.4" @@ -244,7 +244,7 @@ "@babel/traverse@^7.1.0", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5": version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz" integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA== dependencies: "@babel/code-frame" "^7.10.4" @@ -259,7 +259,7 @@ "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.12.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz" integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== dependencies: "@babel/helper-validator-identifier" "^7.10.4" @@ -268,12 +268,12 @@ "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@cnakazawa/watch@^1.0.3": version "1.0.4" - resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz" integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== dependencies: exec-sh "^0.3.2" @@ -281,12 +281,12 @@ "@firebase/analytics-types@0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz#d6716f9fa36a6e340bc0ecfe68af325aa6f60508" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz" integrity sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA== "@firebase/analytics@0.6.2": version "0.6.2" - resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.6.2.tgz#7f45675a1b524fff4d9e9fe318fd6e2ed067a325" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.6.2.tgz" integrity sha512-4Ceov+rPfOEPIdbjlpTim/wbcUUneIesHag4UOzvmFsRRXqbxLwQpyZQWEbTSriUeU8uTKj9yOW32hsskV9Klg== dependencies: "@firebase/analytics-types" "0.4.0" @@ -298,12 +298,12 @@ "@firebase/app-types@0.6.1": version "0.6.1" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.1.tgz#dcbd23030a71c0c74fc95d4a3f75ba81653850e9" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.1.tgz" integrity sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg== "@firebase/app@0.6.13": version "0.6.13" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.6.13.tgz#f2e9fa9e75815e54161dc34659a60f1fffd9a450" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.6.13.tgz" integrity sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ== dependencies: "@firebase/app-types" "0.6.1" @@ -316,24 +316,24 @@ "@firebase/auth-interop-types@0.1.5": version "0.1.5" - resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz#9fc9bd7c879f16b8d1bb08373a0f48c3a8b74557" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz" integrity sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw== "@firebase/auth-types@0.10.1": version "0.10.1" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.1.tgz#7815e71c9c6f072034415524b29ca8f1d1770660" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.1.tgz" integrity sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw== "@firebase/auth@0.15.2": version "0.15.2" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.2.tgz#9ada3f37620d131a1c56994138a599b5c9f9ca2e" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.2.tgz" integrity sha512-2n32PBi6x9jVhc0E/ewKLUCYYTzFEXL4PNkvrrlGKbzeTBEkkyzfgUX7OV9UF5wUOG+gurtUthuur1zspZ/9hg== dependencies: "@firebase/auth-types" "0.10.1" "@firebase/component@0.1.21": version "0.1.21" - resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.1.21.tgz#56062eb0d449dc1e7bbef3c084a9b5fa48c7c14d" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.1.21.tgz" integrity sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg== dependencies: "@firebase/util" "0.3.4" @@ -341,14 +341,14 @@ "@firebase/database-types@0.6.0": version "0.6.0" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.6.0.tgz#7795bc6b1db93f4cbda9a241c8dfe1bb86033dc6" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.6.0.tgz" integrity sha512-ljpU7/uboCGqFSe9CNgwd3+Xu5N8YCunzfPpeueuj2vjnmmypUi4QWxgC3UKtGbuv1q+crjeudZGLxnUoO0h7w== dependencies: "@firebase/app-types" "0.6.1" "@firebase/database@0.7.1": version "0.7.1" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.7.1.tgz#900d2e6ed734249e65e5f159293830e4f4285d6e" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.7.1.tgz" integrity sha512-8j3KwksaYMSbIsEjOIarZD3vj4jGJjIlLGIAiO/4P4XyOtrlnxIiH7G0UdIZlcvKU4Gsgg0nthT2+EapROmHWA== dependencies: "@firebase/auth-interop-types" "0.1.5" @@ -361,12 +361,12 @@ "@firebase/firestore-types@2.0.0": version "2.0.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.0.0.tgz#1f6212553b240f1a8905bb8dcf1f87769138c5c0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.0.0.tgz" integrity sha512-ZGb7p1SSQJP0Z+kc9GAUi+Fx5rJatFddBrS1ikkayW+QHfSIz0omU23OgSHcBGTxe8dJCeKiKA2Yf+tkDKO/LA== "@firebase/firestore@2.0.2": version "2.0.2" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.0.2.tgz#300e7430a08d8c8779471e43c892a2cefe405b62" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.0.2.tgz" integrity sha512-6kO/vWUmTOANA/ql+i16DFMc63gamU76Nycyt7k0r8QfcdXu93Cwizw4ff4DNMnpnkAJkTk36fPAxBxEvBXkzw== dependencies: "@firebase/component" "0.1.21" @@ -381,12 +381,12 @@ "@firebase/functions-types@0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.4.0.tgz#0b789f4fe9a9c0b987606c4da10139345b40f6b9" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.4.0.tgz" integrity sha512-3KElyO3887HNxtxNF1ytGFrNmqD+hheqjwmT3sI09FaDCuaxGbOnsXAXH2eQ049XRXw9YQpHMgYws/aUNgXVyQ== "@firebase/functions@0.6.1": version "0.6.1" - resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.6.1.tgz#32640b8f877637057dfaaeb122be8c8e99ad1af7" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.6.1.tgz" integrity sha512-xNCAY3cLlVWE8Azf+/84OjnaXMoyUstJ3vwVRG0ie22QhsdQuPa1tXTiPX4Tmm+Hbbd/Aw0A/7dkEnuW+zYzaQ== dependencies: "@firebase/component" "0.1.21" @@ -397,12 +397,12 @@ "@firebase/installations-types@0.3.4": version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.3.4.tgz#589a941d713f4f64bf9f4feb7f463505bab1afa2" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.3.4.tgz" integrity sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q== "@firebase/installations@0.4.19": version "0.4.19" - resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.4.19.tgz#53f50aeb022996963f89f59560d7b4cf801869da" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.4.19.tgz" integrity sha512-QqAQzosKVVqIx7oMt5ujF4NsIXgtlTnej4JXGJ8sQQuJoMnt3T+PFQRHbr7uOfVaBiHYhEaXCcmmhfKUHwKftw== dependencies: "@firebase/component" "0.1.21" @@ -413,17 +413,17 @@ "@firebase/logger@0.2.6": version "0.2.6" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.6.tgz" integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== "@firebase/messaging-types@0.5.0": version "0.5.0" - resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.5.0.tgz#c5d0ef309ced1758fda93ef3ac70a786de2e73c4" + resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.5.0.tgz" integrity sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg== "@firebase/messaging@0.7.3": version "0.7.3" - resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.7.3.tgz#31dded892455e4d0680e1452ff2fbfdfb9e4ce9b" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.7.3.tgz" integrity sha512-63nOP2SmQJrj9jrhV3K96L5MRKS6AqmFVLX1XbGk6K6lz38ZC4LIoCcHxzUBXY7fCAuZvNmh/YB3pE8B2mTs8A== dependencies: "@firebase/component" "0.1.21" @@ -435,12 +435,12 @@ "@firebase/performance-types@0.0.13": version "0.0.13" - resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.13.tgz#58ce5453f57e34b18186f74ef11550dfc558ede6" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.13.tgz" integrity sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA== "@firebase/performance@0.4.4": version "0.4.4" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.4.4.tgz#5f13ea3b9a72a0ae9c36520c419be31448a0955a" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.4.4.tgz" integrity sha512-CY/fzz7qGQ9hUkvOow22MeJhayHSjXmI4+0AqcxaUC4CWk4oQubyIC4pk62aH+yCwZNNeC7JJUEDbtqI/0rGkQ== dependencies: "@firebase/component" "0.1.21" @@ -452,7 +452,7 @@ "@firebase/polyfill@0.3.36": version "0.3.36" - resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.36.tgz#c057cce6748170f36966b555749472b25efdb145" + resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.36.tgz" integrity sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg== dependencies: core-js "3.6.5" @@ -461,12 +461,12 @@ "@firebase/remote-config-types@0.1.9": version "0.1.9" - resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz#fe6bbe4d08f3b6e92fce30e4b7a9f4d6a96d6965" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz" integrity sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA== "@firebase/remote-config@0.1.30": version "0.1.30" - resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.30.tgz#2cd6bbbed526a98b154e13a2cc73e748a77d7c3d" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.30.tgz" integrity sha512-LAfLDcp1AN0V/7AkxBuTKy+Qnq9fKYKxbA5clrXRNVzJbTVnF5eFGsaUOlkes0ESG6lbqKy5ZcDgdl73zBIhAA== dependencies: "@firebase/component" "0.1.21" @@ -478,12 +478,12 @@ "@firebase/storage-types@0.3.13": version "0.3.13" - resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.13.tgz#cd43e939a2ab5742e109eb639a313673a48b5458" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.13.tgz" integrity sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog== "@firebase/storage@0.4.2": version "0.4.2" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.4.2.tgz#bc5924b87bd2fdd4ab0de49851c0125ebc236b89" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.4.2.tgz" integrity sha512-87CrvKrf8kijVekRBmUs8htsNz7N5X/pDhv3BvJBqw8K65GsUolpyjx0f4QJRkCRUYmh3MSkpa5P08lpVbC6nQ== dependencies: "@firebase/component" "0.1.21" @@ -493,19 +493,19 @@ "@firebase/util@0.3.4": version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.3.4.tgz#e389d0e0e2aac88a5235b06ba9431db999d4892b" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.3.4.tgz" integrity sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ== dependencies: tslib "^1.11.1" "@firebase/webchannel-wrapper@0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz#becce788818d3f47f0ac1a74c3c061ac1dcf4f6d" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz" integrity sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ== "@grpc/grpc-js@^1.0.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.0.tgz#4ff1ac4cdf7eb2af80d3c67316be9c2308d8d9bf" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.0.tgz" integrity sha512-09H50V7rmz0gFrGz6IbP49z9A8+2p4yZYcNDEb7bytr90vWn52VBQE1a+LMBlrucmNN0wSsiCr3TJx+dStHTng== dependencies: "@types/node" "^12.12.47" @@ -514,7 +514,7 @@ "@grpc/proto-loader@^0.5.0": version "0.5.5" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.5.tgz#6725e7a1827bdf8e92e29fbf4e9ef0203c0906a9" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.5.tgz" integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== dependencies: lodash.camelcase "^4.3.0" @@ -522,7 +522,7 @@ "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -533,12 +533,12 @@ "@istanbuljs/schema@^0.1.2": version "0.1.2" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== "@jest/console@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz" integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== dependencies: "@jest/types" "^26.6.2" @@ -550,7 +550,7 @@ "@jest/core@^26.6.3": version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz" integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== dependencies: "@jest/console" "^26.6.2" @@ -584,7 +584,7 @@ "@jest/environment@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz" integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== dependencies: "@jest/fake-timers" "^26.6.2" @@ -594,7 +594,7 @@ "@jest/fake-timers@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz" integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== dependencies: "@jest/types" "^26.6.2" @@ -606,7 +606,7 @@ "@jest/globals@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz" integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== dependencies: "@jest/environment" "^26.6.2" @@ -615,7 +615,7 @@ "@jest/reporters@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz" integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -647,7 +647,7 @@ "@jest/source-map@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz" integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== dependencies: callsites "^3.0.0" @@ -656,7 +656,7 @@ "@jest/test-result@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz" integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== dependencies: "@jest/console" "^26.6.2" @@ -666,7 +666,7 @@ "@jest/test-sequencer@^26.6.3": version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz" integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== dependencies: "@jest/test-result" "^26.6.2" @@ -677,7 +677,7 @@ "@jest/transform@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz" integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== dependencies: "@babel/core" "^7.1.0" @@ -698,7 +698,7 @@ "@jest/types@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -709,27 +709,27 @@ "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= "@protobufjs/base64@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz" integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= "@protobufjs/fetch@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz" integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= dependencies: "@protobufjs/aspromise" "^1.1.1" @@ -737,51 +737,51 @@ "@protobufjs/float@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz" integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= "@protobufjs/inquire@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz" integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= "@protobufjs/path@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz" integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= "@protobufjs/pool@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz" integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= "@protobufjs/utf8@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= "@sinonjs/commons@^1.7.0": version "1.8.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz" integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^6.0.1": version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz" integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== dependencies: "@sinonjs/commons" "^1.7.0" "@tootallnate/once@1": version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz" integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ== dependencies: "@babel/parser" "^7.1.0" @@ -792,14 +792,14 @@ "@types/babel__generator@*": version "7.6.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz" integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz#0c888dd70b3ee9eebb6e4f200e809da0076262be" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz" integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== dependencies: "@babel/parser" "^7.1.0" @@ -807,14 +807,14 @@ "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz" integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== dependencies: "@babel/types" "^7.3.0" "@types/body-parser@*": version "1.19.0" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz" integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== dependencies: "@types/connect" "*" @@ -822,14 +822,14 @@ "@types/connect@*": version "3.4.34" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz" integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== dependencies: "@types/node" "*" "@types/express-jwt@0.0.42": version "0.0.42" - resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz#4f04e1fadf9d18725950dc041808a4a4adf7f5ae" + resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz" integrity sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag== dependencies: "@types/express" "*" @@ -837,7 +837,7 @@ "@types/express-serve-static-core@*": version "4.17.15" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.15.tgz#7c3d37829a991da9a507c1efd44d97532e8909e3" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.15.tgz" integrity sha512-pb71P0BrBAx7cQE+/7QnA1HTQUkdBKMlkPY7lHUMn0YvPJkL2UA+KW3BdWQ309IT+i9En/qm45ZxpjIcpgEhNQ== dependencies: "@types/node" "*" @@ -846,14 +846,14 @@ "@types/express-unless@*": version "0.5.1" - resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.1.tgz#4f440b905e42bbf53382b8207bc337dc5ff9fd1f" + resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.1.tgz" integrity sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw== dependencies: "@types/express" "*" "@types/express@*": version "4.17.9" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.9.tgz#f5f2df6add703ff28428add52bdec8a1091b0a78" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.9.tgz" integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== dependencies: "@types/body-parser" "*" @@ -863,33 +863,33 @@ "@types/graceful-fs@^4.1.2": version "4.1.4" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.4.tgz#4ff9f641a7c6d1a3508ff88bc3141b152772e753" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.4.tgz" integrity sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg== dependencies: "@types/node" "*" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz" integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== "@types/istanbul-lib-report@*": version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz" integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@26.x", "@types/jest@^26.0.15": version "26.0.15" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz" integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== dependencies: jest-diff "^26.0.0" @@ -897,64 +897,64 @@ "@types/jsonwebtoken@^8.3.2": version "8.5.0" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz" integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== dependencies: "@types/node" "*" "@types/long@^4.0.1": version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== "@types/mime@*": version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz" integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== "@types/node@*": version "14.14.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz" integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== "@types/node@^12.12.47": version "12.19.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz" integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== "@types/node@^12.12.67": version "12.12.67" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz" integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== "@types/node@^13.7.0": version "13.13.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.30.tgz#1ed6e01e4ca576d5aec9cc802cc3bcf94c274192" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.30.tgz" integrity sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA== "@types/normalize-package-data@^2.4.0": version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== "@types/prettier@^2.0.0": version "2.1.5" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz" integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== "@types/qs@*": version "6.9.5" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz" integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== "@types/range-parser@*": version "1.2.3" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== "@types/serve-static@*": version "1.13.8" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz" integrity sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA== dependencies: "@types/mime" "*" @@ -962,41 +962,41 @@ "@types/stack-utils@^2.0.0": version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== "@types/yargs-parser@*": version "15.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz" integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== "@types/yargs@^15.0.0": version "15.0.9" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz" integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g== dependencies: "@types/yargs-parser" "*" abab@^2.0.3: version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== abort-controller@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz" integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== dependencies: event-target-shim "^5.0.0" abstract-logging@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.0.tgz#08a85814946c98ef06f4256ad470aba1886d4490" + resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.0.tgz" integrity sha512-/oA9z7JszpIioo6J6dB79LVUgJ3eD3cxkAmdCkvWWS+Y9tPtALs1rLqOekLUXUbYqM2fB9TTK0ibAyZJJOP/CA== acorn-globals@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz" integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== dependencies: acorn "^7.1.1" @@ -1004,24 +1004,24 @@ acorn-globals@^6.0.0: acorn-walk@^7.1.1: version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== acorn@^7.1.1: version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" ajv@^6.11.0, ajv@^6.12.2: version "6.12.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz" integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== dependencies: fast-deep-equal "^3.1.1" @@ -1031,7 +1031,7 @@ ajv@^6.11.0, ajv@^6.12.2: ajv@^6.12.3: version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -1041,33 +1041,33 @@ ajv@^6.12.3: ansi-escapes@^4.2.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz" integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== dependencies: type-fest "^0.11.0" ansi-regex@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" anymatch@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz" integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== dependencies: micromatch "^3.1.4" @@ -1075,7 +1075,7 @@ anymatch@^2.0.0: anymatch@^3.0.3: version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz" integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== dependencies: normalize-path "^3.0.0" @@ -1083,76 +1083,76 @@ anymatch@^3.0.3: archy@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" arr-diff@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= arr-flatten@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz" integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== arr-union@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= array-unique@^0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= arrify@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== asn1@~0.2.3: version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= assign-symbols@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= atob@^2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== atomic-sleep@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== avvio@^7.1.2: version "7.2.0" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.0.tgz#b4bf4eaf4a0207a4e6a58a7859207250793cc81b" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.0.tgz" integrity sha512-KtC63UyZARidAoIV8wXutAZnDIbZcXBqLjTAhZOX+mdMZBQCh5il/15MvCvma1178nhTwvN2D0TOAdiKG1MpUA== dependencies: archy "^1.0.0" @@ -1162,24 +1162,24 @@ avvio@^7.1.2: aws-sign2@~0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axios@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" babel-jest@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz" integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== dependencies: "@jest/transform" "^26.6.2" @@ -1193,7 +1193,7 @@ babel-jest@^26.6.3: babel-plugin-istanbul@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz" integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -1204,7 +1204,7 @@ babel-plugin-istanbul@^6.0.0: babel-plugin-jest-hoist@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz" integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== dependencies: "@babel/template" "^7.3.3" @@ -1214,7 +1214,7 @@ babel-plugin-jest-hoist@^26.6.2: babel-preset-current-node-syntax@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz" integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -1232,7 +1232,7 @@ babel-preset-current-node-syntax@^1.0.0: babel-preset-jest@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz" integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== dependencies: babel-plugin-jest-hoist "^26.6.2" @@ -1240,29 +1240,29 @@ babel-preset-jest@^26.6.2: balanced-match@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= barecolor@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/barecolor/-/barecolor-1.0.1.tgz#1bbbbfc41cedecf74e23fb7d9d054cd8763d92cc" + resolved "https://registry.yarnpkg.com/barecolor/-/barecolor-1.0.1.tgz" integrity sha512-ncJ680U+r1CGBt73L3O6V9GIAPy3hbDmWODEQajwEnDmmzeStvc4UYhapUSxUpS76+MHxyRihzZfwhyl122Zdw== baretest@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/baretest/-/baretest-2.0.0.tgz#5791667b47329c452e1f9bade879ab8dc0b304ba" + resolved "https://registry.yarnpkg.com/baretest/-/baretest-2.0.0.tgz" integrity sha512-hRmYnBojeijT3jH0GtqLoHus+adPoeYh2NmcNT3wBPH903AUphcFqs1gJ64fBovDXql51Df24g9D9jcXRZd4vA== dependencies: barecolor "1.0.1" base64-js@^1.3.0: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== base@^0.11.1: version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz" integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== dependencies: cache-base "^1.0.1" @@ -1275,19 +1275,19 @@ base@^0.11.1: bcrypt-pbkdf@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" bignumber.js@^9.0.0: version "9.0.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== body-parser@^1.19.0: version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== dependencies: bytes "3.1.0" @@ -1303,7 +1303,7 @@ body-parser@^1.19.0: brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -1311,7 +1311,7 @@ brace-expansion@^1.1.7: braces@^2.3.1: version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz" integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== dependencies: arr-flatten "^1.1.0" @@ -1327,48 +1327,48 @@ braces@^2.3.1: braces@^3.0.1: version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" browser-process-hrtime@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== bs-logger@0.x: version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz" integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== dependencies: fast-json-stable-stringify "2.x" bser@2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" buffer-equal-constant-time@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== bytes@3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== cache-base@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz" integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== dependencies: collection-visit "^1.0.0" @@ -1383,34 +1383,34 @@ cache-base@^1.0.1: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.0.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== capture-exit@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz" integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== dependencies: rsvp "^4.8.4" caseless@~0.12.0: version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= chalk@^2.0.0: version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -1419,7 +1419,7 @@ chalk@^2.0.0: chalk@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== dependencies: ansi-styles "^4.1.0" @@ -1427,22 +1427,22 @@ chalk@^4.0.0: char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== ci-info@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== cjs-module-lexer@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz" integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== class-utils@^0.3.5: version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz" integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== dependencies: arr-union "^3.1.0" @@ -1452,7 +1452,7 @@ class-utils@^0.3.5: cliui@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz" integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== dependencies: string-width "^4.2.0" @@ -1461,17 +1461,17 @@ cliui@^6.0.0: co@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= collect-v8-coverage@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== collection-visit@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz" integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= dependencies: map-visit "^1.0.0" @@ -1479,80 +1479,80 @@ collection-visit@^1.0.0: color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" component-emitter@^1.2.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= content-type@~1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" cookie@^0.4.0: version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== copy-descriptor@^0.1.0: version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js@3.6.5: version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz" integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== core-util-is@1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= cross-spawn@^6.0.0: version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== dependencies: nice-try "^1.0.4" @@ -1563,7 +1563,7 @@ cross-spawn@^6.0.0: cross-spawn@^7.0.0: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" @@ -1572,31 +1572,31 @@ cross-spawn@^7.0.0: cssom@^0.4.4: version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== cssom@~0.3.6: version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== cssstyle@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== dependencies: cssom "~0.3.6" dashdash@^1.12.0: version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz" integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= dependencies: assert-plus "^1.0.0" data-urls@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz" integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== dependencies: abab "^2.0.3" @@ -1605,67 +1605,60 @@ data-urls@^2.0.0: debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1: version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz" integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== dependencies: ms "2.1.2" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - decamelize@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= decimal.js@^10.2.0: version "10.2.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz" integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== decode-uri-component@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= deep-is@~0.1.3: version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= deepmerge@^4.2.2: version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== define-property@^0.2.5: version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz" integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= dependencies: is-descriptor "^0.1.0" define-property@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz" integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= dependencies: is-descriptor "^1.0.0" define-property@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz" integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== dependencies: is-descriptor "^1.0.2" @@ -1673,39 +1666,39 @@ define-property@^2.0.2: delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= depd@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= detect-newline@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== diff-sequences@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== dom-storage@2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39" + resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz" integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q== domexception@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz" integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== dependencies: webidl-conversions "^5.0.0" ecc-jsbn@~0.1.1: version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" @@ -1713,53 +1706,53 @@ ecc-jsbn@~0.1.1: ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" ee-first@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= emittery@^0.7.1: version "0.7.2" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz" integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== end-of-stream@^1.1.0: version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escodegen@^1.14.1: version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== dependencies: esprima "^4.0.1" @@ -1771,32 +1764,32 @@ escodegen@^1.14.1: esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== estraverse@^4.2.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== event-target-shim@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== exec-sh@^0.3.2: version "0.3.4" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== execa@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz" integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: cross-spawn "^6.0.0" @@ -1809,7 +1802,7 @@ execa@^1.0.0: execa@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== dependencies: cross-spawn "^7.0.0" @@ -1824,12 +1817,12 @@ execa@^4.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= expand-brackets@^2.1.4: version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz" integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= dependencies: debug "^2.3.3" @@ -1842,7 +1835,7 @@ expand-brackets@^2.1.4: expect@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz" integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== dependencies: "@jest/types" "^26.6.2" @@ -1854,14 +1847,14 @@ expect@^26.6.2: extend-shallow@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz" integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= dependencies: is-extendable "^0.1.0" extend-shallow@^3.0.0, extend-shallow@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz" integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= dependencies: assign-symbols "^1.0.0" @@ -1869,12 +1862,12 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: extend@^3.0.2, extend@~3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== extglob@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz" integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== dependencies: array-unique "^0.3.2" @@ -1888,32 +1881,32 @@ extglob@^2.0.4: extsprintf@1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= fast-decode-uri-component@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== fast-deep-equal@^3.1.1: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-json-stringify@^2.2.1: version "2.2.8" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.2.8.tgz#6c6ff59bda09412c1ee462323eda2debe4446884" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.2.8.tgz" integrity sha512-/ASLcFlk998wuzAqbbspt7038fHXM1mTYk5N2Il+tOSnfVuHBwkH5tRnTMaB45Adv+SeS4McFXvAFI1+7uEkXw== dependencies: ajv "^6.11.0" @@ -1922,46 +1915,54 @@ fast-json-stringify@^2.2.1: fast-levenshtein@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fast-redact@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz" integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w== fast-safe-stringify@^2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== fast-text-encoding@^1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== fastfall@^1.5.0: version "1.5.1" - resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94" + resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz" integrity sha1-P+4DMxpJ0dObPN96XpzWb0dee5Q= dependencies: reusify "^1.0.0" fastify-authz-jwks@^1.1.11: version "1.1.11" - resolved "https://registry.yarnpkg.com/fastify-authz-jwks/-/fastify-authz-jwks-1.1.11.tgz#1b0b8d18962e4f1a4e6033a687eaf5325a6b8351" + resolved "https://registry.yarnpkg.com/fastify-authz-jwks/-/fastify-authz-jwks-1.1.11.tgz" integrity sha512-8puiTBrphRrQy0fpJQLpwYKIyyeAWDkdp9/EDGVWbGOypvA/zc8RXaTCtDQxQ9JOxx0X0/g8IEYQv/37yJtCPQ== dependencies: jwks-rsa "^1.6.0" +fastify-cors@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/fastify-cors/-/fastify-cors-5.1.0.tgz" + integrity sha512-PTq4wSBFljXmbFKmp5eKW9gg0m488nXBl+OSTb/vnWXXLvVn7HFk7WKgt7uQVfd9UuksveCGTXipVlus3qhmrw== + dependencies: + fastify-plugin "^3.0.0" + vary "^1.1.2" + fastify-error@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz#9a1c28d4f42b6259e7a549671c8e5e2d85660634" + resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz" integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== fastify-jwt@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.1.3.tgz#d016a2f6a810299edc72ad79ce2949fb88a20604" + resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.1.3.tgz" integrity sha512-8732zt+7UA9JzeRebJFCH+56laMCAxq/Wyou6pzXZzEWcPGPLcRqCk+R0CcgwjjVToo6wLIeNNKHFegyemyHug== dependencies: "@types/jsonwebtoken" "^8.3.2" @@ -1972,19 +1973,24 @@ fastify-jwt@^2.1.3: fastify-plugin@^2.0.0: version "2.3.4" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz#b17abdc36a97877d88101fb86ad8a07f2c07de87" + resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz" integrity sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ== dependencies: semver "^7.3.2" +fastify-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz" + integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== + fastify-warning@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" + resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz" integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw== fastify@^3.5.1: version "3.5.1" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.5.1.tgz#753a6909e3154d61fcde7402500ed92177132c1a" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.5.1.tgz" integrity sha512-SO/ZZSbbAUrRxxz9zIsf1Ek5qqRux5EN03UF1lJGt/xwFzVpmW4h+p8kRCD08VCL1sItFy2dhFlR8FPaQfRGBw== dependencies: abstract-logging "^2.0.0" @@ -2006,7 +2012,7 @@ fastify@^3.5.1: fastparallel@^2.2.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688" + resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz" integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ== dependencies: reusify "^1.0.4" @@ -2014,21 +2020,21 @@ fastparallel@^2.2.0: fastq@^1.3.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz" integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== dependencies: reusify "^1.0.4" fastq@^1.6.1: version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz" integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== dependencies: reusify "^1.0.4" fastseries@^1.7.0: version "1.7.2" - resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b" + resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz" integrity sha1-0izhO5Qz3/M4jZHb1ri9qbIaD0s= dependencies: reusify "^1.0.0" @@ -2036,21 +2042,21 @@ fastseries@^1.7.0: faye-websocket@0.11.3: version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz" integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== dependencies: websocket-driver ">=0.5.1" fb-watchman@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz" integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== dependencies: bser "2.1.1" fill-range@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz" integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= dependencies: extend-shallow "^2.0.1" @@ -2060,14 +2066,14 @@ fill-range@^4.0.0: fill-range@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz" integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== dependencies: to-regex-range "^5.0.1" find-my-way@^3.0.0: version "3.0.4" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-3.0.4.tgz#a485973d1a3fdafd989ac9f12fd2d88e83cda268" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-3.0.4.tgz" integrity sha512-Trl/mNAVvTgCpo9ox6yixkwiZUvecKYUQZoAuMCBACsgGqv+FbWe+jE5sBA5+U8LIWrJk/cw8zPV53GPrjTnsw== dependencies: fast-decode-uri-component "^1.0.1" @@ -2076,7 +2082,7 @@ find-my-way@^3.0.0: find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -2084,7 +2090,7 @@ find-up@^4.0.0, find-up@^4.1.0: firebase@^8.0.2: version "8.0.2" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.0.2.tgz#7bc8a220ce6b206121c4a9740eca1ad4be23ff86" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.0.2.tgz" integrity sha512-tPtXQ8sifo82f7bOxYcR/yEdJ4IbL4/fpQrophRHFAaYCsYGp2Q/c1zz0voX9cLap8MH2uwh5LIVBqZ8nyT5ZQ== dependencies: "@firebase/analytics" "0.6.2" @@ -2104,29 +2110,27 @@ firebase@^8.0.2: flatstr@^1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" + resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz" integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" +follow-redirects@^1.10.0: + version "1.13.1" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz" + integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== for-in@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= forever-agent@~0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= form-data@~2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz" integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: asynckit "^0.4.0" @@ -2135,34 +2139,34 @@ form-data@~2.3.2: forwarded@~0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= fragment-cache@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz" integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= dependencies: map-cache "^0.2.2" fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.2.1.tgz#1fb02ded2036a8ac288d507a65962bd87b97628d" - integrity sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" + integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== gaxios@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.0.1.tgz#bc7b205a89d883452822cc75e138620c35e3291e" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.0.1.tgz" integrity sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ== dependencies: abort-controller "^3.0.0" @@ -2173,7 +2177,7 @@ gaxios@^4.0.0: gcp-metadata@^4.2.0: version "4.2.1" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.2.1.tgz" integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw== dependencies: gaxios "^4.0.0" @@ -2181,48 +2185,48 @@ gcp-metadata@^4.2.0: gensync@^1.0.0-beta.1: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.1: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-stream@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" get-stream@^5.0.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: pump "^3.0.0" get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= getpass@^0.1.1: version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz" integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= dependencies: assert-plus "^1.0.0" glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" @@ -2234,12 +2238,12 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: globals@^11.1.0: version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== google-auth-library@^6.1.1: version "6.1.3" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.3.tgz#39d868140b70d0c4b32c6f6d8f4ccc1400d84dca" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.3.tgz" integrity sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g== dependencies: arrify "^2.0.0" @@ -2254,24 +2258,24 @@ google-auth-library@^6.1.1: google-p12-pem@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz" integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== dependencies: node-forge "^0.10.0" graceful-fs@^4.2.4: version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== growly@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= gtoken@^5.0.4: version "5.0.5" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.0.5.tgz#e752d18538576777dfe237887e30fc0627870eae" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.0.5.tgz" integrity sha512-wvjkecutFh8kVfbcdBdUWqDRrXb+WrgD79DBDEYf1Om8S1FluhylhtFjrL7Tx69vNhh259qA3Q1P4sPtb+kUYw== dependencies: gaxios "^4.0.0" @@ -2281,12 +2285,12 @@ gtoken@^5.0.4: har-schema@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.3: version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz" integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== dependencies: ajv "^6.12.3" @@ -2294,17 +2298,17 @@ har-validator@~5.1.3: has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-value@^0.3.1: version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz" integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= dependencies: get-value "^2.0.3" @@ -2313,7 +2317,7 @@ has-value@^0.3.1: has-value@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz" integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= dependencies: get-value "^2.0.6" @@ -2322,12 +2326,12 @@ has-value@^1.0.0: has-values@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz" integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= has-values@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz" integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= dependencies: is-number "^3.0.0" @@ -2335,31 +2339,31 @@ has-values@^1.0.0: has@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" hosted-git-info@^2.1.4: version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== html-encoding-sniffer@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz" integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== dependencies: whatwg-encoding "^1.0.5" html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== http-errors@1.7.2: version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz" integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== dependencies: depd "~1.1.2" @@ -2370,7 +2374,7 @@ http-errors@1.7.2: http-errors@^1.7.1: version "1.8.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== dependencies: depd "~1.1.2" @@ -2381,12 +2385,12 @@ http-errors@^1.7.1: http-parser-js@>=0.5.1: version "0.5.2" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz" integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== http-proxy-agent@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== dependencies: "@tootallnate/once" "1" @@ -2395,7 +2399,7 @@ http-proxy-agent@^4.0.1: http-signature@~1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz" integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= dependencies: assert-plus "^1.0.0" @@ -2404,7 +2408,7 @@ http-signature@~1.2.0: https-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: agent-base "6" @@ -2412,24 +2416,24 @@ https-proxy-agent@^5.0.0: human-signals@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" idb@3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384" + resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz" integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw== import-local@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz" integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== dependencies: pkg-dir "^4.2.0" @@ -2437,12 +2441,12 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" @@ -2450,79 +2454,79 @@ inflight@^1.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ip-regex@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-accessor-descriptor@^0.1.6: version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz" integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= dependencies: kind-of "^3.0.2" is-accessor-descriptor@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz" integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== dependencies: kind-of "^6.0.0" is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= is-buffer@^1.1.5: version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-ci@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz" integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== dependencies: ci-info "^2.0.0" is-core-module@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz" integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== dependencies: has "^1.0.3" is-data-descriptor@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz" integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= dependencies: kind-of "^3.0.2" is-data-descriptor@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz" integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== dependencies: kind-of "^6.0.0" is-descriptor@^0.1.0: version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz" integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== dependencies: is-accessor-descriptor "^0.1.6" @@ -2531,7 +2535,7 @@ is-descriptor@^0.1.0: is-descriptor@^1.0.0, is-descriptor@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz" integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== dependencies: is-accessor-descriptor "^1.0.0" @@ -2540,117 +2544,117 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-docker@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz" integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz" integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= is-extendable@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz" integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== dependencies: is-plain-object "^2.0.4" is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-fn@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-number@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz" integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= dependencies: kind-of "^3.0.2" is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: isobject "^3.0.1" is-potential-custom-element-name@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= is-stream@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= is-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= is-windows@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" isarray@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isobject@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz" integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= dependencies: isarray "1.0.0" isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= isstream@~0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= istanbul-lib-coverage@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz" integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz" integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== dependencies: "@babel/core" "^7.7.5" @@ -2660,7 +2664,7 @@ istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: istanbul-lib-report@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -2669,7 +2673,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz" integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== dependencies: debug "^4.1.1" @@ -2678,7 +2682,7 @@ istanbul-lib-source-maps@^4.0.0: istanbul-reports@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz" integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== dependencies: html-escaper "^2.0.0" @@ -2686,7 +2690,7 @@ istanbul-reports@^3.0.2: jest-changed-files@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz" integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== dependencies: "@jest/types" "^26.6.2" @@ -2695,7 +2699,7 @@ jest-changed-files@^26.6.2: jest-cli@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz" integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== dependencies: "@jest/core" "^26.6.3" @@ -2714,7 +2718,7 @@ jest-cli@^26.6.3: jest-config@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz" integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== dependencies: "@babel/core" "^7.1.0" @@ -2738,7 +2742,7 @@ jest-config@^26.6.3: jest-diff@^26.0.0, jest-diff@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz" integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== dependencies: chalk "^4.0.0" @@ -2748,14 +2752,14 @@ jest-diff@^26.0.0, jest-diff@^26.6.2: jest-docblock@^26.0.0: version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz" integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== dependencies: detect-newline "^3.0.0" jest-each@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz" integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== dependencies: "@jest/types" "^26.6.2" @@ -2766,7 +2770,7 @@ jest-each@^26.6.2: jest-environment-jsdom@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz" integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== dependencies: "@jest/environment" "^26.6.2" @@ -2779,7 +2783,7 @@ jest-environment-jsdom@^26.6.2: jest-environment-node@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz" integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== dependencies: "@jest/environment" "^26.6.2" @@ -2791,12 +2795,12 @@ jest-environment-node@^26.6.2: jest-get-type@^26.3.0: version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== jest-haste-map@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz" integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== dependencies: "@jest/types" "^26.6.2" @@ -2817,7 +2821,7 @@ jest-haste-map@^26.6.2: jest-jasmine2@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz" integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== dependencies: "@babel/traverse" "^7.1.0" @@ -2841,7 +2845,7 @@ jest-jasmine2@^26.6.3: jest-leak-detector@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz" integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== dependencies: jest-get-type "^26.3.0" @@ -2849,7 +2853,7 @@ jest-leak-detector@^26.6.2: jest-matcher-utils@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz" integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== dependencies: chalk "^4.0.0" @@ -2859,7 +2863,7 @@ jest-matcher-utils@^26.6.2: jest-message-util@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz" integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== dependencies: "@babel/code-frame" "^7.0.0" @@ -2874,7 +2878,7 @@ jest-message-util@^26.6.2: jest-mock@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz" integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== dependencies: "@jest/types" "^26.6.2" @@ -2882,17 +2886,17 @@ jest-mock@^26.6.2: jest-pnp-resolver@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== jest-regex-util@^26.0.0: version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== jest-resolve-dependencies@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz" integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== dependencies: "@jest/types" "^26.6.2" @@ -2901,7 +2905,7 @@ jest-resolve-dependencies@^26.6.3: jest-resolve@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz" integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== dependencies: "@jest/types" "^26.6.2" @@ -2915,7 +2919,7 @@ jest-resolve@^26.6.2: jest-runner@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz" integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== dependencies: "@jest/console" "^26.6.2" @@ -2941,7 +2945,7 @@ jest-runner@^26.6.3: jest-runtime@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz" integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== dependencies: "@jest/console" "^26.6.2" @@ -2974,7 +2978,7 @@ jest-runtime@^26.6.3: jest-serializer@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz" integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== dependencies: "@types/node" "*" @@ -2982,7 +2986,7 @@ jest-serializer@^26.6.2: jest-snapshot@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz" integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== dependencies: "@babel/types" "^7.0.0" @@ -3004,7 +3008,7 @@ jest-snapshot@^26.6.2: jest-util@^26.1.0, jest-util@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz" integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== dependencies: "@jest/types" "^26.6.2" @@ -3016,7 +3020,7 @@ jest-util@^26.1.0, jest-util@^26.6.2: jest-validate@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz" integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== dependencies: "@jest/types" "^26.6.2" @@ -3028,7 +3032,7 @@ jest-validate@^26.6.2: jest-watcher@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz" integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== dependencies: "@jest/test-result" "^26.6.2" @@ -3041,7 +3045,7 @@ jest-watcher@^26.6.2: jest-worker@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" @@ -3050,7 +3054,7 @@ jest-worker@^26.6.2: jest@^26.6.3: version "26.6.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz" integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== dependencies: "@jest/core" "^26.6.3" @@ -3059,12 +3063,12 @@ jest@^26.6.3: js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" @@ -3072,12 +3076,12 @@ js-yaml@^3.13.1: jsbn@~0.1.0: version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= jsdom@^16.4.0: version "16.4.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz" integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== dependencies: abab "^2.0.3" @@ -3109,46 +3113,46 @@ jsdom@^16.4.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-bigint@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz" integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== dependencies: bignumber.js "^9.0.0" json-parse-even-better-errors@^2.3.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema@0.2.3: version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= json-stringify-safe@~5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= json5@2.x, json5@^2.1.2: version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz" integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== dependencies: minimist "^1.2.5" jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== dependencies: jws "^3.2.2" @@ -3164,7 +3168,7 @@ jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: jsprim@^1.2.2: version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz" integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= dependencies: assert-plus "1.0.0" @@ -3174,7 +3178,7 @@ jsprim@^1.2.2: jwa@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz" integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== dependencies: buffer-equal-constant-time "1.0.1" @@ -3183,7 +3187,7 @@ jwa@^1.4.1: jwa@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz" integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== dependencies: buffer-equal-constant-time "1.0.1" @@ -3191,12 +3195,12 @@ jwa@^2.0.0: safe-buffer "^5.0.1" jwks-rsa@^1.12.0, jwks-rsa@^1.6.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.12.0.tgz#6bfa20c6673abf15c2ff0806edc0a15b3f2f5969" - integrity sha512-6zKwlhnGQo+KivArZQThGoxrliRZgyC8s6yXDFSNRiMEVm+4jv07nBkVPFoaN/rEX0b47GKm5MSyBmuzGjj6wg== + version "1.12.2" + resolved "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.12.2.tgz" + integrity sha512-6gPo/mQUxXJt75oPtjhM3Jm3FSXnmwg73QDA8dpgP7YmIKlIY+2StngFxt4w4Y1podtSbtV3jttNOdctuxAX1Q== dependencies: "@types/express-jwt" "0.0.42" - axios "^0.19.2" + axios "^0.21.1" debug "^4.1.0" http-proxy-agent "^4.0.1" https-proxy-agent "^5.0.0" @@ -3208,7 +3212,7 @@ jwks-rsa@^1.12.0, jwks-rsa@^1.6.0: jws@^3.2.2: version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: jwa "^1.4.1" @@ -3216,7 +3220,7 @@ jws@^3.2.2: jws@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz" integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== dependencies: jwa "^2.0.0" @@ -3224,41 +3228,41 @@ jws@^4.0.0: kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz" integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= dependencies: is-buffer "^1.1.5" kind-of@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz" integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= dependencies: is-buffer "^1.1.5" kind-of@^5.0.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== kleur@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== leven@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== levn@~0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz" integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= dependencies: prelude-ls "~1.1.2" @@ -3266,7 +3270,7 @@ levn@~0.3.0: light-my-request@^4.0.2: version "4.1.1" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.1.1.tgz#71a4f5742affd21d54e70895b6c6cec08feb9bad" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.1.1.tgz" integrity sha512-4H0T0PQcFB/fGTIkNV5ShuftWnuUKdtLWq5t2zt+lwMWRZkVviTfmJqGOXeAAqkdREnGJQXa8zJ4wXJ0LrzrTA== dependencies: ajv "^6.12.2" @@ -3277,96 +3281,96 @@ light-my-request@^4.0.2: limiter@^1.1.5: version "1.1.5" - resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz" integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== lines-and-columns@^1.1.6: version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" lodash.camelcase@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= lodash.clonedeep@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= lodash.includes@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= lodash.isboolean@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= lodash.isinteger@^4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= lodash.isnumber@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= lodash.isplainobject@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= lodash.isstring@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= lodash.memoize@4.x: version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= lodash.once@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= lodash.sortby@^4.7.0: version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash@^4.17.19: version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== long@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" lru-cache@~4.0.0: version "4.0.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz" integrity sha1-HRdnnAac2l0ECZGgnbwsDbN35V4= dependencies: pseudomap "^1.0.1" @@ -3374,7 +3378,7 @@ lru-cache@~4.0.0: lru-memoizer@^2.1.2: version "2.1.3" - resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.1.3.tgz#a16a14248e930658dafbd0998af82a747a230235" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.1.3.tgz" integrity sha512-DcAptVUrKHbyKfSpvthwHwD42bFBLSAhTXJf5PQunu4F0/Hzy41WTamvavUWqsOPps26D0l5534aFvcwEcYzDw== dependencies: lodash.clonedeep "^4.5.0" @@ -3382,48 +3386,48 @@ lru-memoizer@^2.1.2: make-dir@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" make-error@1.x: version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== makeerror@1.0.x: version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz" integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= dependencies: tmpl "1.0.x" map-cache@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= map-visit@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz" integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= dependencies: object-visit "^1.0.0" media-typer@0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== micromatch@^3.1.4: version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== dependencies: arr-diff "^4.0.0" @@ -3442,7 +3446,7 @@ micromatch@^3.1.4: micromatch@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz" integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== dependencies: braces "^3.0.1" @@ -3450,41 +3454,41 @@ micromatch@^4.0.2: mime-db@1.44.0: version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: mime-db "1.44.0" mime@^2.2.0: version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== mixin-deep@^1.2.0: version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz" integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" @@ -3492,27 +3496,27 @@ mixin-deep@^1.2.0: mkdirp@1.x: version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== ms@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= ms@2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== ms@^2.1.1, ms@^2.1.2: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== nanomatch@^1.2.9: version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz" integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== dependencies: arr-diff "^4.0.0" @@ -3529,37 +3533,37 @@ nanomatch@^1.2.9: natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= nice-try@^1.0.4: version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== node-fetch@2.6.1, node-fetch@^2.3.0: version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== node-forge@^0.10.0: version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= node-modules-regexp@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-notifier@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.0.tgz" integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA== dependencies: growly "^1.3.0" @@ -3571,7 +3575,7 @@ node-notifier@^8.0.0: normalize-package-data@^2.5.0: version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: hosted-git-info "^2.1.4" @@ -3581,43 +3585,43 @@ normalize-package-data@^2.5.0: normalize-path@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz" integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= dependencies: remove-trailing-separator "^1.0.1" normalize-path@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== npm-run-path@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz" integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= dependencies: path-key "^2.0.0" npm-run-path@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" nwsapi@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== oauth-sign@~0.9.0: version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== object-copy@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz" integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= dependencies: copy-descriptor "^0.1.0" @@ -3626,42 +3630,42 @@ object-copy@^0.1.0: object-visit@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz" integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= dependencies: isobject "^3.0.0" object.pick@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz" integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= dependencies: isobject "^3.0.1" on-finished@~2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= dependencies: ee-first "1.1.1" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" onetime@^5.1.0: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" optionator@^0.8.1: version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" @@ -3673,36 +3677,36 @@ optionator@^0.8.1: p-each-series@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz" integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== p-finally@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== parse-json@^5.0.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz" integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== dependencies: "@babel/code-frame" "^7.0.0" @@ -3712,57 +3716,57 @@ parse-json@^5.0.0: parse5@5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== pascalcase@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== performance-now@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= picomatch@^2.0.4, picomatch@^2.0.5: version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== pino-std-serializers@^2.4.2: version "2.5.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz#40ead781c65a0ce7ecd9c1c33f409d31fe712315" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz" integrity sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg== pino@^6.2.1: version "6.7.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-6.7.0.tgz#d5d96b7004fed78816b5694fda3eab02b5ca6d23" + resolved "https://registry.yarnpkg.com/pino/-/pino-6.7.0.tgz" integrity sha512-vPXJ4P9rWCwzlTJt+f0Ni4THc3DWyt8iDDCO4edQ8narTu6hnpzdXu8FqeSJCGndl1W6lfbYQUQihUO54y66Lw== dependencies: fast-redact "^3.0.0" @@ -3774,31 +3778,31 @@ pino@^6.2.1: pirates@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz" integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== dependencies: node-modules-regexp "^1.0.0" pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" posix-character-classes@^0.1.0: version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= prelude-ls@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= pretty-format@^26.0.0, pretty-format@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== dependencies: "@jest/types" "^26.6.2" @@ -3808,12 +3812,12 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: promise-polyfill@8.1.3: version "8.1.3" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz" integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== prompts@^2.0.1: version "2.4.0" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz" integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== dependencies: kleur "^3.0.3" @@ -3821,7 +3825,7 @@ prompts@^2.0.1: protobufjs@^6.8.6: version "6.10.1" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz" integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== dependencies: "@protobufjs/aspromise" "^1.1.2" @@ -3840,7 +3844,7 @@ protobufjs@^6.8.6: proxy-addr@^2.0.5: version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz" integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== dependencies: forwarded "~0.1.2" @@ -3848,22 +3852,22 @@ proxy-addr@^2.0.5: proxy-from-env@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== pseudomap@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= psl@^1.1.28: version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== pump@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz" integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== dependencies: end-of-stream "^1.1.0" @@ -3871,32 +3875,32 @@ pump@^3.0.0: punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== qs@6.7.0: version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== qs@~6.5.2: version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== queue-microtask@^1.1.2: version "1.1.4" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.4.tgz#40841ace4356b48b35b5ea61a2e1fe0a23c59ce1" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.4.tgz" integrity sha512-eY/4Obve9cE5FK8YvC1cJsm5cr7XvAurul8UtBDJ2PR1p5NmAwHtvAt5ftcLtwYRCUKNhxCneZZlxmUDFoSeKA== quick-format-unescaped@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz" integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== raw-body@2.4.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz" integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== dependencies: bytes "3.1.0" @@ -3906,12 +3910,12 @@ raw-body@2.4.0: react-is@^17.0.1: version "17.0.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== read-pkg-up@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz" integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== dependencies: find-up "^4.1.0" @@ -3920,7 +3924,7 @@ read-pkg-up@^7.0.1: read-pkg@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz" integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== dependencies: "@types/normalize-package-data" "^2.4.0" @@ -3930,7 +3934,7 @@ read-pkg@^5.2.0: readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" @@ -3939,7 +3943,7 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz" integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== dependencies: extend-shallow "^3.0.2" @@ -3947,29 +3951,29 @@ regex-not@^1.0.0, regex-not@^1.0.2: remove-trailing-separator@^1.0.1: version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= repeat-element@^1.1.2: version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== repeat-string@^1.6.1: version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= request-promise-core@1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz" integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== dependencies: lodash "^4.17.19" request-promise-native@^1.0.8: version "1.0.9" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz" integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== dependencies: request-promise-core "1.1.4" @@ -3978,7 +3982,7 @@ request-promise-native@^1.0.8: request@^2.88.2: version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" @@ -4004,34 +4008,34 @@ request@^2.88.2: require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= require-main-filename@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== resolve-cwd@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz" integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== dependencies: resolve-from "^5.0.0" resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-url@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= resolve@^1.10.0, resolve@^1.18.1, resolve@^1.3.2: version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== dependencies: is-core-module "^2.1.0" @@ -4039,68 +4043,68 @@ resolve@^1.10.0, resolve@^1.18.1, resolve@^1.3.2: ret@~0.1.10: version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== ret@~0.2.0: version "0.2.2" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz" integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== reusify@^1.0.0, reusify@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz" integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== rimraf@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" rsvp@^4.8.4: version "4.8.5" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-regex2@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" + resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz" integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ== dependencies: ret "~0.2.0" safe-regex@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz" integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= dependencies: ret "~0.1.10" "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sane@^4.0.3: version "4.1.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz" integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== dependencies: "@cnakazawa/watch" "^1.0.3" @@ -4115,49 +4119,49 @@ sane@^4.0.3: saxes@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz" integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== dependencies: xmlchars "^2.2.0" secure-json-parse@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz" integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA== semver-store@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" + resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz" integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== semver@7.x, semver@^7.3.2: version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== set-blocking@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= set-cookie-parser@^2.4.1: version "2.4.6" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz#43bdea028b9e6f176474ee5298e758b4a44799c3" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz" integrity sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg== set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz" integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== dependencies: extend-shallow "^2.0.1" @@ -4167,61 +4171,61 @@ set-value@^2.0.0, set-value@^2.0.1: setprototypeof@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shebang-command@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz" integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= dependencies: shebang-regex "^1.0.0" shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shellwords@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== snapdragon-node@^2.0.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz" integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== dependencies: define-property "^1.0.0" @@ -4230,14 +4234,14 @@ snapdragon-node@^2.0.1: snapdragon-util@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz" integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== dependencies: kind-of "^3.2.0" snapdragon@^0.8.1: version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz" integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== dependencies: base "^0.11.1" @@ -4251,7 +4255,7 @@ snapdragon@^0.8.1: sonic-boom@^1.0.2: version "1.3.0" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.3.0.tgz#5c77c846ce6c395dddf2eb8e8e65f9cc576f2e76" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.3.0.tgz" integrity sha512-4nX6OYvOYr6R76xfQKi6cZpTO3YSWe/vd+QdIfoH0lBy0MnPkeAbb2rRWgmgADkXUeCKPwO1FZAKlAVWAadELw== dependencies: atomic-sleep "^1.0.0" @@ -4259,7 +4263,7 @@ sonic-boom@^1.0.2: source-map-resolve@^0.5.0: version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz" integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== dependencies: atob "^2.1.2" @@ -4270,7 +4274,7 @@ source-map-resolve@^0.5.0: source-map-support@^0.5.6: version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" @@ -4278,27 +4282,27 @@ source-map-support@^0.5.6: source-map-url@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.7.3: version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== spdx-correct@^3.0.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz" integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" @@ -4306,12 +4310,12 @@ spdx-correct@^3.0.0: spdx-exceptions@^2.1.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" @@ -4319,24 +4323,24 @@ spdx-expression-parse@^3.0.0: spdx-license-ids@^3.0.0: version "3.0.6" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz" integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz" integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== dependencies: extend-shallow "^3.0.0" sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz" integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== dependencies: asn1 "~0.2.3" @@ -4351,14 +4355,14 @@ sshpk@^1.7.0: stack-utils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== dependencies: escape-string-regexp "^2.0.0" static-extend@^0.1.1: version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz" integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= dependencies: define-property "^0.2.5" @@ -4366,17 +4370,17 @@ static-extend@^0.1.1: "statuses@>= 1.5.0 < 2": version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= stealthy-require@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= steed@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5" + resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz" integrity sha1-8VJd1a2xLrIb90dJU3Zo1iW5q8U= dependencies: fastfall "^1.5.0" @@ -4387,7 +4391,7 @@ steed@^1.1.3: string-length@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz" integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== dependencies: char-regex "^1.0.2" @@ -4395,12 +4399,12 @@ string-length@^4.0.1: string-similarity@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.2.tgz#1dc29518d0e92e50509499ce2974368f77bbb1b1" + resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.2.tgz" integrity sha512-eCsPPyoQBgY4TMpVD6DVfO7pLrimUONriaO4Xjp3WPUW0YnNLqdHgRj23xotLlqrL90eJhBeq3zdAJf2mQgfBQ== string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== dependencies: emoji-regex "^8.0.0" @@ -4409,50 +4413,50 @@ string-width@^4.1.0, string-width@^4.2.0: string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" strip-ansi@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz" integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== dependencies: ansi-regex "^5.0.0" strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-eof@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz" integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== dependencies: has-flag "^4.0.0" @@ -4460,12 +4464,12 @@ supports-hyperlinks@^2.0.0: symbol-tree@^3.2.4: version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== terminal-link@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz" integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== dependencies: ansi-escapes "^4.2.1" @@ -4473,7 +4477,7 @@ terminal-link@^2.0.0: test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -4482,34 +4486,34 @@ test-exclude@^6.0.0: throat@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== tiny-lru@^7.0.0: version "7.0.6" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" + resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz" integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow== tmpl@1.0.x: version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= to-object-path@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz" integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= dependencies: kind-of "^3.0.2" to-regex-range@^2.1.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz" integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= dependencies: is-number "^3.0.0" @@ -4517,14 +4521,14 @@ to-regex-range@^2.1.0: to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz" integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== dependencies: define-property "^2.0.2" @@ -4534,12 +4538,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: toidentifier@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: psl "^1.1.28" @@ -4547,7 +4551,7 @@ tough-cookie@^2.3.3, tough-cookie@~2.5.0: tough-cookie@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz" integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== dependencies: ip-regex "^2.1.0" @@ -4556,14 +4560,14 @@ tough-cookie@^3.0.1: tr46@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz" integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== dependencies: punycode "^2.1.1" ts-jest@^26.4.4: version "26.4.4" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz" integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg== dependencies: "@types/jest" "26.x" @@ -4580,51 +4584,51 @@ ts-jest@^26.4.4: tslib@^1.11.1: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tunnel-agent@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz" integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= type-check@~0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz" integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= dependencies: prelude-ls "~1.1.2" type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.11.0: version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== type-fest@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== type-fest@^0.8.1: version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== type-is@~1.6.17: version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -4632,19 +4636,19 @@ type-is@~1.6.17: typedarray-to-buffer@^3.1.5: version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== dependencies: is-typedarray "^1.0.0" typescript@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz" integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== union-value@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz" integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== dependencies: arr-union "^3.1.0" @@ -4654,12 +4658,12 @@ union-value@^1.0.0: unpipe@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= unset-value@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz" integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= dependencies: has-value "^0.3.1" @@ -4667,39 +4671,39 @@ unset-value@^1.0.0: uri-js@^4.2.2: version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz" integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" urix@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= use@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== util-deprecate@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= uuid@^3.3.2: version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^8.3.0: version "8.3.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== v8-to-istanbul@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz" integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" @@ -4708,15 +4712,20 @@ v8-to-istanbul@^7.0.0: validate-npm-package-license@^3.0.1: version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + verror@1.10.0: version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz" integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: assert-plus "^1.0.0" @@ -4725,38 +4734,38 @@ verror@1.10.0: w3c-hr-time@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz" integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== dependencies: browser-process-hrtime "^1.0.0" w3c-xmlserializer@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz" integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== dependencies: xml-name-validator "^3.0.0" walker@^1.0.7, walker@~1.0.5: version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz" integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= dependencies: makeerror "1.0.x" webidl-conversions@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz" integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== webidl-conversions@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== websocket-driver@>=0.5.1: version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: http-parser-js ">=0.5.1" @@ -4765,29 +4774,29 @@ websocket-driver@>=0.5.1: websocket-extensions@>=0.1.1: version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== dependencies: iconv-lite "0.4.24" whatwg-fetch@2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== whatwg-mimetype@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^8.0.0: version "8.4.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.4.0.tgz" integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== dependencies: lodash.sortby "^4.7.0" @@ -4796,31 +4805,31 @@ whatwg-url@^8.0.0: which-module@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= which@^1.2.9: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" which@^2.0.1, which@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" word-wrap@~1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -4829,12 +4838,12 @@ wrap-ansi@^6.2.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^3.0.0: version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== dependencies: imurmurhash "^0.1.4" @@ -4844,52 +4853,52 @@ write-file-atomic@^3.0.0: ws@^7.2.3: version "7.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz" integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== xml-name-validator@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== xmlchars@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== xmlhttprequest@1.8.0: version "1.8.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" + resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz" integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= xtend@^4.0.0, xtend@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== yallist@^2.0.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yargs-parser@20.x: version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== yargs-parser@^18.1.2: version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" @@ -4897,7 +4906,7 @@ yargs-parser@^18.1.2: yargs@^15.4.1: version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" From 8a69d289373df5c4a1b8267f88d957a104e32898 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 16 Jan 2021 20:55:45 -0330 Subject: [PATCH 28/75] Fixing build & test errors --- src/declarations/fastify-authz-jwks.d.ts | 2 +- tests/utils/dummyData.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/declarations/fastify-authz-jwks.d.ts b/src/declarations/fastify-authz-jwks.d.ts index 3d49243..1145745 100644 --- a/src/declarations/fastify-authz-jwks.d.ts +++ b/src/declarations/fastify-authz-jwks.d.ts @@ -4,4 +4,4 @@ declare module "fastify-authz-jwks" { export default function fastifyJwtSecret( options: {} ): (request : FastifyRequest, token : FastifyReply, cb: SecretCB) => void; -}; +} diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index 0698891..1432d82 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -20,11 +20,11 @@ export const DummyBiz: Business = { industry: "DummyIndustry" }; -export async function createDummyBusiness(bizApp: FastifyInstance) { +export async function createDummyBusiness(bizApp: FastifyInstance, biz: Business = DummyBiz) { return await bizApp.inject({ method: 'POST', url: '/businesses', - payload: DummyBiz + payload: biz }); } From 88eefa4a17442afa724ae69aea6eae0b639c948d Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Tue, 19 Jan 2021 21:56:01 -0330 Subject: [PATCH 29/75] Fixing cors handling for localhost --- src/cors.ts | 2 +- tests/cors.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cors.ts b/src/cors.ts index 1e4622b..a065d72 100644 --- a/src/cors.ts +++ b/src/cors.ts @@ -3,7 +3,7 @@ import fastifyCors from "fastify-cors"; export function registerCorsHandler(server: FastifyInstance) { server.register(fastifyCors, { - origin: ["https://localhost", "http://localhost", "https://ranlab-app-phzez.ondigitalocean.app/"], + origin: [/localhost/, "https://ranlab-app-phzez.ondigitalocean.app/"], credentials: true, strictPreflight: true }); diff --git a/tests/cors.test.ts b/tests/cors.test.ts index 6317470..241fcea 100644 --- a/tests/cors.test.ts +++ b/tests/cors.test.ts @@ -25,7 +25,7 @@ describe('CORS Handler Tests', function () { } it("Accepts localhost requests", async (done) => { - await testCorsRequest("http://localhost", "http://localhost"); + await testCorsRequest("http://localhost:3000", "http://localhost:3000"); done(); }); it("Does not accept requests from unapproved origin", async (done) => { From a022768e39325f433def92046c5ee737053ca5e0 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 21 Jan 2021 22:04:02 -0330 Subject: [PATCH 30/75] Fixing the route for business creation Adding auth0 to business routes fixing tests to match new setup --- package.json | 1 + src/database/productionDataLayer.ts | 59 +++++++++++++++------ src/endpoints/businesses.ts | 79 +++++++++++++++++++---------- src/endpoints/editRequests.ts | 63 +++++++++++++++++++++++ src/endpoints/filters.ts | 2 +- src/endpoints/regions.ts | 7 ++- src/index.ts | 3 +- tests/businesses.test.ts | 55 ++++++++------------ tests/cors.test.ts | 4 +- tests/dataLayer.test.ts | 49 ++++++++++-------- tests/editRequests.test.ts | 41 +++++++++++++++ tests/filters.test.ts | 16 ++++-- tests/regions.test.ts | 18 +++---- tests/utils/dummyData.ts | 34 ++++++++++--- tests/utils/testDataLayer.ts | 18 +++++-- tests/utils/testify.ts | 8 +-- yarn.lock | 18 +++++-- 17 files changed, 339 insertions(+), 136 deletions(-) create mode 100644 src/endpoints/editRequests.ts create mode 100644 tests/editRequests.test.ts diff --git a/package.json b/package.json index a801c30..3a9289f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "fastify-authz-jwks": "^1.1.11", "fastify-cors": "^5.1.0", "fastify-jwt": "^2.1.3", + "fastify-sensible": "^3.1.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0" }, diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index b87a939..6f47fef 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,13 +1,15 @@ import {Business} from "../endpoints/businesses"; import {firestore} from "./firestore"; import firebase from "firebase"; +import {AddRequest, DeleteRequest, UpdateRequest} from "../endpoints/editRequests"; export interface IdObject { id: string } export interface Region { - id: string, + id?: string | undefined, + name: string, manager: string, years?: {year: number, count: number}[] | undefined, industries?: {industry: string, count: number}[] | undefined, @@ -21,6 +23,9 @@ export interface DataLayer { setRegion(region: Region): Promise; deleteRegion(regionId: string): Promise; getAllRegions(): Promise; + createAddRequest(add: AddRequest): Promise; + createUpdateRequest(updateRequest: UpdateRequest): Promise; + createDeleteRequests(deleteRequest: DeleteRequest): Promise; } export interface Filters { @@ -29,15 +34,36 @@ export interface Filters { } export class ProductionDataLayer implements DataLayer { - async getBusinessesByRegion(region: string) : Promise { - let businessSnapshot = await firestore.collection("businesses").where("region", "==", region).get(); + async createAddRequest(addRequest: AddRequest): Promise { + let doc = firestore.collection("editRequests").doc(); + await doc.set(addRequest); + addRequest.id = doc.id; + return addRequest; + } + + async createUpdateRequest(updateRequest: UpdateRequest): Promise { + let doc = firestore.collection("editRequests").doc(); + await doc.set(updateRequest); + updateRequest.id = doc.id; + return updateRequest; + } + + async createDeleteRequests(deleteRequest: DeleteRequest): Promise { + let doc = firestore.collection("editRequests").doc(); + await doc.set(deleteRequest); + deleteRequest.id = doc.id; + return deleteRequest; + } + + async getBusinessesByRegion(regionId: string) : Promise { + let businessSnapshot = await firestore.collection("businesses").where("regionId", "==", regionId).get(); return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); } async setBusiness(newBusinessData: Business) : Promise { const bc = firestore.collection("businesses"); let businessRef = newBusinessData.id ? bc.doc(newBusinessData.id) : bc.doc(); - let regionRef = firestore.collection("regions").doc(newBusinessData.region); + let regionRef = firestore.collection("regions").doc(newBusinessData.regionId); return firestore.runTransaction(async transaction => { let regionDoc = await transaction.get(regionRef); if(!regionDoc.exists) { @@ -89,17 +115,18 @@ export class ProductionDataLayer implements DataLayer { async getAllRegions() : Promise { let regionsSnapshot = await firestore.collection("regions").get(); - return regionsSnapshot.docs.map((r) => ({id: r.id, manager: r.data().manager})); + return regionsSnapshot.docs.map((r) => ({id: r.id, name: r.data().name, manager: r.data().manager})); } async getRegionsManagedBy(managerId: string): Promise { let regionsSnapshot = await firestore.collection("regions").where("manager", "==", managerId).get(); - return regionsSnapshot.docs.map((r) => ({id: r.id, manager: r.data().manager})); + return regionsSnapshot.docs.map((r) => ({id: r.id, name: r.data().name, manager: r.data().manager})); } async setRegion(region: Region): Promise { - await firestore.collection("regions").doc(region.id).set({"manager": region.manager}); - return {id: region.id}; + let regionDoc = !!region.id ? firestore.collection("regions").doc(region.id) : firestore.collection("regions").doc(); + await regionDoc.set({name: region.name, "manager": region.manager}, {merge: true}); + return {id: regionDoc.id}; } async deleteRegion(id: string): Promise { @@ -112,15 +139,17 @@ export class ProductionDataLayer implements DataLayer { let businessRef = firestore.collection("businesses").doc(id); let businessDoc = await transaction.get(businessRef); let businessData = businessDoc.data(); - if (!!businessData && !!businessData.region) { - let regionRef = firestore.collection("regions").doc(businessData.region); - let regionDoc = await transaction.get(regionRef); - let {years, industries} = this.calculateNewFilters(regionDoc, businessData, -1); + if (!!businessData) { + if (!!businessData.regionId) { + let regionRef = firestore.collection("regions").doc(businessData.regionId); + let regionDoc = await transaction.get(regionRef); + let {years, industries} = this.calculateNewFilters(regionDoc, businessData, -1); - let update = {years: years, industries: industries}; + let update = {years: years, industries: industries}; - await transaction.update(regionRef, update) - await transaction.delete(businessRef) + await transaction.update(regionRef, update) + } + await transaction.delete(businessRef); } } ); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 91c642c..e97109d 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -5,7 +5,7 @@ import GeoPoint = firebase.firestore.GeoPoint; interface GetRegionBusinessRequest extends RequestGenericInterface { Params: { - region: string + regionId: string }, Headers: { access_token: string @@ -13,7 +13,10 @@ interface GetRegionBusinessRequest extends RequestGenericInterface { } interface CreateBusinessRequest extends RequestGenericInterface { - Body: Business + Params: { + regionId: string + }, + Body: Business, } interface UpdateBusinessRequest extends RequestGenericInterface { @@ -27,44 +30,64 @@ export interface Business { id?: string | undefined; name: string; employees: number; - region: string; + regionId: string; industry: string; year_added: number; location?: GeoPoint | null | undefined } -export function createRegionBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { - app.get('/regions/:region/businesses', +interface AuthToken { + userId: string, + admin: boolean +} +async function isRegionManager(userId: string, regionId: string, dataLayer: DataLayer) { + const regions = (await dataLayer.getRegionsManagedBy(userId)); + return regions.find((r) => r.name === regionId); +} + +export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { + app.get('/regions/:regionId/businesses', async (request) => { - let response = { - status: "ok", - date: Date.now(), - region: request.params.region, - businesses: [], - pageStart: "1", - pageEnd: "2", - filters: {} - }; - response.businesses = (await dataLayer.getBusinessesByRegion(request.params.region)).slice(0, 10); - response.filters = await dataLayer.getFilters(request.params.region); - return JSON.stringify(response); + let {userId, admin} = await request.jwtVerify(); + if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + throw app.httpErrors.unauthorized("User does not have access to region"); + } else { + let response = { + status: "ok", + date: Date.now(), + region: request.params.regionId, + businesses: [], + pageStart: "1", + pageEnd: "2", + filters: {} + }; + + response.businesses = (await dataLayer.getBusinessesByRegion(request.params.regionId)).slice(0, 10); + response.filters = await dataLayer.getFilters(request.params.regionId); + return JSON.stringify(response); + } } ); - return app; -} -export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.post( - '/businesses', + '/regions/:regionId/businesses', async (request) => { - let businessRef = await dataLayer.setBusiness(request.body); - let response = { - status: "ok", - date: Date.now(), - businessId: businessRef.id - }; - return JSON.stringify(response); + + let {userId, admin} = await request.jwtVerify(); + if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + throw app.httpErrors.unauthorized("User does not have access to region"); + } else if (!!request.body.regionId && request.body.regionId !== request.params.regionId) { + throw app.httpErrors.badRequest("Region ID mismatch between route and body "); + } else { + let businessRef = await dataLayer.setBusiness(request.body); + let response = { + status: "ok", + date: Date.now(), + businessId: businessRef.id + }; + return JSON.stringify(response); + } } ); diff --git a/src/endpoints/editRequests.ts b/src/endpoints/editRequests.ts new file mode 100644 index 0000000..846c083 --- /dev/null +++ b/src/endpoints/editRequests.ts @@ -0,0 +1,63 @@ +import {FastifyInstance, RequestGenericInterface} from "fastify"; +import {DataLayer} from "../database/productionDataLayer"; +import {Business} from "./businesses"; + +export interface EditRequests { + adds: AddRequest[] | undefined, + updates: UpdateRequest[] | undefined, + deletes: DeleteRequest[] | undefined +} + +export interface AddRequest { + id?: string | undefined, + business: Business +} + +export interface UpdateRequest { + id?: string | undefined, + business: Business +} + +export interface DeleteRequest { + id?: string | undefined, + businessId: string +} + +interface CreateEditRequests extends RequestGenericInterface { + Body: EditRequests +} + +export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer) { + app.post( + '/edits', + async (request, reply) => { + let response = { + status: "ok", + editRequests: {} + }; + let adds : AddRequest[] = []; + const editRequests = request.body; + if(!!editRequests.adds) { + for(let addRequest of editRequests.adds) { + adds.push(await dataLayer.createAddRequest(addRequest)); + } + } + let updates : UpdateRequest[] = []; + if(!!editRequests.updates) { + for(let updateRequest of editRequests.updates) { + updates.push(await dataLayer.createUpdateRequest(updateRequest)); + } + } + let deletes : DeleteRequest[] = []; + if(!!editRequests.deletes) { + for(let deleteRequest of editRequests.deletes) { + deletes.push(await dataLayer.createDeleteRequests(deleteRequest)); + } + } + response.editRequests = {adds, updates, deletes}; + reply.code(201); + return JSON.stringify(response); + } + ); + return app; +} diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index 89c0ffc..046b147 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -8,7 +8,7 @@ interface GetFiltersRequest extends RequestGenericInterface { } export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { - app.get('/filters/:regionId', + app.get('/regions/:regionId/filters', async (request) => { let response = { status: "ok", diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 4ee3a0d..b94f435 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -13,7 +13,6 @@ interface GetSingleRegionRequest extends RequestGenericInterface { } } - interface CreateRegionRequest extends RequestGenericInterface { Body: Region } @@ -54,7 +53,7 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : async(request, reply) => { let response = { status: "ok", - region: request.body.id + region: request.body.name }; await dataLayer.setRegion(request.body); reply.code(201); @@ -77,7 +76,7 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : } else { regions = (await dataLayer.getRegionsManagedBy(userId)); } - let region: Region | undefined = regions.find((r => r.id == request.params.regionId)) + let region: Region | undefined = regions.find((r => r.name == request.params.regionId)) response.region = !!region ? region : null; return JSON.stringify(response); } @@ -98,7 +97,7 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : app.delete('/regions/:regionId', async (request, reply) => { let {userId} = <{userId:string}>await request.jwtVerify(); - if((await dataLayer.getRegionsManagedBy(userId)).find((r) => r.id === request.params.regionId)) { + if((await dataLayer.getRegionsManagedBy(userId)).find((r) => r.name === request.params.regionId)) { await dataLayer.deleteRegion(request.params.regionId); reply.code(204); } else { diff --git a/src/index.ts b/src/index.ts index 3a25d31..ca153a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import {productionDataLayer} from "./database/productionDataLayer"; import {createFiltersEndpoint} from "./endpoints/filters"; import {registerAuth0} from "./auth0"; import {registerCorsHandler} from "./cors"; - +import fastifySensible from "fastify-sensible"; const port = Number(process.env.PORT || 8080); const server = addRoutes( @@ -19,6 +19,7 @@ const server = addRoutes( (app: FastifyInstance) => createRegionBusinessesEndpoint(app, productionDataLayer) ); +server.register(fastifySensible); registerAuth0(server); registerCorsHandler(server); diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index ed0b830..a38c468 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,28 +1,33 @@ -import {fastify} from "fastify"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {createBusinessesEndpoint, createRegionBusinessesEndpoint} from "../src/endpoints/businesses"; -import {createDummyBusiness, DummyBiz, DummyRegion} from "./utils/dummyData"; -import {MockAuth0Return, testify} from "./utils/testify"; +import {createBusinessesEndpoint} from "../src/endpoints/businesses"; +import { + createDummyBusiness, + createDummyRegion, + DummyBiz, + getDummyBusinesses +} from "./utils/dummyData"; +import { MockAuth0Return, testify} from "./utils/testify"; +import createRegionsEndpoint from "../src/endpoints/regions"; describe("Business Endpoint Tests", () => { let testDataLayer: DummyDatalayer; - beforeEach(() => { + beforeEach(async (done) => { testDataLayer = new DummyDatalayer(); + const server = testify(new MockAuth0Return()); + const regionsApp = createRegionsEndpoint(server, testDataLayer); + await createDummyRegion(regionsApp); + done(); }); it('Can create and retrieve a valid business', async(done) => { const server = testify(new MockAuth0Return()); const bizApp = createBusinessesEndpoint(server, testDataLayer) - const regionBizApp = createRegionBusinessesEndpoint(server, testDataLayer); const createResponse = await createDummyBusiness(bizApp); expect(createResponse.statusCode).toBe(200); - const getResponse = await regionBizApp.inject({ - method: 'GET', - url: `/regions/${DummyRegion.id}/businesses` - }); - + const getResponse = await getDummyBusinesses(bizApp); expect(getResponse.statusCode).toBe(200); + let {businesses, filters} = JSON.parse(getResponse.payload); expect(businesses).toEqual(expect.arrayContaining([expect.objectContaining({name: DummyBiz.name})])); expect(filters).toEqual(expect.objectContaining({years: [DummyBiz.year_added], industries: [DummyBiz.industry]})) @@ -30,31 +35,11 @@ describe("Business Endpoint Tests", () => { done(); }); - it('Can paginate businesses', async(done) => { - const server = testify(new MockAuth0Return()); - const bizApp = createBusinessesEndpoint(server, testDataLayer); - const regionBizApp = createRegionBusinessesEndpoint(server, testDataLayer); - for(let i = 0; i < 20; i++) { - await createDummyBusiness(bizApp); - } - const pagedResponse = await regionBizApp.inject({ - method: "GET", - url: `/regions/${DummyRegion.id}/businesses` - }); - expect(pagedResponse.statusCode).toBe(200); - const pagedPayload = JSON.parse(pagedResponse.payload); - expect(pagedPayload).toStrictEqual(expect.objectContaining({ - businesses: expect.arrayContaining([expect.anything()]), - pageStart: expect.stringMatching("[a-z0-9]*"), - pageEnd: expect.stringMatching("[a-z0-9]*") - })); - expect(pagedPayload.businesses).toHaveLength(10); - done(); - }); - it('Can update and retrieve a business', async(done) => { - const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - const bizId = JSON.parse((await createDummyBusiness(bizApp)).payload).businessId; + const bizApp = createBusinessesEndpoint(testify(new MockAuth0Return()), testDataLayer); + + const createResponse = await createDummyBusiness(bizApp); + const bizId = JSON.parse(createResponse.payload).businessId; const updatedBiz = {...DummyBiz, id: bizId, year_added: 2020, employees: 2}; const updateResponse = await bizApp.inject({ method: 'POST', diff --git a/tests/cors.test.ts b/tests/cors.test.ts index 241fcea..b8f4776 100644 --- a/tests/cors.test.ts +++ b/tests/cors.test.ts @@ -7,11 +7,11 @@ describe('CORS Handler Tests', function () { async function testCorsRequest(origin: string, originResponse: string | boolean) { let mockReturn = new MockAuth0Return(); let testApp = testify(mockReturn); - mockReturn.user = "Dummy"; + mockReturn.userId = "Dummy"; registerCorsHandler(testApp); let response = await testApp.inject({ method: 'OPTIONS', - url: `/regions/${DummyRegion.id}/businesses`, + url: `/regions/${DummyRegion.name}/businesses`, headers: { 'authorization': 'Bearer abc123', 'access-control-request-headers': 'x-requested-with', diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 4639518..f5d5646 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -1,34 +1,39 @@ import {productionDataLayer, Region} from "../src/database/productionDataLayer"; import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; +import objectContaining = jasmine.objectContaining; describe("Production Data Layer Tests", () => { + async function deleteRegionsNamed(regionName: string) { + (await firestore.collection("regions").where("name", "==", regionName).get()).docs + .forEach((doc) => doc.ref.delete()); + } + beforeEach(async(done) => { - let bizDocs = (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs; - bizDocs.forEach((d) => d.ref.delete()); + (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs.forEach((d) => d.ref.delete()); await firestore.collection("years").doc("2019").delete(); - await firestore.collection("regions").doc("DummyRegion").delete(); - await firestore.collection("regions").doc("DummyRegion2").delete(); + await deleteRegionsNamed("DummyRegion"); + await deleteRegionsNamed("DummyRegion2"); done(); }); - it("Creates, retrieves, updates, and deletes businesses", async (done) => { + it("Creates, retrieves, updates, and deletes businesses while maintaining correct region filter data", async (done) => { let biz : Business = { employees: 1, name: "DummyBiz", - region: "DummyRegion", + regionId: "", industry: "DummyIndustry", year_added: 2019 }; - await productionDataLayer.setRegion({id: "DummyRegion", manager: "Dummy"}) + biz.regionId = (await productionDataLayer.setRegion({name: "DummyRegion", manager: "Dummy"})).id; let id = (await productionDataLayer.setBusiness(biz)).id; expect(id).toBeTruthy(); - let bizData = await productionDataLayer.getBusinessesByRegion(biz.region); + let bizData = await productionDataLayer.getBusinessesByRegion(biz.regionId); expect(bizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); - let filters = await productionDataLayer.getFilters(biz.region); + let filters = await productionDataLayer.getFilters(biz.regionId); expect(filters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})) biz.id = id; @@ -37,17 +42,17 @@ describe("Production Data Layer Tests", () => { let updatedId = (await productionDataLayer.setBusiness(biz)).id; expect(updatedId).toEqual(id); - let updatedBizData = await productionDataLayer.getBusinessesByRegion(biz.region); + let updatedBizData = await productionDataLayer.getBusinessesByRegion(biz.regionId); expect(updatedBizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); - let updatedFilters = await productionDataLayer.getFilters(biz.region); + let updatedFilters = await productionDataLayer.getFilters(biz.regionId); expect(updatedFilters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})); await productionDataLayer.deleteBusiness(biz.id); - let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.region); + let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.regionId); expect(emptyBizData).toEqual([]); - let emptyFilters = await productionDataLayer.getFilters(biz.region); + let emptyFilters = await productionDataLayer.getFilters(biz.regionId); expect(emptyFilters.years).toEqual([]); done(); @@ -55,19 +60,21 @@ describe("Production Data Layer Tests", () => { it("Creates, retrieves, updates, and deletes regions", async (done) => { let region: Region = { - id: "DummyRegion", + name: "DummyRegion", manager: "Dummy Manager" }; - let id = await productionDataLayer.setRegion(region); + let {id} = await productionDataLayer.setRegion(region); expect(id).toBeTruthy(); + expect(id).not.toBe(region.name); + region.id = id; let out = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); - expect(out).toEqual(expect.arrayContaining([region])); + expect(out).toEqual(expect.arrayContaining([objectContaining({...region})])); region.manager = "New Manager"; - let updatedId = (await productionDataLayer.setRegion(region)); - expect(updatedId.id).toEqual(region.id); + let{id: updatedId} = (await productionDataLayer.setRegion(region)); + expect(updatedId).toEqual(region.id); let oldManagerRegions = await productionDataLayer.getRegionsManagedBy("Dummy Manager"); expect(oldManagerRegions).toEqual(expect.arrayContaining([])); @@ -76,10 +83,12 @@ describe("Production Data Layer Tests", () => { expect(newManagerRegions).toEqual(expect.arrayContaining([region])); let region2: Region = { - id: "DummyRegion2", + name: "DummyRegion2", manager: "Manager2" }; - await productionDataLayer.setRegion(region2); + region2.id = (await productionDataLayer.setRegion(region2)).id; + expect(region2.id).not.toBe(region.id); + let adminRegions = await productionDataLayer.getAllRegions(); expect(adminRegions).toEqual(expect.arrayContaining([region, region2])); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts new file mode 100644 index 0000000..7882191 --- /dev/null +++ b/tests/editRequests.test.ts @@ -0,0 +1,41 @@ +import {DummyDatalayer} from "./utils/testDataLayer"; +import {getMockToken, MockAuth0Return, testify} from "./utils/testify"; +import {createEditEndpoint} from "../src/endpoints/editRequests"; + +describe("Edit Request tests", () => { + + let testDataLayer: DummyDatalayer + it("Submitted edit requests are seen by region admin", async (done) => { + testDataLayer = new DummyDatalayer(); + let mockAuth0 = new MockAuth0Return(); + const testApp = testify(mockAuth0); + mockAuth0.userId = "nobody"; + let editEndpoint = createEditEndpoint(testApp, testDataLayer); + + const editResponse = await editEndpoint.inject({ + method: "POST", + url: `/edits`, + headers: {authorization: `Bearer ${getMockToken({userId: mockAuth0.userId, admin: false})}`}, + payload: { + adds: [], + updates: [], + deletes: [] + } + }); + + expect(editResponse.statusCode).toBe(201); + expect(JSON.parse(editResponse.payload)).toStrictEqual( + expect.objectContaining({ + editRequests: { + adds: [], + updates: [], + deletes: [] + } + }) + ); + + await testApp.close(); + + done(); + }); +}); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index f33a493..694c794 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -2,19 +2,25 @@ import {fastify} from "fastify"; import {createFiltersEndpoint} from "../src/endpoints/filters"; import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {createDummyBusiness, DummyBiz, DummyRegion} from "./utils/dummyData"; +import {createDummyBusiness, createDummyRegion, DummyBiz, DummyRegion} from "./utils/dummyData"; +import createRegionsEndpoint from "../src/endpoints/regions"; +import {MockAuth0Return, testify} from "./utils/testify"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; beforeAll(async (done) => { testDataLayer = new DummyDatalayer(); - const bizApp = createBusinessesEndpoint(fastify(), testDataLayer); - await createDummyBusiness(bizApp) + const server = testify(new MockAuth0Return()); + const regionsApp = createRegionsEndpoint(server, testDataLayer); + const bizApp = createBusinessesEndpoint(server, testDataLayer); + + await createDummyRegion(regionsApp); + await createDummyBusiness(bizApp); await createDummyBusiness(bizApp, { name: `Not ${DummyBiz.name}`, employees: 1, - region: `Not ${DummyRegion.id}`, + regionId: `Not ${DummyRegion.name}`, industry: `Not ${DummyBiz.industry}`, year_added: DummyBiz.year_added + 1 }); @@ -23,7 +29,7 @@ describe("Filter Endpoint Tests", () => { it('Returns only the region-specific filter data', async (done) => { const filterApp = createFiltersEndpoint(fastify(), testDataLayer); - const filterResponse = await filterApp.inject({ method: 'GET', url: `/filters/${DummyRegion.id}` }); + const filterResponse = await filterApp.inject({ method: 'GET', url: `/regions/${DummyRegion.name}/filters` }); expect(filterResponse.statusCode).toBe(200); expect(JSON.parse(filterResponse.payload).filters).toStrictEqual( diff --git a/tests/regions.test.ts b/tests/regions.test.ts index f24e585..50e1cea 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -21,9 +21,9 @@ describe("Region Endpoint Tests", () => { it('Can create and retrieve all regions as SysAdmin', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer); - let testRegions: Region[] = [{id: "region1", manager: "manager1"}, {id: "region2", manager: "manager2"}]; + let testRegions: Region[] = [{name: "region1", manager: "manager1"}, {name: "region2", manager: "manager2"}]; testRegions.forEach((r) => testDataLayer.setRegion(r)); - mockAuth0Return.user = "DummyUser"; + mockAuth0Return.userId = "DummyUser"; const response = await getDummyRegions(app, getMockToken({userId: 'admin', admin: true})); expect(mockAuth0Return.callCount).toBe(1); @@ -35,7 +35,7 @@ describe("Region Endpoint Tests", () => { it('Can create and retrieve a region as Region Manager', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer); - mockAuth0Return.user = "DummyUser"; + mockAuth0Return.userId = "DummyUser"; const response = await getDummyRegions(app); expect(mockAuth0Return.callCount).toBe(1); @@ -48,17 +48,17 @@ describe("Region Endpoint Tests", () => { it('Can update and retrieve a region', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer); const updatedRegion = { - id: "TestRegion", + name: "TestRegion", manager: "TestManager" }; const response = await app.inject( { method: 'POST', - url: `/regions/${DummyRegion.id}`, + url: `/regions/${DummyRegion.name}`, payload: updatedRegion }); expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).region).toEqual(updatedRegion); + expect(JSON.parse(response.payload).region).toStrictEqual(updatedRegion); await app.close(); done(); }); @@ -67,7 +67,7 @@ describe("Region Endpoint Tests", () => { const app = createRegionsEndpoint(testApp, testDataLayer); const deleteResponse = await app.inject( { method: 'DELETE', - url: `/regions/${DummyRegion.id}`, + url: `/regions/${DummyRegion.name}`, headers: {authorization: `Bearer ${dummyToken}`} }); @@ -84,7 +84,7 @@ describe("Region Endpoint Tests", () => { const app = createRegionsEndpoint(testApp, testDataLayer); const getRegionAdminResponse = await app.inject({ method: 'GET', - url: `/regions/${DummyRegion.id}`, + url: `/regions/${DummyRegion.name}`, headers: {authorization: `Bearer ${dummyToken}`} }); @@ -93,7 +93,7 @@ describe("Region Endpoint Tests", () => { const getSysAdminResponse = await app.inject({ method: 'GET', - url: `/regions/${DummyRegion.id}`, + url: `/regions/${DummyRegion.name}`, headers: {authorization: `Bearer ${dummyAdminToken}`} }); diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index 1432d82..df547d5 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -8,26 +8,48 @@ export const dummyToken = getMockToken({userId: dummyManager}) export const dummyAdminToken = getMockToken({userId: "", admin: true}); export const DummyRegion: Region = { - id: "DummyRegion", + name: "DummyRegion", manager: dummyManager }; export const DummyBiz: Business = { name: "DummyBiz", - region: DummyRegion.id, + regionId: DummyRegion.name, year_added: 2009, employees: 1, industry: "DummyIndustry" }; -export async function createDummyBusiness(bizApp: FastifyInstance, biz: Business = DummyBiz) { +export async function createDummyRegion(regionsApp: FastifyInstance) { + await regionsApp.inject({ + method: "POST", + url: "/regions", + payload: DummyRegion, + headers: {authorization: `Bearer ${dummyToken}`} + }); +} + +export async function createDummyBusiness(bizApp: FastifyInstance, biz : Business = DummyBiz, token: string = dummyToken) { return await bizApp.inject({ method: 'POST', - url: '/businesses', - payload: biz + url: `/regions/${biz.regionId}/businesses`, + payload: biz, + headers: {authorization: `Bearer ${token}`} + }); +} + +export async function getDummyBusinesses(bizApp: FastifyInstance, token: string = dummyToken) { + return await bizApp.inject({ + method: 'GET', + url: `/regions/${DummyBiz.regionId}/businesses`, + headers: {authorization: `Bearer ${token}`} }); } export async function getDummyRegions(app: FastifyInstance, token: string = dummyToken) { - return await app.inject({method: 'GET', headers: {authorization: `Bearer ${token}`}, url: `/regions`}); + return await app.inject({ + method: 'GET', + headers: {authorization: `Bearer ${token}`}, + url: `/regions` + }); } diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index e6f8829..364b84a 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -1,7 +1,17 @@ import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; import {Business} from "../../src/endpoints/businesses"; +import { AddRequest, UpdateRequest, DeleteRequest } from "../../src/endpoints/editRequests"; export class DummyDatalayer implements DataLayer { + createAddRequest(_: AddRequest): Promise { + throw new Error("Method not implemented."); + } + createUpdateRequest(_: UpdateRequest): Promise { + throw new Error("Method not implemented."); + } + createDeleteRequests(_: DeleteRequest): Promise { + throw new Error("Method not implemented."); + } businesses: Business[] = []; regions: Region[] = []; @@ -16,14 +26,14 @@ export class DummyDatalayer implements DataLayer { async getFilters(regionId: string): Promise { return { - years: this.businesses.filter(b => b.region=== regionId).map((b) => b.year_added), - industries: this.businesses.filter(b => b.region=== regionId).map((b) => b.industry) + years: this.businesses.filter(b => b.regionId=== regionId).map((b) => b.year_added), + industries: this.businesses.filter(b => b.regionId=== regionId).map((b) => b.industry) }; } async setRegion(region: Region): Promise { this.regions.push(region); - return {id: region.id}; + return {id: region.name}; } async getAllRegions() : Promise { @@ -35,7 +45,7 @@ export class DummyDatalayer implements DataLayer { } async deleteRegion(regionId: string): Promise { - this.regions = this.regions.filter((r) => r.id !== regionId); + this.regions = this.regions.filter((r) => r.name !== regionId); } clearRegions() { diff --git a/tests/utils/testify.ts b/tests/utils/testify.ts index c635c86..900c67f 100644 --- a/tests/utils/testify.ts +++ b/tests/utils/testify.ts @@ -1,14 +1,15 @@ import fastify from "fastify"; import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; import jwt from "jsonwebtoken"; +import fastifySensible from "fastify-sensible"; export const mockSecret = 'dummy'; export class MockAuth0Return { - user: string|null = null; + userId: string|null = null; callCount: number = 0; reset() { this.callCount = 0; - this.user = null + this.userId = null } } @@ -22,8 +23,9 @@ export const testify = (mockAuth0Return : MockAuth0Return) => { secret: (_request, _reply, _provider) => { mockAuth0Return.callCount++; _provider(null, mockSecret);}, audience: 'https://localhost', issuer: 'https://localhost/', - algorithms: ['RS256'], + algorithms: ['none'], decode: { complete: true }, }); + f.register(fastifySensible); return f; }; diff --git a/yarn.lock b/yarn.lock index a44d86f..94b8ca4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1983,6 +1983,18 @@ fastify-plugin@^3.0.0: resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz" integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== +fastify-sensible@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/fastify-sensible/-/fastify-sensible-3.1.0.tgz#259f13373e0df2d37b720e08474db96c6f62985a" + integrity sha512-T5ZzpZ0I+seu4WvtU3O1YD3OhYU2RLewTObaMHCxkxEw8uLFRk9zgnbYk7YeiS8azV65jTPYTElCfgrHhfqOXA== + dependencies: + fast-deep-equal "^3.1.1" + fastify-plugin "^3.0.0" + forwarded "^0.1.2" + http-errors "^1.7.3" + type-is "^1.6.18" + vary "^1.1.2" + fastify-warning@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz" @@ -2137,7 +2149,7 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -forwarded@~0.1.2: +forwarded@^0.1.2, forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= @@ -2372,7 +2384,7 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.1: +http-errors@^1.7.1, http-errors@^1.7.3: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== @@ -4626,7 +4638,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17: +type-is@^1.6.18, type-is@~1.6.17: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== From c28cb35e1bf87d66211069dba5b8e42a57177571 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 31 Jan 2021 02:47:12 -0330 Subject: [PATCH 31/75] Adding swagger docs to the api at [server]/documentation Adding the /regions/manager/:managerId route --- .npmignore | 0 jsdoc.json | 15 +++ package.json | 1 + src/database/productionDataLayer.ts | 23 +++-- src/endpoints/businesses.ts | 12 ++- src/endpoints/docs/basicSchemas.ts | 9 ++ src/endpoints/docs/businessesSchemas.ts | 85 +++++++++++++++++ src/endpoints/docs/filterSchemas.ts | 32 +++++++ src/endpoints/docs/regionSchemas.ts | 120 ++++++++++++++++++++++++ src/endpoints/filters.ts | 3 +- src/endpoints/regions.ts | 59 +++++++----- src/index.ts | 23 +++-- src/swagger.ts | 21 +++++ tests/utils/dummyData.ts | 2 +- tsoa.json | 12 +++ yarn.lock | 119 ++++++++++++++++++++++- 16 files changed, 486 insertions(+), 50 deletions(-) create mode 100644 .npmignore create mode 100644 jsdoc.json create mode 100644 src/endpoints/docs/basicSchemas.ts create mode 100644 src/endpoints/docs/businessesSchemas.ts create mode 100644 src/endpoints/docs/filterSchemas.ts create mode 100644 src/endpoints/docs/regionSchemas.ts create mode 100644 src/swagger.ts create mode 100644 tsoa.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e69de29 diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..fcb2bb1 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,15 @@ +{ + "opts": { + "template": "node_modules/better-docs" + }, + "tags": { + "allowUnknownTags": ["optional", "category"] + }, + "plugins": [ + "node_modules/better-docs/typescript", + "node_modules/better-docs/category" + ], + "source": { + "includePattern": ".+\\.(jsx|js|ts|tsx)$" + } +} diff --git a/package.json b/package.json index 3a9289f..40c152b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "fastify-cors": "^5.1.0", "fastify-jwt": "^2.1.3", "fastify-sensible": "^3.1.0", + "fastify-swagger": "^4.0.1", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0" }, diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 6f47fef..570f315 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -11,8 +11,15 @@ export interface Region { id?: string | undefined, name: string, manager: string, - years?: {year: number, count: number}[] | undefined, - industries?: {industry: string, count: number}[] | undefined, + filters?: { + years?: { year: number, count: number }[] | undefined, + industries?: { industry: string, count: number }[] | undefined, + } +} + +export interface Filters { + years?: number[] | undefined, + industries?: string[] } export interface DataLayer { @@ -28,10 +35,6 @@ export interface DataLayer { createDeleteRequests(deleteRequest: DeleteRequest): Promise; } -export interface Filters { - years?: number[] | undefined, - industries?: string[] -} export class ProductionDataLayer implements DataLayer { async createAddRequest(addRequest: AddRequest): Promise { @@ -97,9 +100,9 @@ export class ProductionDataLayer implements DataLayer { } } } - let update = {years: years, industries: industries}; + let regionUpdate = {filters: {years: years, industries: industries}}; - await transaction.update(regionRef, update) + await transaction.update(regionRef, regionUpdate) await transaction.set(businessRef, newBusinessData); }).then(() => {id: businessRef.id}); } @@ -145,9 +148,9 @@ export class ProductionDataLayer implements DataLayer { let regionDoc = await transaction.get(regionRef); let {years, industries} = this.calculateNewFilters(regionDoc, businessData, -1); - let update = {years: years, industries: industries}; + let regionUpdate = {filters: {years: years, industries: industries}}; - await transaction.update(regionRef, update) + await transaction.update(regionRef, regionUpdate) } await transaction.delete(businessRef); } diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index e97109d..d670eee 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -2,6 +2,7 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; +import {createBizSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; interface GetRegionBusinessRequest extends RequestGenericInterface { Params: { @@ -46,7 +47,10 @@ async function isRegionManager(userId: string, regionId: string, dataLayer: Data } export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { - app.get('/regions/:regionId/businesses', + + app.get( + '/regions/:regionId/businesses', + {schema: getBizSchema}, async (request) => { let {userId, admin} = await request.jwtVerify(); @@ -58,8 +62,6 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa date: Date.now(), region: request.params.regionId, businesses: [], - pageStart: "1", - pageEnd: "2", filters: {} }; @@ -70,10 +72,11 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa } ); + app.post( '/regions/:regionId/businesses', + {schema: createBizSchema}, async (request) => { - let {userId, admin} = await request.jwtVerify(); if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { throw app.httpErrors.unauthorized("User does not have access to region"); @@ -93,6 +96,7 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa app.post( '/businesses/:businessId', + {schema: updateBizSchema}, async (request) => { let updatedBiz = {...request.body, id: request.params.businessId}; await dataLayer.setBusiness(updatedBiz); diff --git a/src/endpoints/docs/basicSchemas.ts b/src/endpoints/docs/basicSchemas.ts new file mode 100644 index 0000000..be2be35 --- /dev/null +++ b/src/endpoints/docs/basicSchemas.ts @@ -0,0 +1,9 @@ +export const byRegionIdSchema = { + type: 'object', + properties: { + regionId: { + type: 'string', + description: "The region to use for this request" + } + } +}; diff --git a/src/endpoints/docs/businessesSchemas.ts b/src/endpoints/docs/businessesSchemas.ts new file mode 100644 index 0000000..d8277ba --- /dev/null +++ b/src/endpoints/docs/businessesSchemas.ts @@ -0,0 +1,85 @@ +import {filtersSchema} from "./filterSchemas"; +import {byRegionIdSchema} from "./basicSchemas"; + +export const businessSchema = { + type: 'object', + properties: { + id: { + type: "string", + description: "The id for the specific business. Should be omitted when creating new entries.", + nullable: true + }, + name: {type: 'string'}, + employees: { type: 'number'}, + regionId: {type: 'string'}, + industry: {type: 'string'}, + year_added: {type: 'number'}, + location: { + type: 'array', + items: { type: "number", max: 2}, + nullable: true + } + } +}; + +export const businessesSchema = { + type: 'array', + items: businessSchema +}; + +export const getBizSchema = { + description: "Returns all businesses located in the specified region", + params: byRegionIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + region: {type: 'string'}, + businesses: businessesSchema, + filters: filtersSchema + } + } + } +}; + +export const createBizSchema = { + description: "Creates a business in the specified region", + params: byRegionIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: "string"}, + businessId: {type: "string"} + } + } + } +}; + +const bizByIdSchema = { + type: 'object', + properties: { + businessId: {type: "string"} + } +}; + +export const updateBizSchema = { + description: "Updates the specified business", + params: bizByIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: "string"}, + business: businessSchema + } + } + } +}; diff --git a/src/endpoints/docs/filterSchemas.ts b/src/endpoints/docs/filterSchemas.ts new file mode 100644 index 0000000..1a31719 --- /dev/null +++ b/src/endpoints/docs/filterSchemas.ts @@ -0,0 +1,32 @@ +import {byRegionIdSchema} from "./basicSchemas"; + +export const filtersSchema = { + type: 'object', + description: "Only valid for GET responses", + properties: { + years: { + type: 'array', + items: {type: 'number'} + }, + industries: { + type: 'array', + items: {type: 'string'} + }, + } +} + +export const getFilterSchema = { + description: 'Endpoint for interacting directly with filters for a particular region', + params: byRegionIdSchema, + security: [], + response: { + 200: { + type: 'object', + properties: { + status: { type: 'string'}, + date: {type: 'string' }, + filters: filtersSchema + } + } + } +} diff --git a/src/endpoints/docs/regionSchemas.ts b/src/endpoints/docs/regionSchemas.ts new file mode 100644 index 0000000..1fee9dc --- /dev/null +++ b/src/endpoints/docs/regionSchemas.ts @@ -0,0 +1,120 @@ +import {filtersSchema} from "./filterSchemas"; +import {byRegionIdSchema} from "./basicSchemas"; + +const byManagerIdSchema = { + type: 'object', + properties: { + managerId: {type: 'string'} + } +}; + +const getRegionSchema = { + type: 'object', + properties: { + id: {type: "string"}, + name: {type: "string"}, + manager: {type: "string"}, + filters: filtersSchema + } +}; + +const createRegionSchema = { + type: 'object', + properties: { + name: {type: "string"}, + manager: {type: "string"} + } +}; + +const updateRegionSchema = { + id: { + type: "string", + description: "The id of the region, if applicable.", + nullable: true + }, + name: {type: "string"}, + manager: {type: "string"} +} + + +export const getManagedRegionsReqSchema = { + description: "Returns all regions managed by the authenticated user", + securitySchemes: [], + params: byManagerIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + regions: { + type: 'array', + items: getRegionSchema + } + + } + } + } +}; + +export const getSingleRegionReqSchema = { + description: "Returns a single region specified by ID", + securitySchemes: [], + params: byRegionIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + region: getRegionSchema + } + } + } +}; + +export const createRegionReqSchema = { + description: "Returns all regions managed by the authenticated user", + securitySchemes: [], + params: byManagerIdSchema, + body: createRegionSchema, + response: { + 201: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + region: {type: 'string'} + } + } + } +}; + +export const updateRegionReqSchema = { + description: "Updates the data for a specified region", + params: byRegionIdSchema, + body: updateRegionSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + region: getRegionSchema + } + } + } +}; + +export const deleteRegionReqSchema = { + description: "Deletes the specified region", + params: byRegionIdSchema, + response: { + 204: { + description: 'Region successfully deleted', + type: 'null' + } + } +}; diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index 046b147..daf9e71 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,5 +1,6 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; +import {getFilterSchema} from "./docs/filterSchemas"; interface GetFiltersRequest extends RequestGenericInterface { Params: { @@ -8,7 +9,7 @@ interface GetFiltersRequest extends RequestGenericInterface { } export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { - app.get('/regions/:regionId/filters', + app.get('/regions/:regionId/filters', {schema: getFilterSchema}, async (request) => { let response = { status: "ok", diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index b94f435..8656a60 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -1,5 +1,12 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Region} from "../database/productionDataLayer"; +import { + getManagedRegionsReqSchema, + getSingleRegionReqSchema, + createRegionReqSchema, + updateRegionReqSchema, + deleteRegionReqSchema +} from "./docs/regionSchemas"; interface GetManagedRegionsRequest extends RequestGenericInterface { Params: { @@ -31,41 +38,29 @@ interface UpdateRegionRequest extends RequestGenericInterface { } export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer) { - app.get('/regions', + app.get( + '/regions/manager/:managerId', + {schema: getManagedRegionsReqSchema}, async (request) => { - let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); + let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); let response = { - statusCode: 200, status: "ok", date: Date.now(), regions: [] - } - if(admin) { - response.regions.push(...(await dataLayer.getAllRegions())); - } else { + }; + if(admin || userId == request.params.managerId) { response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); } return JSON.stringify(response); } ); - app.post('/regions', - async(request, reply) => { - let response = { - status: "ok", - region: request.body.name - }; - await dataLayer.setRegion(request.body); - reply.code(201); - return JSON.stringify(response); - } - ); - - app.get('/regions/:regionId', + app.get( + '/regions/:regionId', + {schema: getSingleRegionReqSchema}, async(request ) => { let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); let response = { - statusCode: 200, status: "ok", date: Date.now(), region: null @@ -82,7 +77,23 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : } ); - app.post('/regions/:regionId', + app.post( + '/regions', + {schema: createRegionReqSchema}, + async(request, reply) => { + let response = { + status: "ok", + regionId: "" + }; + response.regionId = (await dataLayer.setRegion(request.body)).id; + reply.code(201); + return JSON.stringify(response); + } + ); + + app.post( + '/regions/:regionId', + {schema: updateRegionReqSchema}, async(request) => { let UpdatedRegion: Region = {...request.body}; let response = { @@ -94,7 +105,9 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : } ); - app.delete('/regions/:regionId', + app.delete( + '/regions/:regionId', + {schema: deleteRegionReqSchema}, async (request, reply) => { let {userId} = <{userId:string}>await request.jwtVerify(); if((await dataLayer.getRegionsManagedBy(userId)).find((r) => r.name === request.params.regionId)) { diff --git a/src/index.ts b/src/index.ts index ca153a9..435ea44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,36 @@ import fastify, {FastifyInstance} from 'fastify'; +import fastifySensible from "fastify-sensible"; import createPingEndpoint from './endpoints/ping'; import { addRoutes } from './utils'; -import {createRegionBusinessesEndpoint, createBusinessesEndpoint} from "./endpoints/businesses"; +import { createBusinessesEndpoint} from "./endpoints/businesses"; import createRegionsEndpoint from "./endpoints/regions"; import {productionDataLayer} from "./database/productionDataLayer"; import {createFiltersEndpoint} from "./endpoints/filters"; import {registerAuth0} from "./auth0"; import {registerCorsHandler} from "./cors"; -import fastifySensible from "fastify-sensible"; +import {registerSwagger} from "./swagger"; const port = Number(process.env.PORT || 8080); -const server = addRoutes( - fastify(), +const server = fastify(); +server.register(fastifySensible); +registerSwagger(server); +registerAuth0(server); +registerCorsHandler(server); +addRoutes( + server, createPingEndpoint, (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer), (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer), - (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer), - (app: FastifyInstance) => createRegionBusinessesEndpoint(app, productionDataLayer) + (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer) ); -server.register(fastifySensible); -registerAuth0(server); -registerCorsHandler(server); - server.listen(port, '::', (err, address) => { if (err) { console.error(err); process.exitCode = 1; } else { + console.log("listening") + server.swagger(); console.log(`Server listening at ${address}`); } }); diff --git a/src/swagger.ts b/src/swagger.ts new file mode 100644 index 0000000..3a8c0e7 --- /dev/null +++ b/src/swagger.ts @@ -0,0 +1,21 @@ +import {FastifyInstance} from "fastify"; +import fastifySwagger from "fastify-swagger"; + +const swaggerSchema = { + routePrefix: '/documentation', + openapi: { + info: { + title: 'RAnLabAPI Swagger Schema', + description: 'Swagger Schema for the RAnLab API ', + version: '0.1.0' + }, + servers: [ + {url: "http://localhost:8080", description: "Local dev"}, + {url: "https://ranlab-api-mvp-xxvyt3l5wa-nn.a.run.app/", description: "Cloud dev"} + ] + }, + exposeRoute: true +}; +export function registerSwagger(app: FastifyInstance) { + app.register(fastifySwagger, swaggerSchema); +} diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index df547d5..21c2ce6 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -50,6 +50,6 @@ export async function getDummyRegions(app: FastifyInstance, token: string = dumm return await app.inject({ method: 'GET', headers: {authorization: `Bearer ${token}`}, - url: `/regions` + url: `/regions/manager/${DummyRegion.manager}` }); } diff --git a/tsoa.json b/tsoa.json new file mode 100644 index 0000000..de252fe --- /dev/null +++ b/tsoa.json @@ -0,0 +1,12 @@ +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*Controller.ts"], + "spec": { + "outputDirectory": "build", + "specVersion": 3 + }, + "routes": { + "routesDir": "build" + } +} diff --git a/yarn.lock b/yarn.lock index 94b8ca4..d125800 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1093,6 +1093,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz" @@ -1674,6 +1679,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz" @@ -1726,6 +1736,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz" @@ -1740,6 +1755,11 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" @@ -1777,6 +1797,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz" @@ -1995,6 +2020,27 @@ fastify-sensible@^3.1.0: type-is "^1.6.18" vary "^1.1.2" +fastify-static@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/fastify-static/-/fastify-static-3.4.0.tgz#812f7af416ef2e998c7d6c19ae3dd7afc8d7ca58" + integrity sha512-5y9xTNiPTj6/jDwzH6CqBIcI3/yZtocUiHoLud2NYPfHSOLlS6eW6DTheiU8b9WWlfmHfqOjwFFBdhiH1+nBhg== + dependencies: + fastify-plugin "^3.0.0" + glob "^7.1.4" + readable-stream "^3.4.0" + send "^0.17.1" + +fastify-swagger@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/fastify-swagger/-/fastify-swagger-4.0.1.tgz#b852c56942b244127493330a700e5ae661dd8b8c" + integrity sha512-K3CRNnpEUtBO+CZYGzXPgR+2dwQPqHppPRUI1Q0u2GcR89tT6wZhVOMbtxw8ks9Y6lt1IRRdV0C8ZbT+TC/yjg== + dependencies: + fastify-plugin "^3.0.0" + fastify-static "^3.3.0" + js-yaml "^4.0.0" + json-schema-resolver "^1.2.0" + openapi-types "^7.2.3" + fastify-warning@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz" @@ -2161,6 +2207,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -2395,6 +2446,17 @@ http-errors@^1.7.1, http-errors@^1.7.3: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-parser-js@>=0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz" @@ -3086,6 +3148,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz" @@ -3140,6 +3209,15 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-resolver@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/json-schema-resolver/-/json-schema-resolver-1.2.2.tgz#52aeb8dbe4ece0df82c0ac2f1642384612542270" + integrity sha512-sW4b4BDJzYiKpJind7l1JtH3P1yn43vCv3w51YR2Ixse5rXr006TL10gM0Ek54pET6vxwiWq5RQuIMgmH9YrrQ== + dependencies: + debug "^4.1.1" + rfdc "^1.1.4" + uri-js "^4.2.2" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -3476,6 +3554,11 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: dependencies: mime-db "1.44.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime@^2.2.0: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz" @@ -3516,6 +3599,11 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz" @@ -3675,6 +3763,11 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +openapi-types@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-7.2.3.tgz#83829911a3410a022f0e0cf2b0b2e67232ccf96e" + integrity sha512-olbaNxz12R27+mTyJ/ZAFEfUruauHH27AkeQHDHRq5AF0LdNkK1SSV7EourXQDK+4aX7dv2HtyirAGK06WMAsA== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz" @@ -3910,6 +4003,11 @@ quick-format-unescaped@^4.0.1: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz" integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + raw-body@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz" @@ -4161,6 +4259,25 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +send@^0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz" @@ -4380,7 +4497,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.5.0 < 2": +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= From 54e4fbc92ac688696fac967b353ecfe6c21b19a6 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 1 Feb 2021 21:48:54 -0330 Subject: [PATCH 32/75] Opening up the CORS origins a bit for testing --- src/cors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cors.ts b/src/cors.ts index a065d72..8ceb892 100644 --- a/src/cors.ts +++ b/src/cors.ts @@ -3,7 +3,7 @@ import fastifyCors from "fastify-cors"; export function registerCorsHandler(server: FastifyInstance) { server.register(fastifyCors, { - origin: [/localhost/, "https://ranlab-app-phzez.ondigitalocean.app/"], + origin: [/^https?:\/\/localhost/, /^https?:\/\/ranlab-app-phzez.ondigitalocean.app/], credentials: true, strictPreflight: true }); From 2a78a7545450569eb33a4c75885b97f94e3fd477 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 6 Feb 2021 16:21:15 -0330 Subject: [PATCH 33/75] Fixing up the auth0 tests and code. --- .env | 2 + package.json | 5 +- src/auth0.ts | 15 ++++-- src/endpoints/businesses.ts | 37 ++++++++------- src/endpoints/filters.ts | 21 ++++++--- src/endpoints/regions.ts | 72 ++++++++++++++++++----------- src/utils.ts | 7 +++ tests/auth0.test.ts | 91 +++++++++++++++++++++++++++++++++++++ yarn.lock | 69 +++++++++++++++++++++++++++- 9 files changed, 263 insertions(+), 56 deletions(-) create mode 100644 tests/auth0.test.ts diff --git a/.env b/.env index d60d5c9..a7532a2 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ AUTH0_DOMAIN=lesleychard.auth0.com AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj +AUTH0_API_CLIENT_ID=sKHX2LJBIAFvfFDQZPHRWK35eoBVaaQO +AUTH0_CLAIMS_NAMESPACE=https://mun.ca diff --git a/package.json b/package.json index 40c152b..6a78301 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "node": ">=12" }, "dependencies": { + "@auth0/auth0-spa-js": "^1.13.6", + "@types/node-fetch": "^2.5.8", "body-parser": "^1.19.0", "fastify": "^3.5.1", "fastify-authz-jwks": "^1.1.11", @@ -31,7 +33,8 @@ "fastify-sensible": "^3.1.0", "fastify-swagger": "^4.0.1", "jsonwebtoken": "^8.5.1", - "jwks-rsa": "^1.12.0" + "jwks-rsa": "^1.12.0", + "node-fetch": "^2.6.1" }, "devDependencies": { "@types/jest": "^26.0.15", diff --git a/src/auth0.ts b/src/auth0.ts index 4ce9d8a..f32a099 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -1,20 +1,27 @@ -import {FastifyInstance} from "fastify"; +import {FastifyInstance, FastifyRequest} from "fastify"; import fastifySecretProvider from 'fastify-authz-jwks'; import fastifyJwt, {FastifyJWTOptions} from 'fastify-jwt'; -export function registerAuth0(fastify: FastifyInstance) { +export function registerAuth0(fastify: FastifyInstance, tenant = process.env.AUTH0_DOMAIN) { const faSecretProvider = fastifySecretProvider({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, - jwksUri: 'http://lesleychard.auth0.com/.well-known/jwks.json' + jwksUri: `http://${tenant}/.well-known/jwks.json` }); fastify.register(fastifyJwt, { secret: faSecretProvider, audience: 'https://api.example.com', - issuer: 'https://lesleychard.auth0.com/', + issuer: `https://${tenant}/`, algorithms: ['RS256'], decode: { complete: true }, }); } + +export async function verifyJwt(request: FastifyRequest) { + let jwt = (await request.jwtVerify()); + let userId = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/userId`]; + let admin = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/admin`]; + return {userId, admin}; +} diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index d670eee..d5fcada 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -3,6 +3,8 @@ import {DataLayer, Filters} from "../database/productionDataLayer"; import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; import {createBizSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; +import {isRegionManager} from "../utils"; +import {verifyJwt} from "../auth0"; interface GetRegionBusinessRequest extends RequestGenericInterface { Params: { @@ -41,21 +43,18 @@ interface AuthToken { userId: string, admin: boolean } -async function isRegionManager(userId: string, regionId: string, dataLayer: DataLayer) { - const regions = (await dataLayer.getRegionsManagedBy(userId)); - return regions.find((r) => r.name === regionId); -} export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get( '/regions/:regionId/businesses', {schema: getBizSchema}, - async (request) => { + async (request, reply) => { - let {userId, admin} = await request.jwtVerify(); + let {userId, admin} = await verifyJwt(request) if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { - throw app.httpErrors.unauthorized("User does not have access to region"); + reply.unauthorized("User does not have access to region"); + return; } else { let response = { status: "ok", @@ -97,15 +96,21 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa app.post( '/businesses/:businessId', {schema: updateBizSchema}, - async (request) => { - let updatedBiz = {...request.body, id: request.params.businessId}; - await dataLayer.setBusiness(updatedBiz); - let response = { - status: "ok", - date: Date.now(), - business: updatedBiz - }; - return JSON.stringify(response); + async (request,reply) => { + let {admin} = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Only admin users can directly update business information"); + return; + } else { + let updatedBiz = {...request.body, id: request.params.businessId}; + await dataLayer.setBusiness(updatedBiz); + let response = { + status: "ok", + date: Date.now(), + business: updatedBiz + }; + return JSON.stringify(response); + } } ); diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index daf9e71..e6353fd 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,6 +1,7 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; import {getFilterSchema} from "./docs/filterSchemas"; +import {verifyJwt} from "../auth0"; interface GetFiltersRequest extends RequestGenericInterface { Params: { @@ -10,14 +11,20 @@ interface GetFiltersRequest extends RequestGenericInterface { export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get('/regions/:regionId/filters', {schema: getFilterSchema}, - async (request) => { - let response = { - status: "ok", - date: Date.now(), - filters: null + async (request, reply) => { + let {userId} = await verifyJwt(request); + if (!userId) { + reply.unauthorized("User not found!"); + return; + } else { + let response = { + status: "ok", + date: Date.now(), + filters: null + } + response.filters = await dataLayer.getFilters(request.params.regionId); + return JSON.stringify(response); } - response.filters = await dataLayer.getFilters(request.params.regionId); - return JSON.stringify(response); } ); diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 8656a60..97d3b92 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -7,6 +7,8 @@ import { updateRegionReqSchema, deleteRegionReqSchema } from "./docs/regionSchemas"; +import {verifyJwt} from "../auth0"; +import {isRegionManager} from "../utils"; interface GetManagedRegionsRequest extends RequestGenericInterface { Params: { @@ -41,17 +43,22 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : app.get( '/regions/manager/:managerId', {schema: getManagedRegionsReqSchema}, - async (request) => { - let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); - let response = { - status: "ok", - date: Date.now(), - regions: [] - }; - if(admin || userId == request.params.managerId) { - response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); + async (request,reply) => { + let {userId, admin} = await verifyJwt(request); + if(!userId) { + reply.send(reply.unauthorized); + return; + } else { + let response = { + status: "ok", + date: Date.now(), + regions: [] + }; + if (admin || userId == request.params.managerId) { + response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); + } + return JSON.stringify(response); } - return JSON.stringify(response); } ); @@ -59,7 +66,8 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : '/regions/:regionId', {schema: getSingleRegionReqSchema}, async(request ) => { - let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); + + let {userId, admin} = await verifyJwt(request); let response = { status: "ok", date: Date.now(), @@ -81,27 +89,39 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : '/regions', {schema: createRegionReqSchema}, async(request, reply) => { - let response = { - status: "ok", - regionId: "" - }; - response.regionId = (await dataLayer.setRegion(request.body)).id; - reply.code(201); - return JSON.stringify(response); + let {admin} = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Must be admin to create a region"); + return; + } else { + let response = { + status: "ok", + regionId: "" + }; + response.regionId = (await dataLayer.setRegion(request.body)).id; + reply.code(201); + return JSON.stringify(response); + } } ); app.post( '/regions/:regionId', {schema: updateRegionReqSchema}, - async(request) => { - let UpdatedRegion: Region = {...request.body}; - let response = { - status: "ok", - region: UpdatedRegion - }; - await dataLayer.setRegion(UpdatedRegion); - return JSON.stringify(response); + async(request, reply) => { + let {userId, admin} = await verifyJwt(request); + if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + reply.unauthorized("Only region managers and administrators can update region data") + return; + } else { + let UpdatedRegion: Region = {...request.body}; + let response = { + status: "ok", + region: UpdatedRegion + }; + await dataLayer.setRegion(UpdatedRegion); + return JSON.stringify(response); + } } ); diff --git a/src/utils.ts b/src/utils.ts index 9f1ffb1..a584f6e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from 'fastify'; +import {DataLayer} from "./database/productionDataLayer"; type EndpointFunction = (s: FastifyInstance) => FastifyInstance; @@ -12,3 +13,9 @@ type EndpointFunction = (s: FastifyInstance) => FastifyInstance; export function addRoutes(server: FastifyInstance, ...endpoints: EndpointFunction[]) { return endpoints.reduce((prev, curr) => curr(prev), server); } + +export async function isRegionManager(userId: string, regionId: string, dataLayer: DataLayer) { + const regions = (await dataLayer.getRegionsManagedBy(userId)); + return regions.find((r) => r.name === regionId); +} + diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts new file mode 100644 index 0000000..f5fc2b5 --- /dev/null +++ b/tests/auth0.test.ts @@ -0,0 +1,91 @@ +import {registerAuth0} from "../src/auth0"; +import fastify, {FastifyInstance} from "fastify"; +import {addRoutes} from "../src/utils"; +import createRegionsEndpoint from "../src/endpoints/regions"; +import {DummyDatalayer} from "./utils/testDataLayer"; +import fetch from "node-fetch"; +import {createBusinessesEndpoint} from "../src/endpoints/businesses"; +import fastifySensible from "fastify-sensible"; + +process.env.AUTH0_CLAIMS_NAMESPACE="https://mun.ca"; +process.env.TEST_AUTH0_DOMAIN= "dev-5ju75h98.us.auth0.com"; +process.env.TEST_AUTH0_CLIENT_ID = "iqwBRZwwuKGz0BkHiInTWTyqvOFLepd6"; +process.env.TEST_AUTH0_USERNAME = "liquiddark@gmail.com"; +//process.env.TEST_AUTH0_PASSWORD should be set at runtime + +describe("Auth0 integration works correctly", () => { + let sut : FastifyInstance; + beforeEach(() => { + sut = fastify(); + registerAuth0(sut, process.env.TEST_AUTH0_DOMAIN); + sut.register(fastifySensible); + addRoutes(sut, + () => createRegionsEndpoint(sut, new DummyDatalayer()), + () => createBusinessesEndpoint(sut, new DummyDatalayer()) + ); + }); + afterEach(async (done) => { + await sut.close(); + done(); + }); + it("Disallows an authenticated request", async(done) => { + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion", + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("Allows an authenticated request for regions", async(done) => { + let access_token = await authenticateToTestDomain(); + let response = await sut.inject({ + method: "GET", + url: "/regions/manager/DummyManager", + headers: {authorization: `Bearer ${access_token}` } + }); + expect(response.statusCode).toBe(200); + done(); + }); + + it("Allows an authenticated request for businesses", async(done) => { + let access_token = await authenticateToTestDomain(); + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion/businesses", + headers: {authorization: `Bearer ${access_token}` } + }); + expect(response.statusCode).toBe(200); + done(); + }); + + it("Allows an authenticated request", async(done) => { + let access_token = await authenticateToTestDomain(); + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion", + headers: {authorization: `Bearer ${access_token}` } + }); + expect(response.statusCode).toBe(200); + done(); + }); + + async function authenticateToTestDomain() { + let authResponse = await fetch(`https://${process.env.TEST_AUTH0_DOMAIN}/oauth/token`, { + method: "POST", + headers: {'content-type': 'application/json'}, + body: JSON.stringify({ + client_id: process.env.TEST_AUTH0_CLIENT_ID, + client_secret: process.env.TEST_AUTH0_CLIENT_SECRET, + username: process.env.TEST_AUTH0_USERNAME, + password: process.env.TEST_AUTH0_PASSWORD, + audience: `https://testing-ranlab.com`, + scope: 'read:sample', + grant_type: "password" + }) + }); + let response = await authResponse.json(); + let access_token = response.access_token; + return access_token; + } +}); diff --git a/yarn.lock b/yarn.lock index d125800..a8bf948 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,19 @@ # yarn lockfile v1 +"@auth0/auth0-spa-js@^1.13.6": + version "1.13.6" + resolved "https://registry.yarnpkg.com/@auth0/auth0-spa-js/-/auth0-spa-js-1.13.6.tgz#3ad50e1a60467cd35ff3887edef67399bff98dfc" + integrity sha512-o+Mb0YXrasP364j+WB5hoALoL9sn8SZC0rDFLcyks6HUnQH/vzupdoWWFAIl1+EocAFB3CGMgSc425wpHNB4pw== + dependencies: + abortcontroller-polyfill "^1.5.0" + browser-tabs-lock "1.2.9" + core-js "^3.8.0" + es-cookie "^1.3.2" + fast-text-encoding "^1.0.3" + promise-polyfill "^8.2.0" + unfetch "^4.2.0" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz" @@ -912,6 +925,14 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz" integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== +"@types/node-fetch@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" + integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": version "14.14.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz" @@ -989,6 +1010,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abortcontroller-polyfill@^1.5.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.1.tgz#27084bac87d78a7224c8ee78135d05df430c2d2f" + integrity sha512-yml9NiDEH4M4p0G4AcPkg8AAa4mF3nfYF28VQxaokpO67j9H7gWgmsVWJ/f1Rn+PzsnDYvzJzWIQzCqDKRvWlA== + abstract-logging@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.0.tgz" @@ -1342,6 +1368,11 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browser-tabs-lock@1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/browser-tabs-lock/-/browser-tabs-lock-1.2.9.tgz#fa9568471c49de1dbe311a6da95f86794bbe1fdf" + integrity sha512-cczryjv6i6kAfTWKhhNW3LWhFDwzPazEsNG9IG2n6AeYzPVb1tUCY3aqTKeFJL0rKUnfSXto7esjrqY3fz+ugA== + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz" @@ -1506,7 +1537,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -1550,6 +1581,11 @@ core-js@3.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz" integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== +core-js@^3.8.0: + version "3.8.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" + integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz" @@ -1755,6 +1791,11 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-cookie@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/es-cookie/-/es-cookie-1.3.2.tgz#80e831597f72a25721701bdcb21d990319acd831" + integrity sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -1953,7 +1994,7 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fast-text-encoding@^1.0.0: +fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== @@ -2186,6 +2227,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz" @@ -3646,6 +3696,11 @@ node-fetch@2.6.1, node-fetch@^2.3.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz" @@ -3920,6 +3975,11 @@ promise-polyfill@8.1.3: resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz" integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== +promise-polyfill@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" + integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g== + prompts@^2.0.1: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz" @@ -4775,6 +4835,11 @@ typescript@^4.0.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz" integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz" From 6ee8d7ec42f98c4d7d6c4b9093e4f03334c8c0b0 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 7 Feb 2021 02:09:46 -0330 Subject: [PATCH 34/75] Fixed a bunch of tests that broke in the last couple of updates --- src/database/productionDataLayer.ts | 142 ++++++++++++++++------------ src/endpoints/businesses.ts | 9 +- src/endpoints/regions.ts | 6 +- tests/auth0.test.ts | 115 +++++++++++++--------- tests/businesses.test.ts | 11 ++- tests/editRequests.test.ts | 2 +- tests/filters.test.ts | 13 ++- tests/regions.test.ts | 33 ++++--- tests/utils/dummyData.ts | 8 +- tests/utils/testify.ts | 24 +++-- 10 files changed, 219 insertions(+), 144 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 570f315..6dba4ef 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,20 +1,22 @@ import {Business} from "../endpoints/businesses"; import {firestore} from "./firestore"; -import firebase from "firebase"; import {AddRequest, DeleteRequest, UpdateRequest} from "../endpoints/editRequests"; +import firebase from "firebase"; export interface IdObject { id: string } +interface RegionFilters { + years?: {year: number, count: number}[] | undefined, + industries?: {industry: string, count: number}[] | undefined +} + export interface Region { id?: string | undefined, name: string, manager: string, - filters?: { - years?: { year: number, count: number }[] | undefined, - industries?: { industry: string, count: number }[] | undefined, - } + filters?: RegionFilters } export interface Filters { @@ -35,7 +37,6 @@ export interface DataLayer { createDeleteRequests(deleteRequest: DeleteRequest): Promise; } - export class ProductionDataLayer implements DataLayer { async createAddRequest(addRequest: AddRequest): Promise { let doc = firestore.collection("editRequests").doc(); @@ -72,37 +73,28 @@ export class ProductionDataLayer implements DataLayer { if(!regionDoc.exists) { throw "Bad Region"; } - let {years, industries} = this.calculateNewFilters(regionDoc, newBusinessData, 1); - + let filters = ProductionDataLayer.getCurrentFilters(regionDoc); let businessDoc = await transaction.get(businessRef); + let updates : RegionFilters = { + years: [{year: newBusinessData.year_added, count: 1}], + industries: [{industry: newBusinessData.industry, count: 1}] + }; + if(businessDoc.exists) { let existingBusinessData = businessDoc.data(); - if(!!existingBusinessData && !!existingBusinessData.year_added) { - // @ts-ignore I don't know why, but it thinks existingBusinessData could be null here, despite the enclosing if statement - let oldYearIndex = years.findIndex((y) => y.year === existingBusinessData.year_added); - if(oldYearIndex >= 0) { - if (years[oldYearIndex].count > 1) { - years[oldYearIndex].count -= 1; - } else { - years.splice(oldYearIndex, 1); - } - } - } - if (!!existingBusinessData && !!existingBusinessData.industry) { - // @ts-ignore I don't know why, but it thinks existingBusinessData could be null here, despite the enclosing if statement - let existingIndustryIndex = industries.findIndex(i => i.industry === existingBusinessData.industry); - if(existingIndustryIndex >= 0) { - if (industries[existingIndustryIndex].count > 1) { - industries[existingIndustryIndex].count -= 1; - } else { - industries.splice(existingIndustryIndex, 1); - } - } + if(!!existingBusinessData) { + // @ts-ignore no idea why it thinks updates can be undefined here. + updates.years.push( + {year: existingBusinessData.year_added, count: -1}, + ); + // @ts-ignore no idea why it thinks updates can be undefined here. + updates.industries.push( + {industry: existingBusinessData.industry, count: -1}, + ); } } - let regionUpdate = {filters: {years: years, industries: industries}}; - await transaction.update(regionRef, regionUpdate) + await transaction.update(regionRef, {filters: this.calculateNewFilters(filters, updates)}) await transaction.set(businessRef, newBusinessData); }).then(() => {id: businessRef.id}); } @@ -110,10 +102,7 @@ export class ProductionDataLayer implements DataLayer { async getFilters(region: string) : Promise{ let regionData = (await firestore.collection("regions").doc(region).get()).data(); regionData = !!regionData ? regionData : {}; - return { - years: regionData.years ? regionData.years : [], - industries: regionData.industries ? regionData.industries : [] - }; + return regionData.filters; } async getAllRegions() : Promise { @@ -144,13 +133,12 @@ export class ProductionDataLayer implements DataLayer { let businessData = businessDoc.data(); if (!!businessData) { if (!!businessData.regionId) { - let regionRef = firestore.collection("regions").doc(businessData.regionId); - let regionDoc = await transaction.get(regionRef); - let {years, industries} = this.calculateNewFilters(regionDoc, businessData, -1); - - let regionUpdate = {filters: {years: years, industries: industries}}; + let deleteBizFromFilter = { + years: [{year: businessData.year_added, count: -1}], + industries: [{industry: businessData.industry, count: -1}] + } - await transaction.update(regionRef, regionUpdate) + await this.updateRegionFilters(businessData.regionId, deleteBizFromFilter, transaction); } await transaction.delete(businessRef); } @@ -158,29 +146,65 @@ export class ProductionDataLayer implements DataLayer { ); } - private calculateNewFilters(regionDoc: firebase.firestore.DocumentSnapshot, businessData: firebase.firestore.DocumentData , change: number) { - let regionData = regionDoc.data(); - let years: { year: number, count: number }[] = !!regionData && !!regionData.years ? regionData.years : []; - let yearEntryIndex = years.findIndex(y => y.year === businessData.year_added); - if (yearEntryIndex < 0 && change > 0) { - years.push({year: businessData.year_added, count: change}); - } else if (years[yearEntryIndex].count + change > 0) { - years[yearEntryIndex].count += change; - } else { - years.splice(yearEntryIndex, 1); + private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: firebase.firestore.Transaction) { + let regionRef = firestore.collection("regions").doc(regionId); + let regionDoc = await transaction.get(regionRef); + if (!!regionDoc) { + let regionData = regionDoc.data(); + if (!!regionData) { + let updatedFilters = this.calculateNewFilters(regionData.filters, filterUpdate); + let regionUpdate = {filters: updatedFilters}; + + await transaction.update(regionRef, regionUpdate) + } } + } - let industries: { industry: string, count: number }[] = !!regionData && !!regionData.industries ? regionData.industries : []; - let industryEntryIndex = industries.findIndex(i => i.industry === businessData.industry); - if (industryEntryIndex < 0 && change > 0) { - industries.push({industry: businessData.industry, count: change}); - } else if (industries[industryEntryIndex].count + change > 0) { - industries[industryEntryIndex].count += change; - } else { - industries.splice(industryEntryIndex, 1); + private calculateNewFilters(filters: RegionFilters, updates: RegionFilters) { + let years : {year: number, count: number}[] = !!filters.years ? filters.years : []; + let industries : {industry: string, count: number}[] = !!filters.industries ? filters.industries : []; + if(!!updates.years && updates.years.length > 0) { + for(let yearUpdate of updates.years) { + let yearEntryIndex = years.findIndex(y => y.year === yearUpdate.year); + if (yearEntryIndex < 0) { + years.push({year: yearUpdate.year, count: yearUpdate.count}); + } else { + years[yearEntryIndex].count += yearUpdate.count; + } + } + for(let i = years.length-1; i >=0; i--) { + if(years[i].count <= 0) { + years.splice(i,1); + } + } + } + + if(!!updates.industries && updates.industries.length > 0) { + for(let industryUpdate of updates.industries) { + let industryEntryIndex = industries.findIndex(i => i.industry === industryUpdate.industry); + if (industryEntryIndex < 0) { + industries.push(industryUpdate); + } else { + industries[industryEntryIndex].count += industryUpdate.count; + } + for(let i = industries.length-1; i >=0; i--) { + if(industries[i].count <= 0) { + industries.splice(i,1); + } + } + } } return {years, industries}; } + + private static getCurrentFilters(regionDoc: firebase.firestore.DocumentSnapshot) : RegionFilters{ + let regionData = !!regionDoc && !!regionDoc.data() ? regionDoc.data() : {filters: {}}; + if(!!regionData) { + return !!regionData.filters ? regionData.filters : {}; + } else { + return {}; + } + } } export const productionDataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index d5fcada..253bfd0 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -39,11 +39,6 @@ export interface Business { location?: GeoPoint | null | undefined } -interface AuthToken { - userId: string, - admin: boolean -} - export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { app.get( @@ -52,7 +47,7 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa async (request, reply) => { let {userId, admin} = await verifyJwt(request) - if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { reply.unauthorized("User does not have access to region"); return; } else { @@ -76,7 +71,7 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa '/regions/:regionId/businesses', {schema: createBizSchema}, async (request) => { - let {userId, admin} = await request.jwtVerify(); + let {userId, admin} = await verifyJwt(request); if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { throw app.httpErrors.unauthorized("User does not have access to region"); } else if (!!request.body.regionId && request.body.regionId !== request.params.regionId) { diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 97d3b92..8492120 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -55,7 +55,7 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : regions: [] }; if (admin || userId == request.params.managerId) { - response.regions.push(...(await dataLayer.getRegionsManagedBy(userId))); + response.regions.push(...(await dataLayer.getRegionsManagedBy(request.params.managerId))); } return JSON.stringify(response); } @@ -129,8 +129,8 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : '/regions/:regionId', {schema: deleteRegionReqSchema}, async (request, reply) => { - let {userId} = <{userId:string}>await request.jwtVerify(); - if((await dataLayer.getRegionsManagedBy(userId)).find((r) => r.name === request.params.regionId)) { + let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); + if(admin || isRegionManager(userId, request.params.regionId, dataLayer)) { await dataLayer.deleteRegion(request.params.regionId); reply.code(204); } else { diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index f5fc2b5..8aef82d 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -6,68 +6,93 @@ import {DummyDatalayer} from "./utils/testDataLayer"; import fetch from "node-fetch"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import fastifySensible from "fastify-sensible"; +import {setupAuth0TestEnv} from "./utils/testify"; -process.env.AUTH0_CLAIMS_NAMESPACE="https://mun.ca"; -process.env.TEST_AUTH0_DOMAIN= "dev-5ju75h98.us.auth0.com"; -process.env.TEST_AUTH0_CLIENT_ID = "iqwBRZwwuKGz0BkHiInTWTyqvOFLepd6"; -process.env.TEST_AUTH0_USERNAME = "liquiddark@gmail.com"; -//process.env.TEST_AUTH0_PASSWORD should be set at runtime - -describe("Auth0 integration works correctly", () => { +describe("Auth0 integration tests", () => { let sut : FastifyInstance; + let access_token : string; + + beforeAll(async (done) => { + setupAuth0TestEnv() + await authenticateToTestDomain(); + done(); + }); + beforeEach(() => { sut = fastify(); registerAuth0(sut, process.env.TEST_AUTH0_DOMAIN); + + // fastifySensible gives us the decorator function needed when rejecting unauthorized reqs sut.register(fastifySensible); - addRoutes(sut, - () => createRegionsEndpoint(sut, new DummyDatalayer()), - () => createBusinessesEndpoint(sut, new DummyDatalayer()) - ); }); + afterEach(async (done) => { await sut.close(); done(); }); - it("Disallows an authenticated request", async(done) => { - let response = await sut.inject({ - method: "GET", - url: "/regions/DummyRegion", + + describe("Region Tests", () => { + beforeEach(() => { + addRoutes(sut, + () => createRegionsEndpoint(sut, new DummyDatalayer()), + ); }); - expect(response.statusCode).toBe(401); - done(); - }); - it("Allows an authenticated request for regions", async(done) => { - let access_token = await authenticateToTestDomain(); - let response = await sut.inject({ - method: "GET", - url: "/regions/manager/DummyManager", - headers: {authorization: `Bearer ${access_token}` } + it("Disallows an unauthenticated request", async(done) => { + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion", + }); + expect(response.statusCode).toBe(401); + done(); }); - expect(response.statusCode).toBe(200); - done(); - }); - it("Allows an authenticated request for businesses", async(done) => { - let access_token = await authenticateToTestDomain(); - let response = await sut.inject({ - method: "GET", - url: "/regions/DummyRegion/businesses", - headers: {authorization: `Bearer ${access_token}` } + it("Allows an authenticated request for regions by manager", async(done) => { + let response = await sut.inject({ + method: "GET", + url: "/regions/manager/DummyManager", + headers: {authorization: `Bearer ${access_token}` } + }); + expect(response.statusCode).toBe(200); + done(); + }); + + it("Allows an authenticated request for a region by ID", async(done) => { + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion", + headers: {authorization: `Bearer ${access_token}` } + }); + expect(response.statusCode).toBe(200); + done(); }); - expect(response.statusCode).toBe(200); - done(); }); - it("Allows an authenticated request", async(done) => { - let access_token = await authenticateToTestDomain(); - let response = await sut.inject({ - method: "GET", - url: "/regions/DummyRegion", - headers: {authorization: `Bearer ${access_token}` } + describe("Business Tests", () => { + beforeEach(() => { + addRoutes(sut, + () => createBusinessesEndpoint(sut, new DummyDatalayer()), + ); + }); + + it("Disallows an unauthenticated request", async(done) => { + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion/businesses", + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("Allows an authenticated request for businesses", async (done) => { + let response = await sut.inject({ + method: "GET", + url: "/regions/DummyRegion/businesses", + headers: {authorization: `Bearer ${access_token}`} + }); + expect(response.statusCode).toBe(200); + done(); }); - expect(response.statusCode).toBe(200); - done(); }); async function authenticateToTestDomain() { @@ -84,8 +109,6 @@ describe("Auth0 integration works correctly", () => { grant_type: "password" }) }); - let response = await authResponse.json(); - let access_token = response.access_token; - return access_token; + access_token = (await authResponse.json()).access_token; } }); diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index a38c468..0b52c37 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -3,15 +3,19 @@ import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import { createDummyBusiness, createDummyRegion, - DummyBiz, + DummyBiz, DummyRegion, getDummyBusinesses } from "./utils/dummyData"; -import { MockAuth0Return, testify} from "./utils/testify"; +import {getMockToken, MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; import createRegionsEndpoint from "../src/endpoints/regions"; describe("Business Endpoint Tests", () => { let testDataLayer: DummyDatalayer; + beforeAll(() => { + setupAuth0TestEnv(); + }); + beforeEach(async (done) => { testDataLayer = new DummyDatalayer(); const server = testify(new MockAuth0Return()); @@ -44,7 +48,8 @@ describe("Business Endpoint Tests", () => { const updateResponse = await bizApp.inject({ method: 'POST', url: `/businesses/${bizId}`, - payload: updatedBiz + payload: updatedBiz, + headers:{authorization: `Bearer ${getMockToken({userId: DummyRegion.manager, admin: false})}`} }); expect(updateResponse.statusCode).toBe(200); expect(JSON.parse(updateResponse.payload).business).toEqual(updatedBiz); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 7882191..4ec5298 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -2,7 +2,7 @@ import {DummyDatalayer} from "./utils/testDataLayer"; import {getMockToken, MockAuth0Return, testify} from "./utils/testify"; import {createEditEndpoint} from "../src/endpoints/editRequests"; -describe("Edit Request tests", () => { +describe("Edit Request unit tests", () => { let testDataLayer: DummyDatalayer it("Submitted edit requests are seen by region admin", async (done) => { diff --git a/tests/filters.test.ts b/tests/filters.test.ts index 694c794..d2cf6eb 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,15 +1,15 @@ -import {fastify} from "fastify"; import {createFiltersEndpoint} from "../src/endpoints/filters"; import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; import {DummyDatalayer} from "./utils/testDataLayer"; import {createDummyBusiness, createDummyRegion, DummyBiz, DummyRegion} from "./utils/dummyData"; import createRegionsEndpoint from "../src/endpoints/regions"; -import {MockAuth0Return, testify} from "./utils/testify"; +import {getMockToken, MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; beforeAll(async (done) => { + setupAuth0TestEnv(); testDataLayer = new DummyDatalayer(); const server = testify(new MockAuth0Return()); const regionsApp = createRegionsEndpoint(server, testDataLayer); @@ -28,8 +28,13 @@ describe("Filter Endpoint Tests", () => { }); it('Returns only the region-specific filter data', async (done) => { - const filterApp = createFiltersEndpoint(fastify(), testDataLayer); - const filterResponse = await filterApp.inject({ method: 'GET', url: `/regions/${DummyRegion.name}/filters` }); + const server = testify(new MockAuth0Return()); + const filterApp = createFiltersEndpoint(server, testDataLayer); + const filterResponse = await filterApp.inject({ + method: 'GET', + url: `/regions/${DummyRegion.name}/filters`, + headers: { authorization: `Bearer ${getMockToken({userId:DummyRegion.manager, admin: false})}`} + }); expect(filterResponse.statusCode).toBe(200); expect(JSON.parse(filterResponse.payload).filters).toStrictEqual( diff --git a/tests/regions.test.ts b/tests/regions.test.ts index 50e1cea..adabcd0 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,16 +1,19 @@ import createRegionsEndpoint from "../src/endpoints/regions"; -import {testify, getMockToken, MockAuth0Return} from "./utils/testify"; +import {testify, getMockToken, MockAuth0Return, setupAuth0TestEnv} from "./utils/testify"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {dummyAdminToken, DummyRegion, dummyToken, getDummyRegions} from "./utils/dummyData"; +import {dummyAdminToken, DummyRegion, dummyToken, getRegionsByDummyManager} from "./utils/dummyData"; import {Region} from "../src/database/productionDataLayer"; import {FastifyInstance} from "fastify"; - describe("Region Endpoint Tests", () => { let testDataLayer: DummyDatalayer; let mockAuth0Return: MockAuth0Return; let testApp: FastifyInstance; + beforeAll(() => { + setupAuth0TestEnv(); + }); + beforeEach(async (done) => { testDataLayer= new DummyDatalayer() mockAuth0Return = new MockAuth0Return(); @@ -24,11 +27,18 @@ describe("Region Endpoint Tests", () => { let testRegions: Region[] = [{name: "region1", manager: "manager1"}, {name: "region2", manager: "manager2"}]; testRegions.forEach((r) => testDataLayer.setRegion(r)); mockAuth0Return.userId = "DummyUser"; - const response = await getDummyRegions(app, getMockToken({userId: 'admin', admin: true})); - - expect(mockAuth0Return.callCount).toBe(1); - expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions, DummyRegion])); + let authCalls = 0; + for(let region of testRegions) { + const response = await app.inject({ + method: 'GET', + headers: {authorization: `Bearer ${getMockToken({userId: "admin", "admin": true})}`}, + url: `/regions/manager/${region.manager}` + }); + authCalls++; + expect(mockAuth0Return.callCount).toBe(authCalls); + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions.filter(r => r.manager == region.manager)])); + } await app.close(); done(); }); @@ -36,7 +46,7 @@ describe("Region Endpoint Tests", () => { it('Can create and retrieve a region as Region Manager', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer); mockAuth0Return.userId = "DummyUser"; - const response = await getDummyRegions(app); + const response = await getRegionsByDummyManager(app); expect(mockAuth0Return.callCount).toBe(1); expect(response.statusCode).toBe(200); @@ -54,7 +64,8 @@ describe("Region Endpoint Tests", () => { const response = await app.inject( { method: 'POST', url: `/regions/${DummyRegion.name}`, - payload: updatedRegion + payload: updatedRegion, + headers: {authorization: `Bearer ${getMockToken({userId: DummyRegion.manager, "admin": false})}`}, }); expect(response.statusCode).toBe(200); @@ -73,7 +84,7 @@ describe("Region Endpoint Tests", () => { expect(deleteResponse.statusCode).toBe(204); - const getResponse = await getDummyRegions(app); + const getResponse = await getRegionsByDummyManager(app); expect(JSON.parse(getResponse.payload).regions).toEqual([]); await app.close(); diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index 21c2ce6..9822baa 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -4,8 +4,8 @@ import {Region} from "../../src/database/productionDataLayer"; import {getMockToken} from "./testify"; const dummyManager = "DummyManagerId"; -export const dummyToken = getMockToken({userId: dummyManager}) -export const dummyAdminToken = getMockToken({userId: "", admin: true}); +export const dummyToken = getMockToken({userId: dummyManager, admin: false}) +export const dummyAdminToken = getMockToken({userId: "admin", admin: true}); export const DummyRegion: Region = { name: "DummyRegion", @@ -21,7 +21,7 @@ export const DummyBiz: Business = { }; export async function createDummyRegion(regionsApp: FastifyInstance) { - await regionsApp.inject({ + return await regionsApp.inject({ method: "POST", url: "/regions", payload: DummyRegion, @@ -46,7 +46,7 @@ export async function getDummyBusinesses(bizApp: FastifyInstance, token: string }); } -export async function getDummyRegions(app: FastifyInstance, token: string = dummyToken) { +export async function getRegionsByDummyManager(app: FastifyInstance, token: string = dummyToken) { return await app.inject({ method: 'GET', headers: {authorization: `Bearer ${token}`}, diff --git a/tests/utils/testify.ts b/tests/utils/testify.ts index 900c67f..84c6f26 100644 --- a/tests/utils/testify.ts +++ b/tests/utils/testify.ts @@ -3,18 +3,30 @@ import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; import jwt from "jsonwebtoken"; import fastifySensible from "fastify-sensible"; +export const AUTH0_CLAIMS_NAMESPACE = "https://mun.ca"; export const mockSecret = 'dummy'; + export class MockAuth0Return { userId: string|null = null; + admin: boolean = false; callCount: number = 0; - reset() { - this.callCount = 0; - this.userId = null - } } -export function getMockToken(payload: object) { - return jwt.sign(payload, mockSecret) +export function setupAuth0TestEnv() { + process.env.AUTH0_CLAIMS_NAMESPACE = AUTH0_CLAIMS_NAMESPACE; + process.env.TEST_AUTH0_DOMAIN = "dev-5ju75h98.us.auth0.com"; + process.env.TEST_AUTH0_CLIENT_ID = "iqwBRZwwuKGz0BkHiInTWTyqvOFLepd6"; + process.env.TEST_AUTH0_USERNAME = "liquiddark@gmail.com"; + +// process.env.TEST_AUTH0_PASSWORD must be set in local runtime environment +// process.env.TEST_AUTH0_CLIENT_SECRET must be set in local runtime environment +}; + +export function getMockToken(payload: {userId: string, admin: boolean }) { + let token : any = {}; + token[`${AUTH0_CLAIMS_NAMESPACE}/userId`] = payload.userId; + token[`${AUTH0_CLAIMS_NAMESPACE}/admin`] = `${payload.admin}`; + return jwt.sign(token, mockSecret) } export const testify = (mockAuth0Return : MockAuth0Return) => { From 538efe85d831fcb4466c629d718fdb69d0adfe4d Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 8 Feb 2021 13:34:56 -0330 Subject: [PATCH 35/75] Changes based on conversation with Lesley over the weekend: 1. user id is the second half of the "sub" claim 2. claims namespace changed 3. admin looks the same, but the rule on the Auth0 tenant is slightly different now (may need further adjustment on api server, we'll see) 4. Fixed some tests while I was at it. --- .env | 2 +- package.json | 1 + src/auth0.ts | 4 ++-- tests/auth0.test.ts | 48 +++++++++++++++++++++++++++++++++------- tests/businesses.test.ts | 7 +++--- tests/dataLayer.test.ts | 2 +- tests/filters.test.ts | 12 +++++++--- tests/regions.test.ts | 12 +++++----- tests/utils/dummyData.ts | 33 ++++++++++++++++++--------- tests/utils/testify.ts | 8 ++++--- yarn.lock | 12 +++++----- 11 files changed, 97 insertions(+), 44 deletions(-) diff --git a/.env b/.env index a7532a2..022420b 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ AUTH0_DOMAIN=lesleychard.auth0.com AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj AUTH0_API_CLIENT_ID=sKHX2LJBIAFvfFDQZPHRWK35eoBVaaQO -AUTH0_CLAIMS_NAMESPACE=https://mun.ca +AUTH0_CLAIMS_NAMESPACE=https://lesleychard diff --git a/package.json b/package.json index 6a78301..1994f23 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "fastify-swagger": "^4.0.1", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0", + "jwt-decode": "^3.1.2", "node-fetch": "^2.6.1" }, "devDependencies": { diff --git a/src/auth0.ts b/src/auth0.ts index f32a099..5740c78 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -21,7 +21,7 @@ export function registerAuth0(fastify: FastifyInstance, tenant = process.env.AUT export async function verifyJwt(request: FastifyRequest) { let jwt = (await request.jwtVerify()); - let userId = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/userId`]; - let admin = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/admin`]; + let userId = jwt[`sub`].split("|")[1]; + let admin = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/admin`]; return {userId, admin}; } diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index 8aef82d..d4cede1 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -7,10 +7,14 @@ import fetch from "node-fetch"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import fastifySensible from "fastify-sensible"; import {setupAuth0TestEnv} from "./utils/testify"; +import jwtDecode from "jwt-decode"; +import {DummyRegion} from "./utils/dummyData"; +import {DataLayer} from "../src/database/productionDataLayer"; describe("Auth0 integration tests", () => { let sut : FastifyInstance; - let access_token : string; + let userAccessToken : string; + let adminAccessToken: string; beforeAll(async (done) => { setupAuth0TestEnv() @@ -51,7 +55,7 @@ describe("Auth0 integration tests", () => { let response = await sut.inject({ method: "GET", url: "/regions/manager/DummyManager", - headers: {authorization: `Bearer ${access_token}` } + headers: {authorization: `Bearer ${userAccessToken}` } }); expect(response.statusCode).toBe(200); done(); @@ -61,7 +65,7 @@ describe("Auth0 integration tests", () => { let response = await sut.inject({ method: "GET", url: "/regions/DummyRegion", - headers: {authorization: `Bearer ${access_token}` } + headers: {authorization: `Bearer ${userAccessToken}` } }); expect(response.statusCode).toBe(200); done(); @@ -69,9 +73,12 @@ describe("Auth0 integration tests", () => { }); describe("Business Tests", () => { + let testDataLayer: DataLayer; beforeEach(() => { + testDataLayer = new DummyDatalayer(); addRoutes(sut, - () => createBusinessesEndpoint(sut, new DummyDatalayer()), + () => createBusinessesEndpoint(sut, testDataLayer), + () => createRegionsEndpoint(sut, testDataLayer) ); }); @@ -85,10 +92,21 @@ describe("Auth0 integration tests", () => { }); it("Allows an authenticated request for businesses", async (done) => { + let userId = jwtDecode(userAccessToken).sub.toString().split("|")[1]; + let authRegion = {...DummyRegion}; + authRegion.manager = userId + let regionsResponse = await sut.inject({ + method: 'POST', + url: `/regions`, + payload: authRegion, + headers:{authorization: `Bearer ${adminAccessToken}`} + }); + console.log(regionsResponse); + let response = await sut.inject({ method: "GET", - url: "/regions/DummyRegion/businesses", - headers: {authorization: `Bearer ${access_token}`} + url: `/regions/${DummyRegion.name}/businesses`, + headers: {authorization: `Bearer ${userAccessToken}`} }); expect(response.statusCode).toBe(200); done(); @@ -96,7 +114,7 @@ describe("Auth0 integration tests", () => { }); async function authenticateToTestDomain() { - let authResponse = await fetch(`https://${process.env.TEST_AUTH0_DOMAIN}/oauth/token`, { + let userResponse = await fetch(`https://${process.env.TEST_AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: {'content-type': 'application/json'}, body: JSON.stringify({ @@ -109,6 +127,20 @@ describe("Auth0 integration tests", () => { grant_type: "password" }) }); - access_token = (await authResponse.json()).access_token; + userAccessToken = (await userResponse.json()).access_token; + let adminResponse = await fetch(`https://${process.env.TEST_AUTH0_DOMAIN}/oauth/token`, { + method: "POST", + headers: {'content-type': 'application/json'}, + body: JSON.stringify({ + client_id: process.env.TEST_AUTH0_CLIENT_ID, + client_secret: process.env.TEST_AUTH0_CLIENT_SECRET, + username: process.env.TEST_AUTH0_ADMIN_USERNAME, + password: process.env.TEST_AUTH0_ADMIN_PASSWORD, + audience: `https://testing-ranlab.com`, + scope: 'read:sample', + grant_type: "password" + }) + }); + adminAccessToken = (await adminResponse.json()).access_token; } }); diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index 0b52c37..b4f8b06 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -3,10 +3,11 @@ import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import { createDummyBusiness, createDummyRegion, - DummyBiz, DummyRegion, + dummyAdminToken, + DummyBiz, getDummyBusinesses } from "./utils/dummyData"; -import {getMockToken, MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; +import {MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; import createRegionsEndpoint from "../src/endpoints/regions"; describe("Business Endpoint Tests", () => { @@ -49,7 +50,7 @@ describe("Business Endpoint Tests", () => { method: 'POST', url: `/businesses/${bizId}`, payload: updatedBiz, - headers:{authorization: `Bearer ${getMockToken({userId: DummyRegion.manager, admin: false})}`} + headers:{authorization: `Bearer ${dummyAdminToken}`} }); expect(updateResponse.statusCode).toBe(200); expect(JSON.parse(updateResponse.payload).business).toEqual(updatedBiz); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index f5d5646..1058ff4 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -3,7 +3,7 @@ import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; import objectContaining = jasmine.objectContaining; -describe("Production Data Layer Tests", () => { +describe("Production Data Layer Integration Tests", () => { async function deleteRegionsNamed(regionName: string) { (await firestore.collection("regions").where("name", "==", regionName).get()).docs .forEach((doc) => doc.ref.delete()); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index d2cf6eb..ce77f00 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,9 +1,15 @@ import {createFiltersEndpoint} from "../src/endpoints/filters"; import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {createDummyBusiness, createDummyRegion, DummyBiz, DummyRegion} from "./utils/dummyData"; +import { + createDummyBusiness, + createDummyRegion, + DummyBiz, + DummyRegion, + dummyRegionManagerToken +} from "./utils/dummyData"; import createRegionsEndpoint from "../src/endpoints/regions"; -import {getMockToken, MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; +import { MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; @@ -33,7 +39,7 @@ describe("Filter Endpoint Tests", () => { const filterResponse = await filterApp.inject({ method: 'GET', url: `/regions/${DummyRegion.name}/filters`, - headers: { authorization: `Bearer ${getMockToken({userId:DummyRegion.manager, admin: false})}`} + headers: { authorization: `Bearer ${dummyRegionManagerToken}`} }); expect(filterResponse.statusCode).toBe(200); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index adabcd0..dc3ea19 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,7 +1,7 @@ import createRegionsEndpoint from "../src/endpoints/regions"; -import {testify, getMockToken, MockAuth0Return, setupAuth0TestEnv} from "./utils/testify"; +import {testify, MockAuth0Return, setupAuth0TestEnv} from "./utils/testify"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {dummyAdminToken, DummyRegion, dummyToken, getRegionsByDummyManager} from "./utils/dummyData"; +import {dummyAdminToken, DummyRegion, dummyRegionManagerToken, getRegionsByDummyManager} from "./utils/dummyData"; import {Region} from "../src/database/productionDataLayer"; import {FastifyInstance} from "fastify"; @@ -31,7 +31,7 @@ describe("Region Endpoint Tests", () => { for(let region of testRegions) { const response = await app.inject({ method: 'GET', - headers: {authorization: `Bearer ${getMockToken({userId: "admin", "admin": true})}`}, + headers: {authorization: `Bearer ${dummyAdminToken}`}, url: `/regions/manager/${region.manager}` }); authCalls++; @@ -65,7 +65,7 @@ describe("Region Endpoint Tests", () => { method: 'POST', url: `/regions/${DummyRegion.name}`, payload: updatedRegion, - headers: {authorization: `Bearer ${getMockToken({userId: DummyRegion.manager, "admin": false})}`}, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`}, }); expect(response.statusCode).toBe(200); @@ -79,7 +79,7 @@ describe("Region Endpoint Tests", () => { const deleteResponse = await app.inject( { method: 'DELETE', url: `/regions/${DummyRegion.name}`, - headers: {authorization: `Bearer ${dummyToken}`} + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} }); expect(deleteResponse.statusCode).toBe(204); @@ -96,7 +96,7 @@ describe("Region Endpoint Tests", () => { const getRegionAdminResponse = await app.inject({ method: 'GET', url: `/regions/${DummyRegion.name}`, - headers: {authorization: `Bearer ${dummyToken}`} + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} }); expect(getRegionAdminResponse.statusCode).toBe(200); diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index 9822baa..cd46572 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -2,9 +2,10 @@ import {FastifyInstance} from "fastify"; import {Business} from "../../src/endpoints/businesses"; import {Region} from "../../src/database/productionDataLayer"; import {getMockToken} from "./testify"; +import jwtDecode from "jwt-decode"; const dummyManager = "DummyManagerId"; -export const dummyToken = getMockToken({userId: dummyManager, admin: false}) +export const dummyRegionManagerToken = getMockToken({userId: dummyManager, admin: false}) export const dummyAdminToken = getMockToken({userId: "admin", admin: true}); export const DummyRegion: Region = { @@ -20,16 +21,26 @@ export const DummyBiz: Business = { industry: "DummyIndustry" }; -export async function createDummyRegion(regionsApp: FastifyInstance) { - return await regionsApp.inject({ - method: "POST", - url: "/regions", - payload: DummyRegion, - headers: {authorization: `Bearer ${dummyToken}`} - }); +export async function createDummyRegion(regionsApp: FastifyInstance, manager: string = dummyManager) { + let region = {...DummyRegion}; + region.manager = manager + let temp = jwtDecode(dummyAdminToken); + console.log(temp); + try { + let response = await regionsApp.inject({ + method: "POST", + url: "/regions", + payload: region, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + return response; + } catch (e) { + console.log(e); + return null; + } } -export async function createDummyBusiness(bizApp: FastifyInstance, biz : Business = DummyBiz, token: string = dummyToken) { +export async function createDummyBusiness(bizApp: FastifyInstance, biz : Business = DummyBiz, token: string = dummyRegionManagerToken) { return await bizApp.inject({ method: 'POST', url: `/regions/${biz.regionId}/businesses`, @@ -38,7 +49,7 @@ export async function createDummyBusiness(bizApp: FastifyInstance, biz : Busines }); } -export async function getDummyBusinesses(bizApp: FastifyInstance, token: string = dummyToken) { +export async function getDummyBusinesses(bizApp: FastifyInstance, token: string = dummyRegionManagerToken) { return await bizApp.inject({ method: 'GET', url: `/regions/${DummyBiz.regionId}/businesses`, @@ -46,7 +57,7 @@ export async function getDummyBusinesses(bizApp: FastifyInstance, token: string }); } -export async function getRegionsByDummyManager(app: FastifyInstance, token: string = dummyToken) { +export async function getRegionsByDummyManager(app: FastifyInstance, token: string = dummyRegionManagerToken) { return await app.inject({ method: 'GET', headers: {authorization: `Bearer ${token}`}, diff --git a/tests/utils/testify.ts b/tests/utils/testify.ts index 84c6f26..b5e9bb6 100644 --- a/tests/utils/testify.ts +++ b/tests/utils/testify.ts @@ -17,15 +17,17 @@ export function setupAuth0TestEnv() { process.env.TEST_AUTH0_DOMAIN = "dev-5ju75h98.us.auth0.com"; process.env.TEST_AUTH0_CLIENT_ID = "iqwBRZwwuKGz0BkHiInTWTyqvOFLepd6"; process.env.TEST_AUTH0_USERNAME = "liquiddark@gmail.com"; + process.env.TEST_AUTH0_ADMIN_USERNAME = "burton.technical.709@gmail.com"; // process.env.TEST_AUTH0_PASSWORD must be set in local runtime environment +// process.env.TEST_AUTH0_ADMIN_PASSWORD must be set in local runtime environment // process.env.TEST_AUTH0_CLIENT_SECRET must be set in local runtime environment -}; +} export function getMockToken(payload: {userId: string, admin: boolean }) { let token : any = {}; - token[`${AUTH0_CLAIMS_NAMESPACE}/userId`] = payload.userId; - token[`${AUTH0_CLAIMS_NAMESPACE}/admin`] = `${payload.admin}`; + token['sub'] = `auth0|${payload.userId}`; + token[`${AUTH0_CLAIMS_NAMESPACE}/admin`] = payload.admin; return jwt.sign(token, mockSecret) } diff --git a/yarn.lock b/yarn.lock index a8bf948..b263006 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3366,6 +3366,11 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz" @@ -3691,12 +3696,7 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@2.6.1, node-fetch@^2.3.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-fetch@^2.6.1: +node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== From 95e6c5434d6d3ca44a98480159f3714102b54ef4 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 12 Feb 2021 19:30:40 -0330 Subject: [PATCH 36/75] Temporarily disabling auth0 --- src/auth0.ts | 19 ++++++++++--------- tests/regions.test.ts | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/auth0.ts b/src/auth0.ts index 5740c78..1ebc142 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -1,9 +1,9 @@ -import {FastifyInstance, FastifyRequest} from "fastify"; -import fastifySecretProvider from 'fastify-authz-jwks'; -import fastifyJwt, {FastifyJWTOptions} from 'fastify-jwt'; +import {/*FastifyInstance, */ FastifyRequest} from "fastify"; +/*import fastifySecretProvider from 'fastify-authz-jwks'; +import fastifyJwt, {FastifyJWTOptions} from 'fastify-jwt';*/ -export function registerAuth0(fastify: FastifyInstance, tenant = process.env.AUTH0_DOMAIN) { - const faSecretProvider = fastifySecretProvider({ +export function registerAuth0(/*fastify: FastifyInstance, tenant = process.env.AUTH0_DOMAIN*/) { +/* const faSecretProvider = fastifySecretProvider({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, @@ -16,12 +16,13 @@ export function registerAuth0(fastify: FastifyInstance, tenant = process.env.AUT issuer: `https://${tenant}/`, algorithms: ['RS256'], decode: { complete: true }, - }); + });*/ } -export async function verifyJwt(request: FastifyRequest) { - let jwt = (await request.jwtVerify()); +export async function verifyJwt(_/*request*/: FastifyRequest) { + return {userId: "dummy", admin: true}; +/* let jwt = (await request.jwtVerify()); let userId = jwt[`sub`].split("|")[1]; let admin = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/admin`]; - return {userId, admin}; + return {userId, admin};*/ } diff --git a/tests/regions.test.ts b/tests/regions.test.ts index dc3ea19..da13f48 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -27,15 +27,15 @@ describe("Region Endpoint Tests", () => { let testRegions: Region[] = [{name: "region1", manager: "manager1"}, {name: "region2", manager: "manager2"}]; testRegions.forEach((r) => testDataLayer.setRegion(r)); mockAuth0Return.userId = "DummyUser"; - let authCalls = 0; +// let authCalls = 0; for(let region of testRegions) { const response = await app.inject({ method: 'GET', headers: {authorization: `Bearer ${dummyAdminToken}`}, url: `/regions/manager/${region.manager}` }); - authCalls++; - expect(mockAuth0Return.callCount).toBe(authCalls); +// authCalls++; +// expect(mockAuth0Return.callCount).toBe(authCalls); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions.filter(r => r.manager == region.manager)])); } @@ -48,7 +48,7 @@ describe("Region Endpoint Tests", () => { mockAuth0Return.userId = "DummyUser"; const response = await getRegionsByDummyManager(app); - expect(mockAuth0Return.callCount).toBe(1); +// expect(mockAuth0Return.callCount).toBe(1); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); await app.close(); From 7cde9a7b552b272927e2970050392ea43fdd5ec4 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 12 Feb 2021 19:37:26 -0330 Subject: [PATCH 37/75] Fixing syntax errors from previous commit --- src/index.ts | 2 +- tests/auth0.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 435ea44..cc011da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ const port = Number(process.env.PORT || 8080); const server = fastify(); server.register(fastifySensible); registerSwagger(server); -registerAuth0(server); +registerAuth0(/*server*/); registerCorsHandler(server); addRoutes( server, diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index d4cede1..517ad18 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -24,7 +24,7 @@ describe("Auth0 integration tests", () => { beforeEach(() => { sut = fastify(); - registerAuth0(sut, process.env.TEST_AUTH0_DOMAIN); + registerAuth0(/*sut, process.env.TEST_AUTH0_DOMAIN*/); // fastifySensible gives us the decorator function needed when rejecting unauthorized reqs sut.register(fastifySensible); From f026530a47ba340942414437f88b742979dfa40f Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 13 Feb 2021 15:20:57 -0330 Subject: [PATCH 38/75] Revert "Temporarily disabling auth0" This reverts commit 95e6c543 --- tests/regions.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/regions.test.ts b/tests/regions.test.ts index da13f48..dc3ea19 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -27,15 +27,15 @@ describe("Region Endpoint Tests", () => { let testRegions: Region[] = [{name: "region1", manager: "manager1"}, {name: "region2", manager: "manager2"}]; testRegions.forEach((r) => testDataLayer.setRegion(r)); mockAuth0Return.userId = "DummyUser"; -// let authCalls = 0; + let authCalls = 0; for(let region of testRegions) { const response = await app.inject({ method: 'GET', headers: {authorization: `Bearer ${dummyAdminToken}`}, url: `/regions/manager/${region.manager}` }); -// authCalls++; -// expect(mockAuth0Return.callCount).toBe(authCalls); + authCalls++; + expect(mockAuth0Return.callCount).toBe(authCalls); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions.filter(r => r.manager == region.manager)])); } @@ -48,7 +48,7 @@ describe("Region Endpoint Tests", () => { mockAuth0Return.userId = "DummyUser"; const response = await getRegionsByDummyManager(app); -// expect(mockAuth0Return.callCount).toBe(1); + expect(mockAuth0Return.callCount).toBe(1); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); await app.close(); From 6fe5f77f8b8125dd213f33c49addaecc3a1a9452 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 14 Feb 2021 19:09:00 -0330 Subject: [PATCH 39/75] Modified Auth0 configuration to work with opaque user access tokens and the Management API instead of JWT user access tokens --- .env | 1 + package.json | 1 + src/auth0.ts | 90 ++++++++++++++++++------ src/declarations/fastify-authz-jwks.d.ts | 7 -- src/endpoints/businesses.ts | 12 ++-- src/endpoints/editRequests.ts | 4 +- src/endpoints/filters.ts | 8 +-- src/endpoints/regions.ts | 23 +++--- src/index.ts | 9 ++- tests/auth0.test.ts | 42 +++++------ tests/businesses.test.ts | 15 ++-- tests/cors.test.ts | 6 +- tests/editRequests.test.ts | 11 ++- tests/filters.test.ts | 15 ++-- tests/regions.test.ts | 33 ++++----- tests/utils/dummyData.ts | 22 ++++-- tests/utils/testify.ts | 25 ++++--- yarn.lock | 40 ++++++++++- 18 files changed, 226 insertions(+), 138 deletions(-) delete mode 100644 src/declarations/fastify-authz-jwks.d.ts diff --git a/.env b/.env index 022420b..6e16e57 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ AUTH0_DOMAIN=lesleychard.auth0.com AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj AUTH0_API_CLIENT_ID=sKHX2LJBIAFvfFDQZPHRWK35eoBVaaQO +AUTH0_MGMT_API_ID= AUTH0_CLAIMS_NAMESPACE=https://lesleychard diff --git a/package.json b/package.json index 1994f23..f4a0f22 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/node-fetch": "^2.5.8", "body-parser": "^1.19.0", "fastify": "^3.5.1", + "fastify-auth0-verify": "^0.4.2", "fastify-authz-jwks": "^1.1.11", "fastify-cors": "^5.1.0", "fastify-jwt": "^2.1.3", diff --git a/src/auth0.ts b/src/auth0.ts index 1ebc142..a5222b5 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -1,28 +1,74 @@ -import {/*FastifyInstance, */ FastifyRequest} from "fastify"; -/*import fastifySecretProvider from 'fastify-authz-jwks'; -import fastifyJwt, {FastifyJWTOptions} from 'fastify-jwt';*/ +import { FastifyRequest} from "fastify"; +import fetch from "node-fetch"; -export function registerAuth0(/*fastify: FastifyInstance, tenant = process.env.AUTH0_DOMAIN*/) { -/* const faSecretProvider = fastifySecretProvider({ - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: `http://${tenant}/.well-known/jwks.json` +let moduleAdminToken: string; + +async function getAdminToken(refresh : boolean) { + if(refresh || !moduleAdminToken) { + let mgmtDomain = process.env.AUTH0_DOMAIN; + + let refreshResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { + "method": "post", + "headers": { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "grant_type": "client_credentials", + "client_id": process.env.AUTH0_MGMT_CLIENT_ID, + "client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET, + "audience": `https://${mgmtDomain}/api/v2/` + }) + }); + let refreshJson = await refreshResponse.json(); + moduleAdminToken = refreshJson.access_token + } + return moduleAdminToken; +} + +async function getUserRole(userId: string, refresh = false) : Promise<{role: string}> { + let mgmtDomain = process.env.AUTH0_DOMAIN; + let adminToken = await getAdminToken(refresh); + let url = `https://${mgmtDomain}/api/v2/users/${userId.replace("|", "%7C")}`; + let metaResponse = await fetch(url, { + "headers": { + "Authorization": `Bearer ${adminToken}`, + } }); + if(metaResponse.status === 200) { + let metadata = await metaResponse.json(); + return {role: metadata.app_metadata.role}; + } else if (!refresh && metaResponse.status === 401) { + return await getUserRole(userId, true); + } else { + throw new Error(JSON.stringify(metaResponse)); + } +} - fastify.register(fastifyJwt, { - secret: faSecretProvider, - audience: 'https://api.example.com', - issuer: `https://${tenant}/`, - algorithms: ['RS256'], - decode: { complete: true }, - });*/ +export async function getUserInfo(authHeader: string) { + try { + let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, { + "method": "GET", + "headers": {"Authorization": authHeader} + }); + let payload: any = await userResponse.json(); + let userId : string = !payload.sub ? "" : payload.sub; + return {userId}; + } catch (e) { + throw e; + } } -export async function verifyJwt(_/*request*/: FastifyRequest) { - return {userId: "dummy", admin: true}; -/* let jwt = (await request.jwtVerify()); - let userId = jwt[`sub`].split("|")[1]; - let admin = jwt[`${process.env.AUTH0_CLAIMS_NAMESPACE}/admin`]; - return {userId, admin};*/ +export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<{userAppId: string, admin: boolean}>; + +export async function verifyJwt(request: FastifyRequest) { + let authHeader = !request.headers.authorization ? "" : request.headers.authorization + if(!authHeader) { + return {userAppId: "", admin: false}; + } else { + let {userId} = await getUserInfo(authHeader); + let {role} = await getUserRole(userId); + let admin: boolean = role === "admin" + let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId; + return {userAppId, admin}; + } } diff --git a/src/declarations/fastify-authz-jwks.d.ts b/src/declarations/fastify-authz-jwks.d.ts deleted file mode 100644 index 1145745..0000000 --- a/src/declarations/fastify-authz-jwks.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module "fastify-authz-jwks" { - import {FastifyReply, FastifyRequest} from "fastify"; - export type SecretCB = (e: Error | null, secret: string | undefined) => void; - export default function fastifyJwtSecret( - options: {} - ): (request : FastifyRequest, token : FastifyReply, cb: SecretCB) => void; -} diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 253bfd0..76afdf0 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -4,7 +4,7 @@ import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; import {createBizSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; import {isRegionManager} from "../utils"; -import {verifyJwt} from "../auth0"; +import {Auth0JwtVerifier} from "../auth0"; interface GetRegionBusinessRequest extends RequestGenericInterface { Params: { @@ -39,15 +39,15 @@ export interface Business { location?: GeoPoint | null | undefined } -export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer) { +export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { app.get( '/regions/:regionId/businesses', {schema: getBizSchema}, async (request, reply) => { - let {userId, admin} = await verifyJwt(request) - if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + let {userAppId, admin} = await verifyJwt(request) + if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { reply.unauthorized("User does not have access to region"); return; } else { @@ -71,8 +71,8 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa '/regions/:regionId/businesses', {schema: createBizSchema}, async (request) => { - let {userId, admin} = await verifyJwt(request); - if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + let {userAppId, admin} = await verifyJwt(request); + if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { throw app.httpErrors.unauthorized("User does not have access to region"); } else if (!!request.body.regionId && request.body.regionId !== request.params.regionId) { throw app.httpErrors.badRequest("Region ID mismatch between route and body "); diff --git a/src/endpoints/editRequests.ts b/src/endpoints/editRequests.ts index 846c083..2b02faf 100644 --- a/src/endpoints/editRequests.ts +++ b/src/endpoints/editRequests.ts @@ -1,6 +1,7 @@ import {FastifyInstance, RequestGenericInterface} from "fastify"; import {DataLayer} from "../database/productionDataLayer"; import {Business} from "./businesses"; +import {Auth0JwtVerifier} from "../auth0"; export interface EditRequests { adds: AddRequest[] | undefined, @@ -27,10 +28,11 @@ interface CreateEditRequests extends RequestGenericInterface { Body: EditRequests } -export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer) { +export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { app.post( '/edits', async (request, reply) => { + await verifyJwt(request); let response = { status: "ok", editRequests: {} diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index e6353fd..fa801c1 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,7 +1,7 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; import {getFilterSchema} from "./docs/filterSchemas"; -import {verifyJwt} from "../auth0"; +import {Auth0JwtVerifier} from "../auth0"; interface GetFiltersRequest extends RequestGenericInterface { Params: { @@ -9,11 +9,11 @@ interface GetFiltersRequest extends RequestGenericInterface { } } -export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer) { +export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { app.get('/regions/:regionId/filters', {schema: getFilterSchema}, async (request, reply) => { - let {userId} = await verifyJwt(request); - if (!userId) { + let {userAppId} = await verifyJwt(request); + if (!userAppId) { reply.unauthorized("User not found!"); return; } else { diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 8492120..83c796c 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -7,7 +7,7 @@ import { updateRegionReqSchema, deleteRegionReqSchema } from "./docs/regionSchemas"; -import {verifyJwt} from "../auth0"; +import {Auth0JwtVerifier} from "../auth0"; import {isRegionManager} from "../utils"; interface GetManagedRegionsRequest extends RequestGenericInterface { @@ -39,13 +39,13 @@ interface UpdateRegionRequest extends RequestGenericInterface { Body: Region } -export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer) { +export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : DataLayer, verifyJwt: Auth0JwtVerifier) { app.get( '/regions/manager/:managerId', {schema: getManagedRegionsReqSchema}, async (request,reply) => { - let {userId, admin} = await verifyJwt(request); - if(!userId) { + let {userAppId, admin} = await verifyJwt(request); + if(!userAppId) { reply.send(reply.unauthorized); return; } else { @@ -54,7 +54,7 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : date: Date.now(), regions: [] }; - if (admin || userId == request.params.managerId) { + if (admin || userAppId == request.params.managerId) { response.regions.push(...(await dataLayer.getRegionsManagedBy(request.params.managerId))); } return JSON.stringify(response); @@ -65,9 +65,9 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : app.get( '/regions/:regionId', {schema: getSingleRegionReqSchema}, - async(request ) => { + async(request, reply ) => { - let {userId, admin} = await verifyJwt(request); + let {userAppId, admin} = await verifyJwt(request); let response = { status: "ok", date: Date.now(), @@ -76,8 +76,11 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : let regions : Region[] ; if(admin) { regions = (await dataLayer.getAllRegions()); + } else if (!userAppId) { + reply.unauthorized(); + return; } else { - regions = (await dataLayer.getRegionsManagedBy(userId)); + regions = (await dataLayer.getRegionsManagedBy(userAppId)); } let region: Region | undefined = regions.find((r => r.name == request.params.regionId)) response.region = !!region ? region : null; @@ -109,8 +112,8 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : '/regions/:regionId', {schema: updateRegionReqSchema}, async(request, reply) => { - let {userId, admin} = await verifyJwt(request); - if(!(admin || await isRegionManager(userId, request.params.regionId, dataLayer))) { + let {userAppId, admin} = await verifyJwt(request); + if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { reply.unauthorized("Only region managers and administrators can update region data") return; } else { diff --git a/src/index.ts b/src/index.ts index cc011da..366011f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,22 +6,21 @@ import { createBusinessesEndpoint} from "./endpoints/businesses"; import createRegionsEndpoint from "./endpoints/regions"; import {productionDataLayer} from "./database/productionDataLayer"; import {createFiltersEndpoint} from "./endpoints/filters"; -import {registerAuth0} from "./auth0"; import {registerCorsHandler} from "./cors"; import {registerSwagger} from "./swagger"; +import {verifyJwt} from "./auth0"; const port = Number(process.env.PORT || 8080); const server = fastify(); server.register(fastifySensible); registerSwagger(server); -registerAuth0(/*server*/); registerCorsHandler(server); addRoutes( server, createPingEndpoint, - (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer), - (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer), - (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer) + (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer, verifyJwt), + (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer, verifyJwt), + (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, verifyJwt) ); server.listen(port, '::', (err, address) => { diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index 517ad18..c7ba7fe 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -1,4 +1,3 @@ -import {registerAuth0} from "../src/auth0"; import fastify, {FastifyInstance} from "fastify"; import {addRoutes} from "../src/utils"; import createRegionsEndpoint from "../src/endpoints/regions"; @@ -6,25 +5,27 @@ import {DummyDatalayer} from "./utils/testDataLayer"; import fetch from "node-fetch"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import fastifySensible from "fastify-sensible"; -import {setupAuth0TestEnv} from "./utils/testify"; -import jwtDecode from "jwt-decode"; +import { setupAuth0TestEnv} from "./utils/testify"; import {DummyRegion} from "./utils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; +import {getUserInfo, verifyJwt} from "../src/auth0"; describe("Auth0 integration tests", () => { let sut : FastifyInstance; let userAccessToken : string; let adminAccessToken: string; + let userAppId: string; beforeAll(async (done) => { - setupAuth0TestEnv() + setupAuth0TestEnv(); await authenticateToTestDomain(); + let userInfo = await getUserInfo(`Bearer ${userAccessToken}`); + userAppId = userInfo.userId.split("|")[1]; done(); }); beforeEach(() => { sut = fastify(); - registerAuth0(/*sut, process.env.TEST_AUTH0_DOMAIN*/); // fastifySensible gives us the decorator function needed when rejecting unauthorized reqs sut.register(fastifySensible); @@ -38,7 +39,7 @@ describe("Auth0 integration tests", () => { describe("Region Tests", () => { beforeEach(() => { addRoutes(sut, - () => createRegionsEndpoint(sut, new DummyDatalayer()), + () => createRegionsEndpoint(sut, new DummyDatalayer(), verifyJwt), ); }); @@ -77,8 +78,8 @@ describe("Auth0 integration tests", () => { beforeEach(() => { testDataLayer = new DummyDatalayer(); addRoutes(sut, - () => createBusinessesEndpoint(sut, testDataLayer), - () => createRegionsEndpoint(sut, testDataLayer) + () => createBusinessesEndpoint(sut, testDataLayer, verifyJwt), + () => createRegionsEndpoint(sut, testDataLayer, verifyJwt) ); }); @@ -92,15 +93,15 @@ describe("Auth0 integration tests", () => { }); it("Allows an authenticated request for businesses", async (done) => { - let userId = jwtDecode(userAccessToken).sub.toString().split("|")[1]; let authRegion = {...DummyRegion}; - authRegion.manager = userId + authRegion.manager = userAppId let regionsResponse = await sut.inject({ method: 'POST', url: `/regions`, payload: authRegion, headers:{authorization: `Bearer ${adminAccessToken}`} }); + expect(regionsResponse.statusCode).toBe(201); console.log(regionsResponse); let response = await sut.inject({ @@ -114,33 +115,32 @@ describe("Auth0 integration tests", () => { }); async function authenticateToTestDomain() { - let userResponse = await fetch(`https://${process.env.TEST_AUTH0_DOMAIN}/oauth/token`, { + let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: {'content-type': 'application/json'}, body: JSON.stringify({ - client_id: process.env.TEST_AUTH0_CLIENT_ID, - client_secret: process.env.TEST_AUTH0_CLIENT_SECRET, + client_id: process.env.AUTH0_CLIENT_ID, + client_secret: process.env.AUTH0_CLIENT_SECRET, username: process.env.TEST_AUTH0_USERNAME, password: process.env.TEST_AUTH0_PASSWORD, - audience: `https://testing-ranlab.com`, - scope: 'read:sample', + scope: 'openid', grant_type: "password" }) }); userAccessToken = (await userResponse.json()).access_token; - let adminResponse = await fetch(`https://${process.env.TEST_AUTH0_DOMAIN}/oauth/token`, { + let adminResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: {'content-type': 'application/json'}, body: JSON.stringify({ - client_id: process.env.TEST_AUTH0_CLIENT_ID, - client_secret: process.env.TEST_AUTH0_CLIENT_SECRET, + client_id: process.env.AUTH0_CLIENT_ID, + client_secret: process.env.AUTH0_CLIENT_SECRET, username: process.env.TEST_AUTH0_ADMIN_USERNAME, password: process.env.TEST_AUTH0_ADMIN_PASSWORD, - audience: `https://testing-ranlab.com`, - scope: 'read:sample', + scope: 'openid', grant_type: "password" }) }); - adminAccessToken = (await adminResponse.json()).access_token; + let adminJson = await adminResponse.json(); + adminAccessToken = adminJson.access_token; } }); diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index b4f8b06..6a4aef8 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -4,10 +4,10 @@ import { createDummyBusiness, createDummyRegion, dummyAdminToken, - DummyBiz, + DummyBiz, DummyRegion, getDummyBusinesses } from "./utils/dummyData"; -import {MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; +import {getTestJwtVerifier, setupAuth0TestEnv, testify} from "./utils/testify"; import createRegionsEndpoint from "../src/endpoints/regions"; describe("Business Endpoint Tests", () => { @@ -19,14 +19,15 @@ describe("Business Endpoint Tests", () => { beforeEach(async (done) => { testDataLayer = new DummyDatalayer(); - const server = testify(new MockAuth0Return()); - const regionsApp = createRegionsEndpoint(server, testDataLayer); + const server = testify(); + const regionsApp = createRegionsEndpoint(server, testDataLayer, getTestJwtVerifier("admin", true)); await createDummyRegion(regionsApp); done(); }); + it('Can create and retrieve a valid business', async(done) => { - const server = testify(new MockAuth0Return()); - const bizApp = createBusinessesEndpoint(server, testDataLayer) + const server = testify(); + const bizApp = createBusinessesEndpoint(server, testDataLayer, getTestJwtVerifier(DummyRegion.manager, false)) const createResponse = await createDummyBusiness(bizApp); expect(createResponse.statusCode).toBe(200); @@ -41,7 +42,7 @@ describe("Business Endpoint Tests", () => { }); it('Can update and retrieve a business', async(done) => { - const bizApp = createBusinessesEndpoint(testify(new MockAuth0Return()), testDataLayer); + const bizApp = createBusinessesEndpoint(testify(), testDataLayer, getTestJwtVerifier("admin", true)); const createResponse = await createDummyBusiness(bizApp); const bizId = JSON.parse(createResponse.payload).businessId; diff --git a/tests/cors.test.ts b/tests/cors.test.ts index b8f4776..beb7f9e 100644 --- a/tests/cors.test.ts +++ b/tests/cors.test.ts @@ -1,13 +1,11 @@ import {registerCorsHandler} from "../src/cors"; -import {MockAuth0Return, testify} from "./utils/testify"; +import { testify} from "./utils/testify"; import {DummyRegion} from "./utils/dummyData"; describe('CORS Handler Tests', function () { async function testCorsRequest(origin: string, originResponse: string | boolean) { - let mockReturn = new MockAuth0Return(); - let testApp = testify(mockReturn); - mockReturn.userId = "Dummy"; + let testApp = testify(); registerCorsHandler(testApp); let response = await testApp.inject({ method: 'OPTIONS', diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 4ec5298..38dbf8e 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -1,21 +1,20 @@ import {DummyDatalayer} from "./utils/testDataLayer"; -import {getMockToken, MockAuth0Return, testify} from "./utils/testify"; +import { testify} from "./utils/testify"; import {createEditEndpoint} from "../src/endpoints/editRequests"; +import {dummyRegionManagerToken, dummyTokenVerifier} from "./utils/dummyData"; describe("Edit Request unit tests", () => { let testDataLayer: DummyDatalayer it("Submitted edit requests are seen by region admin", async (done) => { testDataLayer = new DummyDatalayer(); - let mockAuth0 = new MockAuth0Return(); - const testApp = testify(mockAuth0); - mockAuth0.userId = "nobody"; - let editEndpoint = createEditEndpoint(testApp, testDataLayer); + const testApp = testify(); + let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); const editResponse = await editEndpoint.inject({ method: "POST", url: `/edits`, - headers: {authorization: `Bearer ${getMockToken({userId: mockAuth0.userId, admin: false})}`}, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`}, payload: { adds: [], updates: [], diff --git a/tests/filters.test.ts b/tests/filters.test.ts index ce77f00..a533f4d 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -6,10 +6,11 @@ import { createDummyRegion, DummyBiz, DummyRegion, - dummyRegionManagerToken + dummyRegionManagerToken, + dummyTokenVerifier } from "./utils/dummyData"; import createRegionsEndpoint from "../src/endpoints/regions"; -import { MockAuth0Return, setupAuth0TestEnv, testify} from "./utils/testify"; +import {setupAuth0TestEnv, testify} from "./utils/testify"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; @@ -17,9 +18,9 @@ describe("Filter Endpoint Tests", () => { beforeAll(async (done) => { setupAuth0TestEnv(); testDataLayer = new DummyDatalayer(); - const server = testify(new MockAuth0Return()); - const regionsApp = createRegionsEndpoint(server, testDataLayer); - const bizApp = createBusinessesEndpoint(server, testDataLayer); + const server = testify(); + const regionsApp = createRegionsEndpoint(server, testDataLayer, dummyTokenVerifier); + const bizApp = createBusinessesEndpoint(server, testDataLayer, dummyTokenVerifier); await createDummyRegion(regionsApp); await createDummyBusiness(bizApp); @@ -34,8 +35,8 @@ describe("Filter Endpoint Tests", () => { }); it('Returns only the region-specific filter data', async (done) => { - const server = testify(new MockAuth0Return()); - const filterApp = createFiltersEndpoint(server, testDataLayer); + const server = testify(); + const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); const filterResponse = await filterApp.inject({ method: 'GET', url: `/regions/${DummyRegion.name}/filters`, diff --git a/tests/regions.test.ts b/tests/regions.test.ts index dc3ea19..ced2b5d 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -1,13 +1,21 @@ import createRegionsEndpoint from "../src/endpoints/regions"; -import {testify, MockAuth0Return, setupAuth0TestEnv} from "./utils/testify"; +import { + testify, + setupAuth0TestEnv +} from "./utils/testify"; import {DummyDatalayer} from "./utils/testDataLayer"; -import {dummyAdminToken, DummyRegion, dummyRegionManagerToken, getRegionsByDummyManager} from "./utils/dummyData"; +import { + dummyAdminToken, + DummyRegion, + dummyRegionManagerToken, + getRegionsByDummyManager, + dummyTokenVerifier +} from "./utils/dummyData"; import {Region} from "../src/database/productionDataLayer"; import {FastifyInstance} from "fastify"; describe("Region Endpoint Tests", () => { let testDataLayer: DummyDatalayer; - let mockAuth0Return: MockAuth0Return; let testApp: FastifyInstance; beforeAll(() => { @@ -16,26 +24,21 @@ describe("Region Endpoint Tests", () => { beforeEach(async (done) => { testDataLayer= new DummyDatalayer() - mockAuth0Return = new MockAuth0Return(); - testApp = testify(mockAuth0Return); + testApp = testify(); await testDataLayer.setRegion(DummyRegion); done(); }); it('Can create and retrieve all regions as SysAdmin', async (done) => { - const app = createRegionsEndpoint(testApp, testDataLayer); + const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); let testRegions: Region[] = [{name: "region1", manager: "manager1"}, {name: "region2", manager: "manager2"}]; testRegions.forEach((r) => testDataLayer.setRegion(r)); - mockAuth0Return.userId = "DummyUser"; - let authCalls = 0; for(let region of testRegions) { const response = await app.inject({ method: 'GET', headers: {authorization: `Bearer ${dummyAdminToken}`}, url: `/regions/manager/${region.manager}` }); - authCalls++; - expect(mockAuth0Return.callCount).toBe(authCalls); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions.filter(r => r.manager == region.manager)])); } @@ -44,11 +47,9 @@ describe("Region Endpoint Tests", () => { }); it('Can create and retrieve a region as Region Manager', async (done) => { - const app = createRegionsEndpoint(testApp, testDataLayer); - mockAuth0Return.userId = "DummyUser"; + const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const response = await getRegionsByDummyManager(app); - expect(mockAuth0Return.callCount).toBe(1); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); await app.close(); @@ -56,7 +57,7 @@ describe("Region Endpoint Tests", () => { }); it('Can update and retrieve a region', async (done) => { - const app = createRegionsEndpoint(testApp, testDataLayer); + const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const updatedRegion = { name: "TestRegion", manager: "TestManager" @@ -75,7 +76,7 @@ describe("Region Endpoint Tests", () => { }); it('Can delete a region', async (done) => { - const app = createRegionsEndpoint(testApp, testDataLayer); + const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const deleteResponse = await app.inject( { method: 'DELETE', url: `/regions/${DummyRegion.name}`, @@ -92,7 +93,7 @@ describe("Region Endpoint Tests", () => { }); it("Can retrieve a single region as either region admin or system admin", async(done) => { - const app = createRegionsEndpoint(testApp, testDataLayer); + const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const getRegionAdminResponse = await app.inject({ method: 'GET', url: `/regions/${DummyRegion.name}`, diff --git a/tests/utils/dummyData.ts b/tests/utils/dummyData.ts index cd46572..d81354e 100644 --- a/tests/utils/dummyData.ts +++ b/tests/utils/dummyData.ts @@ -1,12 +1,11 @@ -import {FastifyInstance} from "fastify"; +import {FastifyInstance, FastifyRequest} from "fastify"; import {Business} from "../../src/endpoints/businesses"; import {Region} from "../../src/database/productionDataLayer"; import {getMockToken} from "./testify"; -import jwtDecode from "jwt-decode"; const dummyManager = "DummyManagerId"; -export const dummyRegionManagerToken = getMockToken({userId: dummyManager, admin: false}) -export const dummyAdminToken = getMockToken({userId: "admin", admin: true}); +export const dummyRegionManagerToken = getMockToken({userAppId: dummyManager, admin: false}) +export const dummyAdminToken = getMockToken({userAppId: "admin", admin: true}); export const DummyRegion: Region = { name: "DummyRegion", @@ -21,11 +20,22 @@ export const DummyBiz: Business = { industry: "DummyIndustry" }; +export async function dummyTokenVerifier (req: FastifyRequest) { + if(!req.headers.authorization) { + return {userAppId: "", admin: false}; + } + if(req.headers.authorization.indexOf(dummyAdminToken) > 0) { + return {userAppId: "admin", admin: true}; + } else if (req.headers.authorization?.indexOf(dummyRegionManagerToken) > 0) { + return {userAppId: DummyRegion.manager, admin: false}; + } else { + throw new Error("Unrecognized token"); + } +} + export async function createDummyRegion(regionsApp: FastifyInstance, manager: string = dummyManager) { let region = {...DummyRegion}; region.manager = manager - let temp = jwtDecode(dummyAdminToken); - console.log(temp); try { let response = await regionsApp.inject({ method: "POST", diff --git a/tests/utils/testify.ts b/tests/utils/testify.ts index b5e9bb6..25135b9 100644 --- a/tests/utils/testify.ts +++ b/tests/utils/testify.ts @@ -1,4 +1,4 @@ -import fastify from "fastify"; +import fastify, {FastifyRequest} from "fastify"; import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; import jwt from "jsonwebtoken"; import fastifySensible from "fastify-sensible"; @@ -6,16 +6,11 @@ import fastifySensible from "fastify-sensible"; export const AUTH0_CLAIMS_NAMESPACE = "https://mun.ca"; export const mockSecret = 'dummy'; -export class MockAuth0Return { - userId: string|null = null; - admin: boolean = false; - callCount: number = 0; -} - export function setupAuth0TestEnv() { process.env.AUTH0_CLAIMS_NAMESPACE = AUTH0_CLAIMS_NAMESPACE; - process.env.TEST_AUTH0_DOMAIN = "dev-5ju75h98.us.auth0.com"; - process.env.TEST_AUTH0_CLIENT_ID = "iqwBRZwwuKGz0BkHiInTWTyqvOFLepd6"; + process.env.AUTH0_DOMAIN = "dev-5ju75h98.us.auth0.com"; + process.env.AUTH0_CLIENT_ID = "iqwBRZwwuKGz0BkHiInTWTyqvOFLepd6"; + process.env.AUTH0_MGMT_CLIENT_ID = "HU171Qtg3j3395vGXuNsgmRq8XQPbKzT"; process.env.TEST_AUTH0_USERNAME = "liquiddark@gmail.com"; process.env.TEST_AUTH0_ADMIN_USERNAME = "burton.technical.709@gmail.com"; @@ -24,17 +19,21 @@ export function setupAuth0TestEnv() { // process.env.TEST_AUTH0_CLIENT_SECRET must be set in local runtime environment } -export function getMockToken(payload: {userId: string, admin: boolean }) { +export function getMockToken(payload: {userAppId: string, admin: boolean }) { let token : any = {}; - token['sub'] = `auth0|${payload.userId}`; + token['sub'] = `auth0|${payload.userAppId}`; token[`${AUTH0_CLAIMS_NAMESPACE}/admin`] = payload.admin; return jwt.sign(token, mockSecret) } -export const testify = (mockAuth0Return : MockAuth0Return) => { +export function getTestJwtVerifier(userAppId: string, admin: boolean) { + return async(_: FastifyRequest) => ({userAppId, admin}); +} + +export const testify = () => { const f = fastify(); f.register(fastifyJWT, { - secret: (_request, _reply, _provider) => { mockAuth0Return.callCount++; _provider(null, mockSecret);}, + secret: (_request, _reply, _provider) => { _provider(null, mockSecret);}, audience: 'https://localhost', issuer: 'https://localhost/', algorithms: ['none'], diff --git a/yarn.lock b/yarn.lock index b263006..5b80099 100644 --- a/yarn.lock +++ b/yarn.lock @@ -908,7 +908,7 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/jsonwebtoken@^8.3.2": +"@types/jsonwebtoken@^8.3.2", "@types/jsonwebtoken@^8.5.0": version "8.5.0" resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz" integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== @@ -1495,6 +1495,11 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clone@2.x: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz" @@ -2006,9 +2011,20 @@ fastfall@^1.5.0: dependencies: reusify "^1.0.0" +fastify-auth0-verify@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/fastify-auth0-verify/-/fastify-auth0-verify-0.4.2.tgz#96281adeb13f0f8298f3ffe80647d141411f28d6" + integrity sha512-l7LBdS5mAYJgYD+khzB3DH5ZLuO2+/MxOa2NsKIowDAcrVntteUavMQljvweLWQLFe1KfmmmZEbf5JktlSYtoQ== + dependencies: + fastify-jwt "^2.1.2" + fastify-plugin "^3.0.0" + http-errors "^1.7.3" + node-cache "^5.0.1" + node-fetch "^2.6.1" + fastify-authz-jwks@^1.1.11: version "1.1.11" - resolved "https://registry.yarnpkg.com/fastify-authz-jwks/-/fastify-authz-jwks-1.1.11.tgz" + resolved "https://registry.yarnpkg.com/fastify-authz-jwks/-/fastify-authz-jwks-1.1.11.tgz#1b0b8d18962e4f1a4e6033a687eaf5325a6b8351" integrity sha512-8puiTBrphRrQy0fpJQLpwYKIyyeAWDkdp9/EDGVWbGOypvA/zc8RXaTCtDQxQ9JOxx0X0/g8IEYQv/37yJtCPQ== dependencies: jwks-rsa "^1.6.0" @@ -2026,6 +2042,17 @@ fastify-error@^0.2.0: resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.2.0.tgz" integrity sha512-zabxsBatj59ROG0fhP36zNdc5Q1/eYeH9oSF9uvfrurZf8/JKfrJbMcIGrLpLWcf89rS6L91RHWm20A/X85hcA== +fastify-jwt@^2.1.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.3.0.tgz#0f20e3267728d6b570e4240915b087a1f8853e13" + integrity sha512-dRjkrfVpZnic1DxIjN8lyhL/KM/YQsLy6SeyPhKxRkTEJIjY3sViDqfhCUc812jSgbqM7Sx22BA7moqpmCtA3Q== + dependencies: + "@types/jsonwebtoken" "^8.5.0" + fastify-plugin "^3.0.0" + http-errors "^1.8.0" + jsonwebtoken "^8.5.1" + steed "^1.1.3" + fastify-jwt@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-2.1.3.tgz" @@ -2485,7 +2512,7 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.1, http-errors@^1.7.3: +http-errors@^1.7.1, http-errors@^1.7.3, http-errors@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== @@ -3696,6 +3723,13 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-cache@^5.0.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" From dbacd552429940d62b8276a3081359f08af91863 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 20 Feb 2021 13:43:55 -0330 Subject: [PATCH 40/75] Added "all industries" endpoint for admin users Implemented admin check for region updates that attempt to change the region manager. --- src/endpoints/docs/filterSchemas.ts | 28 +++++++++++++++--- src/endpoints/filters.ts | 32 +++++++++++++++++++- src/endpoints/regions.ts | 7 +++-- tests/auth0.test.ts | 1 - tests/dataLayer.test.ts | 1 + tests/filters.test.ts | 45 +++++++++++++++++++++++++++-- tests/regions.test.ts | 30 ++++++++++++++++++- tests/utils/testDataLayer.ts | 14 +++++++++ 8 files changed, 147 insertions(+), 11 deletions(-) diff --git a/src/endpoints/docs/filterSchemas.ts b/src/endpoints/docs/filterSchemas.ts index 1a31719..00522cf 100644 --- a/src/endpoints/docs/filterSchemas.ts +++ b/src/endpoints/docs/filterSchemas.ts @@ -10,7 +10,7 @@ export const filtersSchema = { }, industries: { type: 'array', - items: {type: 'string'} + items: {type: 'string' } }, } } @@ -23,10 +23,30 @@ export const getFilterSchema = { 200: { type: 'object', properties: { - status: { type: 'string'}, - date: {type: 'string' }, + status: { type: 'string' }, + date: { type: 'string' }, filters: filtersSchema } } } -} +}; + +export const getAllIndustriesSchema = { + description: 'Admin-only endpoint allowing access to the global list of industries', + security: [], + response: { + 200: { + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + industries: { + type: "array", + items: { + type: "string" + } + } + } + } + } +}; diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index fa801c1..99ce0a3 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,6 +1,6 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; -import {getFilterSchema} from "./docs/filterSchemas"; +import {getAllIndustriesSchema, getFilterSchema} from "./docs/filterSchemas"; import {Auth0JwtVerifier} from "../auth0"; interface GetFiltersRequest extends RequestGenericInterface { @@ -28,5 +28,35 @@ export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer } ); + app.get("/filters/industries", {schema: getAllIndustriesSchema}, + async (request, reply) =>{ + let {admin} = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Only admins have access to this data"); + return; + } else { + let response = { + status: "ok", + date: Date.now(), + industries: [] + }; + let regions = await dataLayer.getAllRegions(); + regions.forEach((r) => { + if (!!r.filters && !!r.filters.industries) { + r.filters.industries.forEach((f_i) => { + if(!!f_i.industry) { + if (!response.industries.find((r_i) => r_i === f_i.industry)) { + response.industries.push(f_i.industry); + } + } + }) + } + }); + + return JSON.stringify(response); + } + } + ) + return app; } diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 83c796c..a19f7c4 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -113,11 +113,14 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : {schema: updateRegionReqSchema}, async(request, reply) => { let {userAppId, admin} = await verifyJwt(request); - if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { + let UpdatedRegion: Region = {...request.body}; + if(!admin && !!UpdatedRegion.manager && !(await isRegionManager(UpdatedRegion.manager, request.params.regionId, dataLayer))) { + reply.unauthorized("Only admin users can update the region manager"); + return; + } else if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { reply.unauthorized("Only region managers and administrators can update region data") return; } else { - let UpdatedRegion: Region = {...request.body}; let response = { status: "ok", region: UpdatedRegion diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index c7ba7fe..7092f36 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -102,7 +102,6 @@ describe("Auth0 integration tests", () => { headers:{authorization: `Bearer ${adminAccessToken}`} }); expect(regionsResponse.statusCode).toBe(201); - console.log(regionsResponse); let response = await sut.inject({ method: "GET", diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 1058ff4..319a928 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -98,4 +98,5 @@ describe("Production Data Layer Integration Tests", () => { done(); }); + }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index a533f4d..a456bea 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -3,7 +3,7 @@ import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; import {DummyDatalayer} from "./utils/testDataLayer"; import { createDummyBusiness, - createDummyRegion, + createDummyRegion, dummyAdminToken, DummyBiz, DummyRegion, dummyRegionManagerToken, @@ -23,11 +23,20 @@ describe("Filter Endpoint Tests", () => { const bizApp = createBusinessesEndpoint(server, testDataLayer, dummyTokenVerifier); await createDummyRegion(regionsApp); + let notDummyRegion = {...DummyRegion}; + notDummyRegion.name = `Not ${DummyRegion.name}`; + await regionsApp.inject({ + method: "POST", + url: "/regions", + payload: notDummyRegion, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + await createDummyBusiness(bizApp); await createDummyBusiness(bizApp, { name: `Not ${DummyBiz.name}`, employees: 1, - regionId: `Not ${DummyRegion.name}`, + regionId: notDummyRegion.name, industry: `Not ${DummyBiz.industry}`, year_added: DummyBiz.year_added + 1 }); @@ -54,4 +63,36 @@ describe("Filter Endpoint Tests", () => { await filterApp.close(); done(); }); + + it("Gets all industries for admin users", async (done) => { + const server = testify(); + const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); + const industriesResponse = await filterApp.inject({ + method: 'GET', + url: `/filters/industries`, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + expect(industriesResponse.statusCode).toBe(200); + expect(JSON.parse(industriesResponse.payload).industries).toStrictEqual( + expect.arrayContaining([ + DummyBiz.industry, + `Not ${DummyBiz.industry}` + ]) + ); + await filterApp.close(); + done(); + }); + + it("Denies all industries for non-admin users", async (done) => { + const server = testify(); + const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); + const industriesResponse = await filterApp.inject({ + method: 'GET', + url: `/filters/industries`, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(industriesResponse.statusCode).toBe(401); + await filterApp.close(); + done(); + }); }); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index ced2b5d..de650f1 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -60,7 +60,7 @@ describe("Region Endpoint Tests", () => { const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const updatedRegion = { name: "TestRegion", - manager: "TestManager" + manager: DummyRegion.manager }; const response = await app.inject( { method: 'POST', @@ -75,6 +75,34 @@ describe("Region Endpoint Tests", () => { done(); }); + it('Can update region manager only as admin', async (done) => { + const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); + const updatedRegion = { + name: "TestRegion", + manager: `Not ${DummyRegion.manager}` + }; + const nonAdminResponse = await app.inject( { + method: 'POST', + url: `/regions/${DummyRegion.name}`, + payload: updatedRegion, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`}, + }); + expect(nonAdminResponse.statusCode).toBe(401); + + const adminResponse = await app.inject( { + method: 'POST', + url: `/regions/${DummyRegion.name}`, + payload: updatedRegion, + headers: {authorization: `Bearer ${dummyAdminToken}`}, + }); + expect(adminResponse.statusCode).toBe(200); + expect(JSON.parse(adminResponse.payload).region).toStrictEqual(updatedRegion); + + await app.close(); + done(); + }); + + it('Can delete a region', async (done) => { const app = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const deleteResponse = await app.inject( { diff --git a/tests/utils/testDataLayer.ts b/tests/utils/testDataLayer.ts index 364b84a..95aacca 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/utils/testDataLayer.ts @@ -21,6 +21,20 @@ export class DummyDatalayer implements DataLayer { async setBusiness(business:Business): Promise { this.businesses.push(business); + let regionIndex = this.regions.findIndex((r) => r.name == business.regionId); + let bizRegion = this.regions[regionIndex] + if(!bizRegion.filters) { + bizRegion.filters = {}; + } + if(!bizRegion.filters.industries) { + bizRegion.filters.industries = []; + } + let industryIndex = bizRegion.filters.industries.findIndex((i) => i.industry === business.industry); + if(industryIndex < 0) { + bizRegion.filters.industries.push({industry: business.industry, count: 1}); + } else { + bizRegion.filters.industries[industryIndex].count++; + } return {id:"1"}; } From e7446d54eceee46946da0cf22c1d40ea2ad61771 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 27 Feb 2021 00:08:24 -0330 Subject: [PATCH 41/75] Adding more edit request functionality and testing --- src/database/productionDataLayer.ts | 81 +++++++--- src/endpoints/businesses.ts | 12 +- src/endpoints/docs/editRequestSchemas.ts | 78 +++++++++ src/endpoints/editRequest.ts | 87 ++++++++++ src/endpoints/editRequests.ts | 65 -------- src/endpoints/endpointUtils.ts | 16 ++ tests/auth0.test.ts | 6 +- tests/businesses.test.ts | 6 +- tests/cors.test.ts | 4 +- tests/dataLayer.test.ts | 75 ++++++++- tests/editRequests.test.ts | 168 +++++++++++++++++--- tests/filters.test.ts | 6 +- tests/regions.test.ts | 6 +- tests/{utils => testUtils}/dummyData.ts | 0 tests/{utils => testUtils}/testDataLayer.ts | 29 ++-- tests/{utils => testUtils}/testify.ts | 0 16 files changed, 485 insertions(+), 154 deletions(-) create mode 100644 src/endpoints/docs/editRequestSchemas.ts create mode 100644 src/endpoints/editRequest.ts delete mode 100644 src/endpoints/editRequests.ts create mode 100644 src/endpoints/endpointUtils.ts rename tests/{utils => testUtils}/dummyData.ts (100%) rename tests/{utils => testUtils}/testDataLayer.ts (76%) rename tests/{utils => testUtils}/testify.ts (100%) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 6dba4ef..af943a1 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,7 +1,8 @@ import {Business} from "../endpoints/businesses"; import {firestore} from "./firestore"; -import {AddRequest, DeleteRequest, UpdateRequest} from "../endpoints/editRequests"; +import { EditRequest } from "../endpoints/editRequest"; import firebase from "firebase"; +import Timestamp = firebase.firestore.Timestamp; export interface IdObject { id: string @@ -32,33 +33,12 @@ export interface DataLayer { setRegion(region: Region): Promise; deleteRegion(regionId: string): Promise; getAllRegions(): Promise; - createAddRequest(add: AddRequest): Promise; - createUpdateRequest(updateRequest: UpdateRequest): Promise; - createDeleteRequests(deleteRequest: DeleteRequest): Promise; + createEditRequest(add: EditRequest): Promise; + getEditRequestsForRegion(regionId: string): Promise; + getAllEditRequests(): Promise; } export class ProductionDataLayer implements DataLayer { - async createAddRequest(addRequest: AddRequest): Promise { - let doc = firestore.collection("editRequests").doc(); - await doc.set(addRequest); - addRequest.id = doc.id; - return addRequest; - } - - async createUpdateRequest(updateRequest: UpdateRequest): Promise { - let doc = firestore.collection("editRequests").doc(); - await doc.set(updateRequest); - updateRequest.id = doc.id; - return updateRequest; - } - - async createDeleteRequests(deleteRequest: DeleteRequest): Promise { - let doc = firestore.collection("editRequests").doc(); - await doc.set(deleteRequest); - deleteRequest.id = doc.id; - return deleteRequest; - } - async getBusinessesByRegion(regionId: string) : Promise { let businessSnapshot = await firestore.collection("businesses").where("regionId", "==", regionId).get(); return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); @@ -146,6 +126,56 @@ export class ProductionDataLayer implements DataLayer { ); } + async createEditRequest(editRequest: EditRequest): Promise { + let doc = firestore.collection("editRequests").doc(); + await doc.set(editRequest); + return {id : doc.id}; + } + + async getEditRequestsForRegion(regionId: string) : Promise { + let regionRequests : EditRequest[] = []; + (await firestore.collection("editRequests").where("regionId", "==", regionId).get()).docs.forEach( + (req) => { + let converted = this.convertToEditRequest(req.id, req.data()); + if (!!converted) { + regionRequests.push(converted); + } + } + ); + return regionRequests; + } + + async getAllEditRequests(): Promise { + let allRequests: EditRequest[] = []; + (await firestore.collection("editRequests").get()).docs.forEach( + (req) => { + let converted = this.convertToEditRequest(req.id, req.data()); + if (!!converted) { + allRequests.push(converted); + } + } + ); + return allRequests; + } + + async getEditRequestById(id: string) : Promise { + firestore.collection("editRequests").doc(id); + return this.convertToEditRequest(id, (await firestore.collection("editRequests").doc(id).get()).data()); + } + + convertToEditRequest(id: string, documentData: firebase.firestore.DocumentData | undefined) : EditRequest | null { + if(!!documentData) { + let request = documentData; + request.dateSubmitted = ((documentData.dateSubmitted)).toDate(); + request.dateUpdated = ((documentData.dateUpdated)).toDate(); + request.id = id; + return request; + } else { + return null; + } + } + + private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: firebase.firestore.Transaction) { let regionRef = firestore.collection("regions").doc(regionId); let regionDoc = await transaction.get(regionRef); @@ -205,6 +235,7 @@ export class ProductionDataLayer implements DataLayer { return {}; } } + } export const productionDataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 76afdf0..a4a165b 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -5,15 +5,7 @@ import GeoPoint = firebase.firestore.GeoPoint; import {createBizSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; import {isRegionManager} from "../utils"; import {Auth0JwtVerifier} from "../auth0"; - -interface GetRegionBusinessRequest extends RequestGenericInterface { - Params: { - regionId: string - }, - Headers: { - access_token: string - } -} +import {AuthenticatedRequestByRegionId} from "./endpointUtils"; interface CreateBusinessRequest extends RequestGenericInterface { Params: { @@ -41,7 +33,7 @@ export interface Business { export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { - app.get( + app.get( '/regions/:regionId/businesses', {schema: getBizSchema}, async (request, reply) => { diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts new file mode 100644 index 0000000..10e9ddd --- /dev/null +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -0,0 +1,78 @@ +import {byRegionIdSchema} from "./basicSchemas"; +import { businessSchema} from "./businessesSchemas"; + + const editRequestSchema = { + type: "object", + properties: { + id: { + type: "string", + description: "Omit when creating new edit requests" + }, + regionId: {type: "string"}, + submitter: {type: "string"}, + dateSubmitted: {type: "string"}, + dateUpdated: {type: "string"}, + status: {type: "string"}, + adds: { + type: "array", + items: businessSchema + }, + updates: { + type: "array", + items: businessSchema + }, + deletes: { + type: "array", + items: {type: "string"} + } + } +} + +export const getEditRequestsByRegionSchema = { + params: byRegionIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + editRequests: { + type: "array", + items: editRequestSchema + } + } + } + } +}; + +export const getAllEditRequestsSchema = { + description: "Returns all edit requests. Only usable by system admins", + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + editRequests: { + type: "array", + items: editRequestSchema + } + } + } + } +}; + +export const createEditRequestSchema = { + description: "Creates the supplied edit request", + body: editRequestSchema, + response: { + 201: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + id: {type: "string"} + } + } + } +}; diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts new file mode 100644 index 0000000..44a01a6 --- /dev/null +++ b/src/endpoints/editRequest.ts @@ -0,0 +1,87 @@ +import {FastifyInstance } from "fastify"; +import {DataLayer} from "../database/productionDataLayer"; +import {Business} from "./businesses"; +import {Auth0JwtVerifier} from "../auth0"; +import {AuthenticatedRequest, AuthenticatedRequestByRegionId} from "./endpointUtils"; +import {isRegionManager} from "../utils"; +import { + createEditRequestSchema, + getAllEditRequestsSchema, + getEditRequestsByRegionSchema +} from "./docs/editRequestSchemas"; + +export interface EditRequest { + id?: string, + regionId: string, + submitter: string, + dateSubmitted: Date | string, + dateUpdated: Date | string + status: string, + adds?: Business[], + updates?: Business[] | undefined, + deletes?: string[] | undefined +} + +interface CreateEditRequest extends AuthenticatedRequestByRegionId { + Body: EditRequest +} + +export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { + app.get( + `/region/:regionId/edits`, + {schema: getEditRequestsByRegionSchema}, + async (request, reply) => { + let {userAppId, admin} = await verifyJwt(request); + if(!admin && !userAppId && !(await isRegionManager(userAppId, request.params.regionId, dataLayer))) { + reply.unauthorized("Must be logged in to submit requests"); + return; + } else { + let response = { + status: "ok", + editRequests: [] + } + response.editRequests = await dataLayer.getEditRequestsForRegion(request.params.regionId) + return JSON.stringify(response); + } + } + ); + + app.get( + "/edits/all", + {schema: getAllEditRequestsSchema}, + async (request, reply) => { + let { admin } = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Only admins may access this data!"); + return; + } else { + let response = { + status: "ok", + editRequests: await dataLayer.getAllEditRequests() + } + return JSON.stringify(response); + } + } + ); + + app.post( + '/region/:regionId/edits', + {schema: createEditRequestSchema}, + async (request, reply) => { + let {userAppId, admin} = await verifyJwt(request); + if(!admin && !userAppId && !(await isRegionManager(userAppId, request.params.regionId, dataLayer))) { + reply.unauthorized("Must be logged in to submit requests"); + return; + } else { + const incomingRequest = request.body; + let response = { + status: "ok", + id: (await dataLayer.createEditRequest(incomingRequest)).id + }; + reply.code(201); + return JSON.stringify(response); + } + } + ); + return app; +} diff --git a/src/endpoints/editRequests.ts b/src/endpoints/editRequests.ts deleted file mode 100644 index 2b02faf..0000000 --- a/src/endpoints/editRequests.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {FastifyInstance, RequestGenericInterface} from "fastify"; -import {DataLayer} from "../database/productionDataLayer"; -import {Business} from "./businesses"; -import {Auth0JwtVerifier} from "../auth0"; - -export interface EditRequests { - adds: AddRequest[] | undefined, - updates: UpdateRequest[] | undefined, - deletes: DeleteRequest[] | undefined -} - -export interface AddRequest { - id?: string | undefined, - business: Business -} - -export interface UpdateRequest { - id?: string | undefined, - business: Business -} - -export interface DeleteRequest { - id?: string | undefined, - businessId: string -} - -interface CreateEditRequests extends RequestGenericInterface { - Body: EditRequests -} - -export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { - app.post( - '/edits', - async (request, reply) => { - await verifyJwt(request); - let response = { - status: "ok", - editRequests: {} - }; - let adds : AddRequest[] = []; - const editRequests = request.body; - if(!!editRequests.adds) { - for(let addRequest of editRequests.adds) { - adds.push(await dataLayer.createAddRequest(addRequest)); - } - } - let updates : UpdateRequest[] = []; - if(!!editRequests.updates) { - for(let updateRequest of editRequests.updates) { - updates.push(await dataLayer.createUpdateRequest(updateRequest)); - } - } - let deletes : DeleteRequest[] = []; - if(!!editRequests.deletes) { - for(let deleteRequest of editRequests.deletes) { - deletes.push(await dataLayer.createDeleteRequests(deleteRequest)); - } - } - response.editRequests = {adds, updates, deletes}; - reply.code(201); - return JSON.stringify(response); - } - ); - return app; -} diff --git a/src/endpoints/endpointUtils.ts b/src/endpoints/endpointUtils.ts new file mode 100644 index 0000000..0abf40a --- /dev/null +++ b/src/endpoints/endpointUtils.ts @@ -0,0 +1,16 @@ +import {RequestGenericInterface} from "fastify"; + +export interface AuthenticatedRequest extends RequestGenericInterface { + Headers: { + access_token: string + } +} + +export interface AuthenticatedRequestByRegionId extends AuthenticatedRequest { + Params: { + regionId: string + }, + Headers: { + access_token: string + } +} diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index 7092f36..d4f3e9d 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -1,12 +1,12 @@ import fastify, {FastifyInstance} from "fastify"; import {addRoutes} from "../src/utils"; import createRegionsEndpoint from "../src/endpoints/regions"; -import {DummyDatalayer} from "./utils/testDataLayer"; +import {DummyDatalayer} from "./testUtils/testDataLayer"; import fetch from "node-fetch"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import fastifySensible from "fastify-sensible"; -import { setupAuth0TestEnv} from "./utils/testify"; -import {DummyRegion} from "./utils/dummyData"; +import { setupAuth0TestEnv} from "./testUtils/testify"; +import {DummyRegion} from "./testUtils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; import {getUserInfo, verifyJwt} from "../src/auth0"; diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index 6a4aef8..d142040 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,4 +1,4 @@ -import {DummyDatalayer} from "./utils/testDataLayer"; +import {DummyDatalayer} from "./testUtils/testDataLayer"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import { createDummyBusiness, @@ -6,8 +6,8 @@ import { dummyAdminToken, DummyBiz, DummyRegion, getDummyBusinesses -} from "./utils/dummyData"; -import {getTestJwtVerifier, setupAuth0TestEnv, testify} from "./utils/testify"; +} from "./testUtils/dummyData"; +import {getTestJwtVerifier, setupAuth0TestEnv, testify} from "./testUtils/testify"; import createRegionsEndpoint from "../src/endpoints/regions"; describe("Business Endpoint Tests", () => { diff --git a/tests/cors.test.ts b/tests/cors.test.ts index beb7f9e..4ba56be 100644 --- a/tests/cors.test.ts +++ b/tests/cors.test.ts @@ -1,6 +1,6 @@ import {registerCorsHandler} from "../src/cors"; -import { testify} from "./utils/testify"; -import {DummyRegion} from "./utils/dummyData"; +import { testify} from "./testUtils/testify"; +import {DummyRegion} from "./testUtils/dummyData"; describe('CORS Handler Tests', function () { diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 319a928..e324091 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -2,8 +2,14 @@ import {productionDataLayer, Region} from "../src/database/productionDataLayer"; import {firestore} from "../src/database/firestore"; import {Business} from "../src/endpoints/businesses"; import objectContaining = jasmine.objectContaining; +import {EditRequest} from "../src/endpoints/editRequest"; +import arrayContaining = jasmine.arrayContaining; +import {DummyBiz} from "./testUtils/dummyData"; describe("Production Data Layer Integration Tests", () => { + const DUMMY_REGION_1 = "DummyRegion"; + const DUMMY_REGION_2 = "DummyRegion2"; + async function deleteRegionsNamed(regionName: string) { (await firestore.collection("regions").where("name", "==", regionName).get()).docs .forEach((doc) => doc.ref.delete()); @@ -12,8 +18,10 @@ describe("Production Data Layer Integration Tests", () => { beforeEach(async(done) => { (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs.forEach((d) => d.ref.delete()); await firestore.collection("years").doc("2019").delete(); - await deleteRegionsNamed("DummyRegion"); - await deleteRegionsNamed("DummyRegion2"); + (await firestore.collection("editRequests").get()).docs.forEach(req => req.ref.delete()); + await deleteRegionsNamed(DUMMY_REGION_1); + await deleteRegionsNamed(DUMMY_REGION_2); + done(); }); @@ -25,7 +33,7 @@ describe("Production Data Layer Integration Tests", () => { industry: "DummyIndustry", year_added: 2019 }; - biz.regionId = (await productionDataLayer.setRegion({name: "DummyRegion", manager: "Dummy"})).id; + biz.regionId = (await productionDataLayer.setRegion({name: DUMMY_REGION_1, manager: "Dummy"})).id; let id = (await productionDataLayer.setBusiness(biz)).id; expect(id).toBeTruthy(); @@ -60,7 +68,7 @@ describe("Production Data Layer Integration Tests", () => { it("Creates, retrieves, updates, and deletes regions", async (done) => { let region: Region = { - name: "DummyRegion", + name: DUMMY_REGION_1, manager: "Dummy Manager" }; @@ -83,7 +91,7 @@ describe("Production Data Layer Integration Tests", () => { expect(newManagerRegions).toEqual(expect.arrayContaining([region])); let region2: Region = { - name: "DummyRegion2", + name: DUMMY_REGION_2, manager: "Manager2" }; region2.id = (await productionDataLayer.setRegion(region2)).id; @@ -99,4 +107,61 @@ describe("Production Data Layer Integration Tests", () => { done(); }); + it("Creates, retrieves, and updates edit requests", async(done) => { + let region: Region = { + name: DUMMY_REGION_1, + manager: "Dummy Manager" + }; + let regionId : string; + regionId = (await productionDataLayer.setRegion(region)).id; + + let testRequest : EditRequest = { + regionId: regionId, + submitter: "", + dateSubmitted: new Date(), + dateUpdated: new Date(), + status: "", + adds:[DummyBiz], + updates: [DummyBiz], + deletes: [], + }; + + let spoilerRequest : EditRequest = { + regionId: `Not${regionId}`, + submitter: "", + dateSubmitted: new Date(), + dateUpdated: new Date(), + status: "", + adds:[DummyBiz], + updates: [DummyBiz], + deletes: [], + }; + + let {id} = await productionDataLayer.createEditRequest(testRequest); + expect(id).toBeTruthy(); + testRequest.id = id; + + let {id:spoilerId} = await productionDataLayer.createEditRequest(spoilerRequest); + expect(spoilerId).toBeTruthy(); + spoilerRequest.id = spoilerId; + + let editRequests = await productionDataLayer.getEditRequestsForRegion(regionId); + expect(editRequests).toBeTruthy(); + expect(editRequests.length).toBe(1); + expect(editRequests).toStrictEqual(arrayContaining([testRequest])); + + editRequests = await productionDataLayer.getAllEditRequests(); + expect(editRequests).toBeTruthy(); + expect(editRequests.length).toBe(2); + expect(editRequests).toStrictEqual(arrayContaining([testRequest, spoilerRequest])) + + let singleRequest = await productionDataLayer.getEditRequestById(id); + expect(singleRequest).toBeTruthy(); + if(!!singleRequest) { + expect(singleRequest).toStrictEqual(testRequest); + expect(singleRequest.regionId).toBe(regionId); + } + + done(); + }); }); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 38dbf8e..36ef7b7 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -1,40 +1,158 @@ -import {DummyDatalayer} from "./utils/testDataLayer"; -import { testify} from "./utils/testify"; -import {createEditEndpoint} from "../src/endpoints/editRequests"; -import {dummyRegionManagerToken, dummyTokenVerifier} from "./utils/dummyData"; +import {DummyDatalayer} from "./testUtils/testDataLayer"; +import { testify} from "./testUtils/testify"; +import {createEditEndpoint, EditRequest} from "../src/endpoints/editRequest"; +import { + dummyAdminToken, + DummyBiz, + DummyRegion, + dummyRegionManagerToken, + dummyTokenVerifier +} from "./testUtils/dummyData"; +import arrayContaining = jasmine.arrayContaining; + +const DummyAdd : EditRequest = { + regionId: DummyRegion.name, + submitter: DummyRegion.manager, + dateSubmitted: new Date(), + dateUpdated: new Date(), + status: "", + adds: [DummyBiz] +} describe("Edit Request unit tests", () => { let testDataLayer: DummyDatalayer - it("Submitted edit requests are seen by region admin", async (done) => { + + it("Cannot submit or view edit requests as an unauthenticated user", async (done) => { testDataLayer = new DummyDatalayer(); const testApp = testify(); let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); - const editResponse = await editEndpoint.inject({ + const postResponse = await editEndpoint.inject({ method: "POST", - url: `/edits`, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`}, - payload: { - adds: [], - updates: [], - deletes: [] - } - }); - - expect(editResponse.statusCode).toBe(201); - expect(JSON.parse(editResponse.payload)).toStrictEqual( - expect.objectContaining({ - editRequests: { - adds: [], - updates: [], - deletes: [] - } - }) - ); + url: `/region/${DummyRegion.name}/edits`, + payload: DummyAdd + }); + expect(postResponse.statusCode).toBe(401); + + const getResponse = await editEndpoint.inject({ + method: "GET", + url: `/region/${DummyRegion.name}/edits` + }) + expect(getResponse.statusCode).toBe(401); await testApp.close(); + done(); + }); + + it("Can submit and view edit requests by region as a region admin", async (done) => { + testDataLayer = new DummyDatalayer(); + const testApp = testify(); + let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); + + const postResponse = await editEndpoint.inject({ + method: "POST", + url: `/region/${DummyRegion.name}/edits`, + payload: DummyAdd, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(postResponse.statusCode).toBe(201); + const responseData = JSON.parse(postResponse.payload); + expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); + const postedEdit = {...DummyAdd, id: responseData.id}; + + const getResponse = await editEndpoint.inject({ + method: "GET", + url: `/region/${DummyRegion.name}/edits`, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }) + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([JSON.parse(JSON.stringify(postedEdit))])); + + await testApp.close(); + done(); + }); + + it("Can submit and view edit requests as a system admin", async (done) => { + testDataLayer = new DummyDatalayer(); + const testApp = testify(); + let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); + + const postResponse = await editEndpoint.inject({ + method: "POST", + url: `/region/${DummyRegion.name}/edits`, + payload: DummyAdd, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + + expect(postResponse.statusCode).toBe(201); + const responseData = JSON.parse(postResponse.payload); + expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); + const postedEdit = {...DummyAdd, id: responseData.id}; + + const getResponse = await editEndpoint.inject({ + method: "GET", + url: `/region/${DummyRegion.name}/edits`, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }) + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([JSON.parse(JSON.stringify(postedEdit))])); + + await testApp.close(); + done(); + }); + + it("Can view all edit requests as a system admin but not as region manager", async(done) => { + testDataLayer = new DummyDatalayer(); + const testApp = testify(); + let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); + + const postResponse1 = await editEndpoint.inject({ + method: "POST", + url: `/region/${DummyRegion.name}/edits`, + payload: DummyAdd, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(postResponse1.statusCode).toBe(201); + let postedRequest1 = {...DummyAdd, id: JSON.parse(postResponse1.payload).id} + + const differentRegionAdd = {...DummyAdd, regionId: `Not${DummyRegion.name}`}; + const postResponse2 = await editEndpoint.inject({ + method: "POST", + url: `/region/${differentRegionAdd.regionId}/edits`, + payload: differentRegionAdd, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(postResponse2.statusCode).toBe(201); + let postedRequest2 = {...differentRegionAdd, id: JSON.parse(postResponse2.payload).id} + + const regionManagerResponse = await editEndpoint.inject({ + method: "GET", + url: `/edits/all`, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(regionManagerResponse.statusCode).toBe(401); + + const byRegionResponse = await editEndpoint.inject({ + method: "GET", + url: `/region/${DummyRegion.name}/edits`, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(JSON.parse(byRegionResponse.payload).editRequests.length).toBe(1); + + const getResponse = await editEndpoint.inject({ + method: "GET", + url: `/edits/all`, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([ + JSON.parse(JSON.stringify(postedRequest1)), + JSON.parse(JSON.stringify(postedRequest2)) + ])); + + await testApp.close(); done(); }); }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index a456bea..cbaac3b 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -1,6 +1,6 @@ import {createFiltersEndpoint} from "../src/endpoints/filters"; import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; -import {DummyDatalayer} from "./utils/testDataLayer"; +import {DummyDatalayer} from "./testUtils/testDataLayer"; import { createDummyBusiness, createDummyRegion, dummyAdminToken, @@ -8,9 +8,9 @@ import { DummyRegion, dummyRegionManagerToken, dummyTokenVerifier -} from "./utils/dummyData"; +} from "./testUtils/dummyData"; import createRegionsEndpoint from "../src/endpoints/regions"; -import {setupAuth0TestEnv, testify} from "./utils/testify"; +import {setupAuth0TestEnv, testify} from "./testUtils/testify"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; diff --git a/tests/regions.test.ts b/tests/regions.test.ts index de650f1..53b9032 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -2,15 +2,15 @@ import createRegionsEndpoint from "../src/endpoints/regions"; import { testify, setupAuth0TestEnv -} from "./utils/testify"; -import {DummyDatalayer} from "./utils/testDataLayer"; +} from "./testUtils/testify"; +import {DummyDatalayer} from "./testUtils/testDataLayer"; import { dummyAdminToken, DummyRegion, dummyRegionManagerToken, getRegionsByDummyManager, dummyTokenVerifier -} from "./utils/dummyData"; +} from "./testUtils/dummyData"; import {Region} from "../src/database/productionDataLayer"; import {FastifyInstance} from "fastify"; diff --git a/tests/utils/dummyData.ts b/tests/testUtils/dummyData.ts similarity index 100% rename from tests/utils/dummyData.ts rename to tests/testUtils/dummyData.ts diff --git a/tests/utils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts similarity index 76% rename from tests/utils/testDataLayer.ts rename to tests/testUtils/testDataLayer.ts index 95aacca..57cbb94 100644 --- a/tests/utils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -1,19 +1,12 @@ import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; import {Business} from "../../src/endpoints/businesses"; -import { AddRequest, UpdateRequest, DeleteRequest } from "../../src/endpoints/editRequests"; +import { EditRequest } from "../../src/endpoints/editRequest"; export class DummyDatalayer implements DataLayer { - createAddRequest(_: AddRequest): Promise { - throw new Error("Method not implemented."); - } - createUpdateRequest(_: UpdateRequest): Promise { - throw new Error("Method not implemented."); - } - createDeleteRequests(_: DeleteRequest): Promise { - throw new Error("Method not implemented."); - } + businesses: Business[] = []; regions: Region[] = []; + editRequests: EditRequest[] = []; getBusinessesByRegion(_:string): Promise { return Promise.resolve(this.businesses); @@ -62,7 +55,23 @@ export class DummyDatalayer implements DataLayer { this.regions = this.regions.filter((r) => r.name !== regionId); } + async createEditRequest(editRequest: EditRequest): Promise { + const newRequest = {...editRequest, id: new Date().toISOString()} + this.editRequests.push(newRequest); + return {id: newRequest.id} ; + } + + async getEditRequestsForRegion(regionId: string): Promise { + return this.editRequests.filter((req) => req.regionId === regionId); + } + + async getAllEditRequests(): Promise { + return this.editRequests; + } + clearRegions() { this.regions = []; } + + } diff --git a/tests/utils/testify.ts b/tests/testUtils/testify.ts similarity index 100% rename from tests/utils/testify.ts rename to tests/testUtils/testify.ts From d15a32e115ea3717ebcf2c81a7e0f3ee25af6071 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 27 Feb 2021 00:16:34 -0330 Subject: [PATCH 42/75] Adding the new edit endpoint to the production server. --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 366011f..5cd9c70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import {createFiltersEndpoint} from "./endpoints/filters"; import {registerCorsHandler} from "./cors"; import {registerSwagger} from "./swagger"; import {verifyJwt} from "./auth0"; +import {createEditEndpoint} from "./endpoints/editRequest"; const port = Number(process.env.PORT || 8080); const server = fastify(); @@ -20,7 +21,8 @@ addRoutes( createPingEndpoint, (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer, verifyJwt), (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer, verifyJwt), - (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, verifyJwt) + (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, verifyJwt), + (app: FastifyInstance) => createEditEndpoint(app, productionDataLayer, verifyJwt) ); server.listen(port, '::', (err, address) => { From 1bde112d6378d1f0fd62244a6452b73106bd8d84 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Sat, 6 Mar 2021 15:11:00 -0330 Subject: [PATCH 43/75] Adding auth0 client secrets to gcloud deployment --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 99b2947..ef557e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -42,3 +42,4 @@ jobs: --region=northamerica-northeast1 \ --allow-unauthenticated \ --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest + --set-env-vars AUTH0_MGMT_CLIENT_SECRET=secrets.AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=secrets.AUTH0_CLIENT_SECRET From 084cb3ab3f52c77fa995a61575158ac5ca9fb001 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 6 Mar 2021 15:13:28 -0330 Subject: [PATCH 44/75] Adding the new management client ID to .env and making a couple of QoL changes. --- .env | 2 +- src/endpoints/docs/regionSchemas.ts | 14 +++++++++----- tests/auth0.test.ts | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 6e16e57..76d0026 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ AUTH0_DOMAIN=lesleychard.auth0.com AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj AUTH0_API_CLIENT_ID=sKHX2LJBIAFvfFDQZPHRWK35eoBVaaQO -AUTH0_MGMT_API_ID= +AUTH0_MGMT_API_ID=mxNP8B5JnQ4v049Vt2mR80z9rvov5yfj AUTH0_CLAIMS_NAMESPACE=https://lesleychard diff --git a/src/endpoints/docs/regionSchemas.ts b/src/endpoints/docs/regionSchemas.ts index 1fee9dc..e14d5b4 100644 --- a/src/endpoints/docs/regionSchemas.ts +++ b/src/endpoints/docs/regionSchemas.ts @@ -1,10 +1,15 @@ import {filtersSchema} from "./filterSchemas"; import {byRegionIdSchema} from "./basicSchemas"; +const managerIdSchema = { + type: 'string', + description: "The second half of the auth0 sub claim. For example, for a user with {sub: 'auth0|123456'}, their manager ID is '123456'" +}; + const byManagerIdSchema = { type: 'object', properties: { - managerId: {type: 'string'} + managerId: managerIdSchema } }; @@ -13,7 +18,7 @@ const getRegionSchema = { properties: { id: {type: "string"}, name: {type: "string"}, - manager: {type: "string"}, + manager: managerIdSchema, filters: filtersSchema } }; @@ -22,7 +27,7 @@ const createRegionSchema = { type: 'object', properties: { name: {type: "string"}, - manager: {type: "string"} + manager: managerIdSchema } }; @@ -33,7 +38,7 @@ const updateRegionSchema = { nullable: true }, name: {type: "string"}, - manager: {type: "string"} + manager: managerIdSchema } @@ -52,7 +57,6 @@ export const getManagedRegionsReqSchema = { type: 'array', items: getRegionSchema } - } } } diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index d4f3e9d..c7a876c 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -126,7 +126,8 @@ describe("Auth0 integration tests", () => { grant_type: "password" }) }); - userAccessToken = (await userResponse.json()).access_token; + let userJson = await userResponse.json() + userAccessToken = userJson.access_token; let adminResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: {'content-type': 'application/json'}, From 55d1b64b711c2bcee49bed021e1051fe14c81593 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Sat, 6 Mar 2021 15:20:31 -0330 Subject: [PATCH 45/75] Syntax fix Missed a line continuation. --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ef557e8..c55bacd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,5 +41,5 @@ jobs: --platform=managed \ --region=northamerica-northeast1 \ --allow-unauthenticated \ - --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest + --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest \ --set-env-vars AUTH0_MGMT_CLIENT_SECRET=secrets.AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=secrets.AUTH0_CLIENT_SECRET From 2a4e57dbb2cfe389555cbe10bfb8b7bad7265c69 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Sat, 6 Mar 2021 17:30:47 -0330 Subject: [PATCH 46/75] Attempting to fix the secrets Moving secrets to environment variable format --- .github/workflows/deploy.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c55bacd..901313d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,6 +11,8 @@ jobs: BRANCH: ${GITHUB_REF##*/} SERVICE_NAME: ${{ secrets.SERVICE_NAME }} PROJECT_ID: ${{ secrets.PROJECT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} + AUTH0_MGMT_CLIENT_SECRET: ${{ secrets.AUTH0_MGMT_CLIENT_SECRET }} steps: - name: Checkout uses: actions/checkout@v2 @@ -42,4 +44,4 @@ jobs: --region=northamerica-northeast1 \ --allow-unauthenticated \ --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest \ - --set-env-vars AUTH0_MGMT_CLIENT_SECRET=secrets.AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=secrets.AUTH0_CLIENT_SECRET + --set-env-vars AUTH0_MGMT_CLIENT_SECRET=$AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=$AUTH0_CLIENT_SECRET From 8d7652035528647ff48848c1b2f1c00e7c568b46 Mon Sep 17 00:00:00 2001 From: BurtonTechnical <74158405+BurtonTechnical@users.noreply.github.com> Date: Sun, 7 Mar 2021 00:44:55 -0330 Subject: [PATCH 47/75] Adding all of the required environment variables --- .github/workflows/deploy.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 901313d..68b4a48 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,7 @@ on: types: [published] jobs: build: + environment: "GC Run" name: Build image runs-on: ubuntu-latest env: @@ -13,6 +14,10 @@ jobs: PROJECT_ID: ${{ secrets.PROJECT_ID }} AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} AUTH0_MGMT_CLIENT_SECRET: ${{ secrets.AUTH0_MGMT_CLIENT_SECRET }} + AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_MGMT_CLIENT_ID: ${{ secrets.AUTH0_MGMT_CLIENT_ID }} + AUTH0_CLAIMS_NAMESPACE: ${{ secrets.AUTH0_CLAIMS_NAMESPACE }} steps: - name: Checkout uses: actions/checkout@v2 @@ -44,4 +49,4 @@ jobs: --region=northamerica-northeast1 \ --allow-unauthenticated \ --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest \ - --set-env-vars AUTH0_MGMT_CLIENT_SECRET=$AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=$AUTH0_CLIENT_SECRET + --set-env-vars AUTH0_MGMT_CLIENT_SECRET=$AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=$AUTH0_CLIENT_SECRET,AUTH0_DOMAIN=$AUTH0_DOMAIN,AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID,AUTH0_MGMT_CLIENT_ID=$AUTH0_MGMT_CLIENT_ID,AUTH0_CLAIMS_NAMESPACE=$AUTH0_CLAIMS_NAMESPACE From f6dc897f85be487c74e544988d1faca30814fd6d Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 7 Mar 2021 19:26:46 -0330 Subject: [PATCH 48/75] Adding endpoint for getting a single edit request by id along with relevant tests. Refactored edit request tests Fixed an issue with test handling for bad bearer tokens --- .env | 3 +- Dockerfile | 2 +- src/database/productionDataLayer.ts | 1 + src/endpoints/docs/basicSchemas.ts | 10 ++ src/endpoints/docs/editRequestSchemas.ts | 16 ++- src/endpoints/editRequest.ts | 21 ++- src/endpoints/endpointUtils.ts | 9 +- src/endpoints/regions.ts | 38 +++--- src/index.ts | 2 +- tests/editRequests.test.ts | 158 ++++++++++++----------- tests/testUtils/dummyData.ts | 2 +- tests/testUtils/testDataLayer.ts | 5 +- tests/testUtils/testify.ts | 2 +- 13 files changed, 168 insertions(+), 101 deletions(-) diff --git a/.env b/.env index 76d0026..c7b14f7 100644 --- a/.env +++ b/.env @@ -1,5 +1,4 @@ AUTH0_DOMAIN=lesleychard.auth0.com AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj -AUTH0_API_CLIENT_ID=sKHX2LJBIAFvfFDQZPHRWK35eoBVaaQO -AUTH0_MGMT_API_ID=mxNP8B5JnQ4v049Vt2mR80z9rvov5yfj +AUTH0_MGMT_CLIENT_ID=mxNP8B5JnQ4v049Vt2mR80z9rvov5yfj AUTH0_CLAIMS_NAMESPACE=https://lesleychard diff --git a/Dockerfile b/Dockerfile index fce382d..5853b65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ COPY . . RUN yarn build # Run the web service on container startup. -CMD [ "npm", "start" ] +CMD [ "yarn", "start" ] diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index af943a1..572a006 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -36,6 +36,7 @@ export interface DataLayer { createEditRequest(add: EditRequest): Promise; getEditRequestsForRegion(regionId: string): Promise; getAllEditRequests(): Promise; + getEditRequestById(id: string): Promise; } export class ProductionDataLayer implements DataLayer { diff --git a/src/endpoints/docs/basicSchemas.ts b/src/endpoints/docs/basicSchemas.ts index be2be35..5d2a365 100644 --- a/src/endpoints/docs/basicSchemas.ts +++ b/src/endpoints/docs/basicSchemas.ts @@ -1,3 +1,13 @@ +export const byIdSchema = { + type: 'object', + properties: { + id: { + type: 'string', + description: 'The ID of the item to retrieve' + } + } +}; + export const byRegionIdSchema = { type: 'object', properties: { diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index 10e9ddd..f430bdc 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -1,4 +1,4 @@ -import {byRegionIdSchema} from "./basicSchemas"; +import {byIdSchema, byRegionIdSchema} from "./basicSchemas"; import { businessSchema} from "./businessesSchemas"; const editRequestSchema = { @@ -28,6 +28,20 @@ import { businessSchema} from "./businessesSchemas"; } } +export const getEditRequestByIdSchema = { + params: byIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + editRequest: editRequestSchema + } + } + } +}; + export const getEditRequestsByRegionSchema = { params: byRegionIdSchema, response: { diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 44a01a6..af0ac52 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -2,7 +2,7 @@ import {FastifyInstance } from "fastify"; import {DataLayer} from "../database/productionDataLayer"; import {Business} from "./businesses"; import {Auth0JwtVerifier} from "../auth0"; -import {AuthenticatedRequest, AuthenticatedRequestByRegionId} from "./endpointUtils"; +import {AuthenticatedRequest, AuthenticatedRequestById, AuthenticatedRequestByRegionId} from "./endpointUtils"; import {isRegionManager} from "../utils"; import { createEditRequestSchema, @@ -27,6 +27,25 @@ interface CreateEditRequest extends AuthenticatedRequestByRegionId { } export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { + app.get( + `/edits/:id`, + {schema: getEditRequestsByRegionSchema}, + async (request, reply) => { + let {userAppId, admin} = await verifyJwt(request); + if(!admin && !userAppId) { + reply.unauthorized("Must be logged in to submit requests"); + return; + } else { + let response = { + status: "ok", + editRequest: null + } + response.editRequest = await dataLayer.getEditRequestById(request.params.id) + return JSON.stringify(response); + } + } + ); + app.get( `/region/:regionId/edits`, {schema: getEditRequestsByRegionSchema}, diff --git a/src/endpoints/endpointUtils.ts b/src/endpoints/endpointUtils.ts index 0abf40a..96ff239 100644 --- a/src/endpoints/endpointUtils.ts +++ b/src/endpoints/endpointUtils.ts @@ -9,8 +9,11 @@ export interface AuthenticatedRequest extends RequestGenericInterface { export interface AuthenticatedRequestByRegionId extends AuthenticatedRequest { Params: { regionId: string - }, - Headers: { - access_token: string + } +} + +export interface AuthenticatedRequestById extends AuthenticatedRequest { + Params: { + id: string } } diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index a19f7c4..eb10bde 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -66,25 +66,29 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : '/regions/:regionId', {schema: getSingleRegionReqSchema}, async(request, reply ) => { - - let {userAppId, admin} = await verifyJwt(request); - let response = { - status: "ok", - date: Date.now(), - region: null - } - let regions : Region[] ; - if(admin) { - regions = (await dataLayer.getAllRegions()); - } else if (!userAppId) { - reply.unauthorized(); + try { + let {userAppId, admin} = await verifyJwt(request); + let response = { + status: "ok", + date: Date.now(), + region: null + } + let regions: Region[]; + if (admin) { + regions = (await dataLayer.getAllRegions()); + } else if (!userAppId) { + reply.unauthorized(); + return; + } else { + regions = (await dataLayer.getRegionsManagedBy(userAppId)); + } + let region: Region | undefined = regions.find((r => r.name == request.params.regionId)) + response.region = !!region ? region : null; + return JSON.stringify(response); + } catch (e) { + reply.badRequest(`Error during request: ${JSON.stringify(e)}`); return; - } else { - regions = (await dataLayer.getRegionsManagedBy(userAppId)); } - let region: Region | undefined = regions.find((r => r.name == request.params.regionId)) - response.region = !!region ? region : null; - return JSON.stringify(response); } ); diff --git a/src/index.ts b/src/index.ts index 5cd9c70..9162499 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import {verifyJwt} from "./auth0"; import {createEditEndpoint} from "./endpoints/editRequest"; const port = Number(process.env.PORT || 8080); -const server = fastify(); +const server = fastify({logger: true}); server.register(fastifySensible); registerSwagger(server); registerCorsHandler(server); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 36ef7b7..88bc438 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -9,6 +9,7 @@ import { dummyTokenVerifier } from "./testUtils/dummyData"; import arrayContaining = jasmine.arrayContaining; +import {FastifyInstance} from "fastify"; const DummyAdd : EditRequest = { regionId: DummyRegion.name, @@ -22,17 +23,17 @@ const DummyAdd : EditRequest = { describe("Edit Request unit tests", () => { let testDataLayer: DummyDatalayer + let testApp : FastifyInstance; + let editEndpoint : FastifyInstance; - it("Cannot submit or view edit requests as an unauthenticated user", async (done) => { + beforeEach(() => { testDataLayer = new DummyDatalayer(); - const testApp = testify(); - let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); + testApp = testify(); + editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); + }); - const postResponse = await editEndpoint.inject({ - method: "POST", - url: `/region/${DummyRegion.name}/edits`, - payload: DummyAdd - }); + it("Cannot submit or view edit requests as an unauthenticated user", async (done) => { + const postResponse = await submitEditRequest(DummyAdd, DummyRegion.name, ""); expect(postResponse.statusCode).toBe(401); const getResponse = await editEndpoint.inject({ @@ -45,85 +46,72 @@ describe("Edit Request unit tests", () => { done(); }); - it("Can submit and view edit requests by region as a region admin", async (done) => { - testDataLayer = new DummyDatalayer(); - const testApp = testify(); - let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); - - const postResponse = await editEndpoint.inject({ - method: "POST", - url: `/region/${DummyRegion.name}/edits`, - payload: DummyAdd, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`} - }); + it("Can view a single edit request by request id as region or system admin", async(done) => { + async function getRequestById(requestId: string, token: string) : Promise { + return await editEndpoint.inject({ + method: "GET", + url: `/edits/${requestId}`, + headers: {authorization: `Bearer ${token}`} + }); + } + const postResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(postResponse.statusCode).toBe(201); const responseData = JSON.parse(postResponse.payload); expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); - const postedEdit = {...DummyAdd, id: responseData.id}; - const getResponse = await editEndpoint.inject({ - method: "GET", - url: `/region/${DummyRegion.name}/edits`, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`} - }) - expect(getResponse.statusCode).toBe(200); - expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([JSON.parse(JSON.stringify(postedEdit))])); + const regionManagerResponse = await getRequestById(responseData.id, dummyRegionManagerToken); + expect(regionManagerResponse.statusCode).toBe(200); + const postedEdit = asResponse({...DummyAdd, id: responseData.id}); + expect(JSON.parse(regionManagerResponse.payload).editRequest).toStrictEqual(postedEdit); + + const adminResponse = await getRequestById(responseData.id, dummyAdminToken); + expect(adminResponse.statusCode).toBe(200); + expect(JSON.parse(adminResponse.payload).editRequest).toStrictEqual(postedEdit); await testApp.close(); done(); }); - it("Can submit and view edit requests as a system admin", async (done) => { - testDataLayer = new DummyDatalayer(); - const testApp = testify(); - let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); + describe("Edit Requests by Region", () => { + it("Can submit and view edit requests by region as a region admin", async (done) => { + const postResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); + expect(postResponse.statusCode).toBe(201); + const responseData = JSON.parse(postResponse.payload); + expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); - const postResponse = await editEndpoint.inject({ - method: "POST", - url: `/region/${DummyRegion.name}/edits`, - payload: DummyAdd, - headers: {authorization: `Bearer ${dummyAdminToken}`} + const postedEdit : EditRequest = {...DummyAdd, id: responseData.id}; + const getResponse = await getEditRequestsByRegion(DummyRegion.name, dummyRegionManagerToken) + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([asResponse(postedEdit)])); + + await testApp.close(); + done(); }); - expect(postResponse.statusCode).toBe(201); - const responseData = JSON.parse(postResponse.payload); - expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); - const postedEdit = {...DummyAdd, id: responseData.id}; + it("Can submit and view edit requests as a system admin", async (done) => { + const postResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyAdminToken); + expect(postResponse.statusCode).toBe(201); + const responseData = JSON.parse(postResponse.payload); + expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); + const postedEdit = {...DummyAdd, id: responseData.id}; - const getResponse = await editEndpoint.inject({ - method: "GET", - url: `/region/${DummyRegion.name}/edits`, - headers: {authorization: `Bearer ${dummyAdminToken}`} - }) - expect(getResponse.statusCode).toBe(200); - expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([JSON.parse(JSON.stringify(postedEdit))])); + const getResponse = await getEditRequestsByRegion(DummyRegion.name, dummyAdminToken); + expect(getResponse.statusCode).toBe(200); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([asResponse(postedEdit)])); - await testApp.close(); - done(); + await testApp.close(); + done(); + }); }); it("Can view all edit requests as a system admin but not as region manager", async(done) => { - testDataLayer = new DummyDatalayer(); - const testApp = testify(); - let editEndpoint = createEditEndpoint(testApp, testDataLayer, dummyTokenVerifier); - - const postResponse1 = await editEndpoint.inject({ - method: "POST", - url: `/region/${DummyRegion.name}/edits`, - payload: DummyAdd, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`} - }); + const postResponse1 = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(postResponse1.statusCode).toBe(201); let postedRequest1 = {...DummyAdd, id: JSON.parse(postResponse1.payload).id} const differentRegionAdd = {...DummyAdd, regionId: `Not${DummyRegion.name}`}; - const postResponse2 = await editEndpoint.inject({ - method: "POST", - url: `/region/${differentRegionAdd.regionId}/edits`, - payload: differentRegionAdd, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`} - }); + const postResponse2 = await submitEditRequest(differentRegionAdd, differentRegionAdd.regionId, dummyRegionManagerToken); expect(postResponse2.statusCode).toBe(201); let postedRequest2 = {...differentRegionAdd, id: JSON.parse(postResponse2.payload).id} @@ -134,11 +122,7 @@ describe("Edit Request unit tests", () => { }); expect(regionManagerResponse.statusCode).toBe(401); - const byRegionResponse = await editEndpoint.inject({ - method: "GET", - url: `/region/${DummyRegion.name}/edits`, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`} - }); + const byRegionResponse = await getEditRequestsByRegion(DummyRegion.name, dummyRegionManagerToken); expect(JSON.parse(byRegionResponse.payload).editRequests.length).toBe(1); const getResponse = await editEndpoint.inject({ @@ -148,11 +132,41 @@ describe("Edit Request unit tests", () => { }); expect(getResponse.statusCode).toBe(200); expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([ - JSON.parse(JSON.stringify(postedRequest1)), - JSON.parse(JSON.stringify(postedRequest2)) + asResponse(postedRequest1), + asResponse(postedRequest2) ])); await testApp.close(); done(); }); + + async function submitEditRequest(request: EditRequest, regionId: string, token: string) { + let postOptions = { + method: <"POST">"POST", + url: `/region/${regionId}/edits`, + payload: request, + headers: {} + } + if(!!token) { + postOptions.headers = {authorization: `Bearer ${token}`} + } + return await editEndpoint.inject(postOptions); + } + + async function getEditRequestsByRegion(regionId: string, token: string) { + return await editEndpoint.inject({ + method: "GET", + url: `/region/${regionId}/edits`, + headers: {authorization: `Bearer ${token}`} + }); + } + +/** + * Simulates the standard data conversion when data is submitted in a request and then received in a response + * @param rawEdit - the edit data in its original pre-submission + * @return - the same data, but having been stringified and then parsed again. This has implicates for, for example, Date strings + */ + function asResponse(rawEdit: EditRequest) : EditRequest { + return JSON.parse(JSON.stringify(rawEdit)); + } }); diff --git a/tests/testUtils/dummyData.ts b/tests/testUtils/dummyData.ts index d81354e..d43726d 100644 --- a/tests/testUtils/dummyData.ts +++ b/tests/testUtils/dummyData.ts @@ -21,7 +21,7 @@ export const DummyBiz: Business = { }; export async function dummyTokenVerifier (req: FastifyRequest) { - if(!req.headers.authorization) { + if(!req.headers.authorization || !req.headers.authorization.split("Bearer")[1].trim()) { return {userAppId: "", admin: false}; } if(req.headers.authorization.indexOf(dummyAdminToken) > 0) { diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 57cbb94..e8b559a 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -69,9 +69,12 @@ export class DummyDatalayer implements DataLayer { return this.editRequests; } + async getEditRequestById(id: string): Promise { + return this.editRequests.filter((req) => req.id === id)[0]; + } + clearRegions() { this.regions = []; } - } diff --git a/tests/testUtils/testify.ts b/tests/testUtils/testify.ts index 25135b9..567fcaa 100644 --- a/tests/testUtils/testify.ts +++ b/tests/testUtils/testify.ts @@ -31,7 +31,7 @@ export function getTestJwtVerifier(userAppId: string, admin: boolean) { } export const testify = () => { - const f = fastify(); + const f = fastify({logger: {level: "debug"}}); f.register(fastifyJWT, { secret: (_request, _reply, _provider) => { _provider(null, mockSecret);}, audience: 'https://localhost', From e964eb6bc7ab338067270e0babb90ba55873b265 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 7 Mar 2021 23:55:55 -0330 Subject: [PATCH 49/75] Added update endpoint for edit requests and related tests --- src/database/productionDataLayer.ts | 14 +++- src/endpoints/docs/editRequestSchemas.ts | 15 ++++ src/endpoints/editRequest.ts | 32 +++++++- tests/dataLayer.test.ts | 32 +++++++- tests/editRequests.test.ts | 99 +++++++++++++++++------- tests/testUtils/testDataLayer.ts | 7 +- 6 files changed, 161 insertions(+), 38 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 572a006..9704979 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -37,6 +37,7 @@ export interface DataLayer { getEditRequestsForRegion(regionId: string): Promise; getAllEditRequests(): Promise; getEditRequestById(id: string): Promise; + updateEditRequest(body: EditRequest): Promise; } export class ProductionDataLayer implements DataLayer { @@ -160,8 +161,16 @@ export class ProductionDataLayer implements DataLayer { } async getEditRequestById(id: string) : Promise { - firestore.collection("editRequests").doc(id); - return this.convertToEditRequest(id, (await firestore.collection("editRequests").doc(id).get()).data()); + let requestData = (await firestore.collection("editRequests").doc(id).get()).data(); + return this.convertToEditRequest(id, requestData); + } + + async updateEditRequest(body: EditRequest): Promise { + let id = !!body.id ? body.id : ""; + let requestData = await this.getEditRequestById(id); + let updatedRequestData = {...requestData, ...body}; + await firestore.collection("editRequests").doc(id).set(updatedRequestData) + return Promise.resolve(updatedRequestData); } convertToEditRequest(id: string, documentData: firebase.firestore.DocumentData | undefined) : EditRequest | null { @@ -236,7 +245,6 @@ export class ProductionDataLayer implements DataLayer { return {}; } } - } export const productionDataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index f430bdc..b678b9f 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -42,6 +42,21 @@ export const getEditRequestByIdSchema = { } }; +export const updateEditRequestSchema = { + params: byIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + editRequest: editRequestSchema + } + } + } +} + + export const getEditRequestsByRegionSchema = { params: byRegionIdSchema, response: { diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index af0ac52..7f8f490 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -6,8 +6,8 @@ import {AuthenticatedRequest, AuthenticatedRequestById, AuthenticatedRequestByRe import {isRegionManager} from "../utils"; import { createEditRequestSchema, - getAllEditRequestsSchema, - getEditRequestsByRegionSchema + getAllEditRequestsSchema, getEditRequestByIdSchema, + getEditRequestsByRegionSchema, updateEditRequestSchema } from "./docs/editRequestSchemas"; export interface EditRequest { @@ -16,7 +16,7 @@ export interface EditRequest { submitter: string, dateSubmitted: Date | string, dateUpdated: Date | string - status: string, + status?: string, adds?: Business[], updates?: Business[] | undefined, deletes?: string[] | undefined @@ -26,10 +26,14 @@ interface CreateEditRequest extends AuthenticatedRequestByRegionId { Body: EditRequest } +interface UpdateEditRequest extends AuthenticatedRequestById { + Body: EditRequest +} + export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { app.get( `/edits/:id`, - {schema: getEditRequestsByRegionSchema}, + {schema: getEditRequestByIdSchema}, async (request, reply) => { let {userAppId, admin} = await verifyJwt(request); if(!admin && !userAppId) { @@ -45,6 +49,25 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } } ); + app.post( + `/edits/:id`, + {schema: updateEditRequestSchema}, + async(request, reply) => { + let {userAppId, admin} = await verifyJwt(request); + if(!admin && !!request.body.status) { + reply.unauthorized("Only system administrators may update edit request statuses!"); + return; + } else if (!admin && !(await isRegionManager(userAppId, request.body.regionId, dataLayer))) { + reply.unauthorized("Only administrators and managers may update edit requests!"); + return; + } else { + return { + status: "ok", + editRequest: await dataLayer.updateEditRequest(request.body) + }; + } + } + ); app.get( `/region/:regionId/edits`, @@ -93,6 +116,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v return; } else { const incomingRequest = request.body; + incomingRequest.status = "In Progress"; let response = { status: "ok", id: (await dataLayer.createEditRequest(incomingRequest)).id diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index e324091..25765f3 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -9,6 +9,7 @@ import {DummyBiz} from "./testUtils/dummyData"; describe("Production Data Layer Integration Tests", () => { const DUMMY_REGION_1 = "DummyRegion"; const DUMMY_REGION_2 = "DummyRegion2"; + let regionId : string; async function deleteRegionsNamed(regionName: string) { (await firestore.collection("regions").where("name", "==", regionName).get()).docs @@ -112,7 +113,6 @@ describe("Production Data Layer Integration Tests", () => { name: DUMMY_REGION_1, manager: "Dummy Manager" }; - let regionId : string; regionId = (await productionDataLayer.setRegion(region)).id; let testRequest : EditRequest = { @@ -120,7 +120,7 @@ describe("Production Data Layer Integration Tests", () => { submitter: "", dateSubmitted: new Date(), dateUpdated: new Date(), - status: "", + status: "In Progress", adds:[DummyBiz], updates: [DummyBiz], deletes: [], @@ -131,7 +131,7 @@ describe("Production Data Layer Integration Tests", () => { submitter: "", dateSubmitted: new Date(), dateUpdated: new Date(), - status: "", + status: "In Progress", adds:[DummyBiz], updates: [DummyBiz], deletes: [], @@ -162,6 +162,32 @@ describe("Production Data Layer Integration Tests", () => { expect(singleRequest.regionId).toBe(regionId); } + let updateRequest = { + id: testRequest.id, + status: "Approved", + regionId: testRequest.regionId, + dateSubmitted: testRequest.dateSubmitted, + dateUpdated: new Date(), + submitter: testRequest.submitter + }; + + let requestAfterUpdate = await productionDataLayer.updateEditRequest(updateRequest); + expect(requestAfterUpdate).toStrictEqual( + objectContaining({ + ...testRequest, + ...updateRequest + }) + ); + + let readRequestAfterUpdate = await productionDataLayer.getEditRequestById(testRequest.id); + expect(readRequestAfterUpdate).toBeTruthy(); + expect(readRequestAfterUpdate).toStrictEqual( + objectContaining({ + ...testRequest, + ...updateRequest + }) + ); + done(); }); }); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 88bc438..a0b5136 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -10,6 +10,7 @@ import { } from "./testUtils/dummyData"; import arrayContaining = jasmine.arrayContaining; import {FastifyInstance} from "fastify"; +import objectContaining = jasmine.objectContaining; const DummyAdd : EditRequest = { regionId: DummyRegion.name, @@ -36,10 +37,7 @@ describe("Edit Request unit tests", () => { const postResponse = await submitEditRequest(DummyAdd, DummyRegion.name, ""); expect(postResponse.statusCode).toBe(401); - const getResponse = await editEndpoint.inject({ - method: "GET", - url: `/region/${DummyRegion.name}/edits` - }) + const getResponse = await getEditRequestsByRegion(DummyRegion.name, ""); expect(getResponse.statusCode).toBe(401); await testApp.close(); @@ -47,14 +45,6 @@ describe("Edit Request unit tests", () => { }); it("Can view a single edit request by request id as region or system admin", async(done) => { - async function getRequestById(requestId: string, token: string) : Promise { - return await editEndpoint.inject({ - method: "GET", - url: `/edits/${requestId}`, - headers: {authorization: `Bearer ${token}`} - }); - } - const postResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(postResponse.statusCode).toBe(201); const responseData = JSON.parse(postResponse.payload); @@ -62,7 +52,7 @@ describe("Edit Request unit tests", () => { const regionManagerResponse = await getRequestById(responseData.id, dummyRegionManagerToken); expect(regionManagerResponse.statusCode).toBe(200); - const postedEdit = asResponse({...DummyAdd, id: responseData.id}); + const postedEdit = asResponse(asInitializedEditRequest(DummyAdd, responseData.id)); expect(JSON.parse(regionManagerResponse.payload).editRequest).toStrictEqual(postedEdit); const adminResponse = await getRequestById(responseData.id, dummyAdminToken); @@ -80,10 +70,10 @@ describe("Edit Request unit tests", () => { const responseData = JSON.parse(postResponse.payload); expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); - const postedEdit : EditRequest = {...DummyAdd, id: responseData.id}; + const postedEdit = asResponse(asInitializedEditRequest(DummyAdd, responseData.id)); const getResponse = await getEditRequestsByRegion(DummyRegion.name, dummyRegionManagerToken) expect(getResponse.statusCode).toBe(200); - expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([asResponse(postedEdit)])); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([postedEdit])); await testApp.close(); done(); @@ -94,11 +84,11 @@ describe("Edit Request unit tests", () => { expect(postResponse.statusCode).toBe(201); const responseData = JSON.parse(postResponse.payload); expect(responseData).toStrictEqual(expect.objectContaining({ id: responseData.id})); - const postedEdit = {...DummyAdd, id: responseData.id}; + const postedEdit = asResponse(asInitializedEditRequest(DummyAdd,responseData.id)); const getResponse = await getEditRequestsByRegion(DummyRegion.name, dummyAdminToken); expect(getResponse.statusCode).toBe(200); - expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([asResponse(postedEdit)])); + expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([postedEdit])); await testApp.close(); done(); @@ -106,14 +96,20 @@ describe("Edit Request unit tests", () => { }); it("Can view all edit requests as a system admin but not as region manager", async(done) => { + function getAllEditRequests(token: string) { + return editEndpoint.inject({ + method: "GET", + url: `/edits/all`, + headers: {authorization: `Bearer ${token}`} + }); + } + const postResponse1 = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(postResponse1.statusCode).toBe(201); - let postedRequest1 = {...DummyAdd, id: JSON.parse(postResponse1.payload).id} const differentRegionAdd = {...DummyAdd, regionId: `Not${DummyRegion.name}`}; const postResponse2 = await submitEditRequest(differentRegionAdd, differentRegionAdd.regionId, dummyRegionManagerToken); expect(postResponse2.statusCode).toBe(201); - let postedRequest2 = {...differentRegionAdd, id: JSON.parse(postResponse2.payload).id} const regionManagerResponse = await editEndpoint.inject({ method: "GET", @@ -125,21 +121,66 @@ describe("Edit Request unit tests", () => { const byRegionResponse = await getEditRequestsByRegion(DummyRegion.name, dummyRegionManagerToken); expect(JSON.parse(byRegionResponse.payload).editRequests.length).toBe(1); - const getResponse = await editEndpoint.inject({ - method: "GET", - url: `/edits/all`, - headers: {authorization: `Bearer ${dummyAdminToken}`} - }); + const getResponse = await getAllEditRequests(dummyAdminToken); expect(getResponse.statusCode).toBe(200); + let postedRequest1 = asResponse(asInitializedEditRequest(DummyAdd, JSON.parse(postResponse1.payload).id)); + let postedRequest2 = asResponse(asInitializedEditRequest(differentRegionAdd, JSON.parse(postResponse2.payload).id)); expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([ - asResponse(postedRequest1), - asResponse(postedRequest2) + postedRequest1, + postedRequest2 ])); await testApp.close(); done(); }); + it("Can only update edit request status as sysadmin", async(done) => { + async function updateEditRequestStatus(editRequest: EditRequest, newStatus: string, token: string) : Promise { + return testApp.inject({ + method: "POST", + url: `/edits/${editRequest.id}`, + payload: {...editRequest, status: newStatus}, + headers: { + authorization: `Bearer ${token}` + } + }); + } + + const submitResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); + expect(submitResponse.statusCode).toBe(201); + let createdRequest = {...DummyAdd, id: JSON.parse(submitResponse.payload).id}; + + const unauthorizedResponse = await updateEditRequestStatus(createdRequest, "Approved", ""); + expect(unauthorizedResponse.statusCode).toBe(401); + + const regionManagerResponse = await updateEditRequestStatus(createdRequest, "Approved", dummyRegionManagerToken); + expect(regionManagerResponse.statusCode).toBe(401); + + const updateResponse = await updateEditRequestStatus(createdRequest, "Approved", dummyAdminToken); + expect(updateResponse.statusCode).toBe(200); + + const updatedRequest = asResponse({ + ...createdRequest, + status: "Approved" + }); + expect(JSON.parse(updateResponse.payload).editRequest).toStrictEqual(objectContaining(updatedRequest)); + + const postUpdateRead = await getRequestById(createdRequest.id, dummyRegionManagerToken); + expect(postUpdateRead.statusCode).toBe(200); + expect(JSON.parse(postUpdateRead.payload).editRequest).toStrictEqual(objectContaining(updatedRequest)); + + await testApp.close(); + done(); + }); + + async function getRequestById(requestId: string, token: string) : Promise { + return await editEndpoint.inject({ + method: "GET", + url: `/edits/${requestId}`, + headers: {authorization: `Bearer ${token}`} + }); + } + async function submitEditRequest(request: EditRequest, regionId: string, token: string) { let postOptions = { method: <"POST">"POST", @@ -161,7 +202,7 @@ describe("Edit Request unit tests", () => { }); } -/** + /** * Simulates the standard data conversion when data is submitted in a request and then received in a response * @param rawEdit - the edit data in its original pre-submission * @return - the same data, but having been stringified and then parsed again. This has implicates for, for example, Date strings @@ -169,4 +210,8 @@ describe("Edit Request unit tests", () => { function asResponse(rawEdit: EditRequest) : EditRequest { return JSON.parse(JSON.stringify(rawEdit)); } + + function asInitializedEditRequest(rawEdit: EditRequest, id: string) : EditRequest { + return {...rawEdit, id, status: "In Progress"}; + } }); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index e8b559a..cc96b86 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -73,8 +73,13 @@ export class DummyDatalayer implements DataLayer { return this.editRequests.filter((req) => req.id === id)[0]; } + async updateEditRequest(body: EditRequest): Promise { + let index = this.editRequests.findIndex((req) => req.id === body.id); + this.editRequests[index] = {...this.editRequests[index], ...body}; + return this.editRequests[index]; + } + clearRegions() { this.regions = []; } - } From 403eb177ffc60addb077bcdbe6e75f185b03033d Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 8 Mar 2021 20:22:35 -0330 Subject: [PATCH 50/75] Adding queries by pending and reviewed statuses --- src/database/productionDataLayer.ts | 14 +++++ src/endpoints/editRequest.ts | 29 ++++++++-- tests/dataLayer.test.ts | 26 ++++++--- tests/editRequests.test.ts | 85 +++++++++++++++++++++-------- tests/testUtils/testDataLayer.ts | 7 ++- 5 files changed, 124 insertions(+), 37 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 9704979..2c8b907 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -38,6 +38,7 @@ export interface DataLayer { getAllEditRequests(): Promise; getEditRequestById(id: string): Promise; updateEditRequest(body: EditRequest): Promise; + getEditRequestsByStatus(pending: string): Promise; } export class ProductionDataLayer implements DataLayer { @@ -173,6 +174,19 @@ export class ProductionDataLayer implements DataLayer { return Promise.resolve(updatedRequestData); } + async getEditRequestsByStatus(status: string): Promise { + let requests = []; + (await firestore.collection("editRequests").where("status", "==", status).get()).docs.forEach( + (req) => { + let converted = this.convertToEditRequest(req.id, req.data()); + if (!!converted) { + requests.push(converted); + } + } + ); + return requests; + } + convertToEditRequest(id: string, documentData: firebase.firestore.DocumentData | undefined) : EditRequest | null { if(!!documentData) { let request = documentData; diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 7f8f490..6f62fb4 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -10,6 +10,9 @@ import { getEditRequestsByRegionSchema, updateEditRequestSchema } from "./docs/editRequestSchemas"; +export const URL_PENDING_EDIT_REQUESTS = "/edits/status/pending"; +export const URL_REVIEWED_EDIT_REQUESTS = "/edits/status/reviewed"; + export interface EditRequest { id?: string, regionId: string, @@ -63,7 +66,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } else { return { status: "ok", - editRequest: await dataLayer.updateEditRequest(request.body) + editRequest: await dataLayer.updateEditRequest({...request.body, id: request.params.id}) }; } } @@ -89,7 +92,25 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v ); app.get( - "/edits/all", + URL_PENDING_EDIT_REQUESTS, + {schema: getAllEditRequestsSchema}, + async (request, reply) => { + let { admin } = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Only admins may access this data!"); + return; + } else { + let response = { + status: "ok", + editRequests: await dataLayer.getEditRequestsByStatus("Pending") + } + return JSON.stringify(response); + } + } + ); + + app.get( + URL_REVIEWED_EDIT_REQUESTS, {schema: getAllEditRequestsSchema}, async (request, reply) => { let { admin } = await verifyJwt(request); @@ -99,7 +120,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } else { let response = { status: "ok", - editRequests: await dataLayer.getAllEditRequests() + editRequests: await dataLayer.getEditRequestsByStatus("Reviewed") } return JSON.stringify(response); } @@ -116,7 +137,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v return; } else { const incomingRequest = request.body; - incomingRequest.status = "In Progress"; + incomingRequest.status = "Pending"; let response = { status: "ok", id: (await dataLayer.createEditRequest(incomingRequest)).id diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 25765f3..6ae5f1b 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -120,7 +120,7 @@ describe("Production Data Layer Integration Tests", () => { submitter: "", dateSubmitted: new Date(), dateUpdated: new Date(), - status: "In Progress", + status: "Pending", adds:[DummyBiz], updates: [DummyBiz], deletes: [], @@ -131,7 +131,7 @@ describe("Production Data Layer Integration Tests", () => { submitter: "", dateSubmitted: new Date(), dateUpdated: new Date(), - status: "In Progress", + status: "Pending", adds:[DummyBiz], updates: [DummyBiz], deletes: [], @@ -150,11 +150,6 @@ describe("Production Data Layer Integration Tests", () => { expect(editRequests.length).toBe(1); expect(editRequests).toStrictEqual(arrayContaining([testRequest])); - editRequests = await productionDataLayer.getAllEditRequests(); - expect(editRequests).toBeTruthy(); - expect(editRequests.length).toBe(2); - expect(editRequests).toStrictEqual(arrayContaining([testRequest, spoilerRequest])) - let singleRequest = await productionDataLayer.getEditRequestById(id); expect(singleRequest).toBeTruthy(); if(!!singleRequest) { @@ -162,9 +157,19 @@ describe("Production Data Layer Integration Tests", () => { expect(singleRequest.regionId).toBe(regionId); } + editRequests = await productionDataLayer.getAllEditRequests(); + expect(editRequests).toBeTruthy(); + expect(editRequests.length).toBe(2); + expect(editRequests).toStrictEqual(arrayContaining([testRequest, spoilerRequest])) + + editRequests = await productionDataLayer.getEditRequestsByStatus("Pending"); + expect(editRequests).toBeTruthy(); + expect(editRequests.length).toBe(2); + expect(editRequests).toStrictEqual(arrayContaining([testRequest, spoilerRequest])) + let updateRequest = { id: testRequest.id, - status: "Approved", + status: "Reviewed", regionId: testRequest.regionId, dateSubmitted: testRequest.dateSubmitted, dateUpdated: new Date(), @@ -188,6 +193,11 @@ describe("Production Data Layer Integration Tests", () => { }) ); + editRequests = await productionDataLayer.getEditRequestsByStatus("Reviewed"); + expect(editRequests).toBeTruthy(); + expect(editRequests.length).toBe(1); + expect(editRequests).toStrictEqual(arrayContaining([objectContaining({...testRequest, ...updateRequest, status: "Reviewed"})])) + done(); }); }); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index a0b5136..6bc92c8 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -1,6 +1,6 @@ import {DummyDatalayer} from "./testUtils/testDataLayer"; import { testify} from "./testUtils/testify"; -import {createEditEndpoint, EditRequest} from "../src/endpoints/editRequest"; +import {createEditEndpoint, EditRequest, URL_PENDING_EDIT_REQUESTS, URL_REVIEWED_EDIT_REQUESTS} from "../src/endpoints/editRequest"; import { dummyAdminToken, DummyBiz, @@ -11,6 +11,7 @@ import { import arrayContaining = jasmine.arrayContaining; import {FastifyInstance} from "fastify"; import objectContaining = jasmine.objectContaining; +import any = jasmine.any; const DummyAdd : EditRequest = { regionId: DummyRegion.name, @@ -95,11 +96,11 @@ describe("Edit Request unit tests", () => { }); }); - it("Can view all edit requests as a system admin but not as region manager", async(done) => { - function getAllEditRequests(token: string) { + it("Can view all pending edit requests as a system admin but not as region manager", async(done) => { + function getPendingEditRequests(token: string) { return editEndpoint.inject({ method: "GET", - url: `/edits/all`, + url: URL_PENDING_EDIT_REQUESTS, headers: {authorization: `Bearer ${token}`} }); } @@ -111,17 +112,13 @@ describe("Edit Request unit tests", () => { const postResponse2 = await submitEditRequest(differentRegionAdd, differentRegionAdd.regionId, dummyRegionManagerToken); expect(postResponse2.statusCode).toBe(201); - const regionManagerResponse = await editEndpoint.inject({ - method: "GET", - url: `/edits/all`, - headers: {authorization: `Bearer ${dummyRegionManagerToken}`} - }); + const regionManagerResponse = await getPendingEditRequests(dummyRegionManagerToken); expect(regionManagerResponse.statusCode).toBe(401); const byRegionResponse = await getEditRequestsByRegion(DummyRegion.name, dummyRegionManagerToken); expect(JSON.parse(byRegionResponse.payload).editRequests.length).toBe(1); - const getResponse = await getAllEditRequests(dummyAdminToken); + const getResponse = await getPendingEditRequests(dummyAdminToken); expect(getResponse.statusCode).toBe(200); let postedRequest1 = asResponse(asInitializedEditRequest(DummyAdd, JSON.parse(postResponse1.payload).id)); let postedRequest2 = asResponse(asInitializedEditRequest(differentRegionAdd, JSON.parse(postResponse2.payload).id)); @@ -134,29 +131,58 @@ describe("Edit Request unit tests", () => { done(); }); - it("Can only update edit request status as sysadmin", async(done) => { - async function updateEditRequestStatus(editRequest: EditRequest, newStatus: string, token: string) : Promise { - return testApp.inject({ - method: "POST", - url: `/edits/${editRequest.id}`, - payload: {...editRequest, status: newStatus}, - headers: { - authorization: `Bearer ${token}` - } + it("Can view all reviewed edit requests as sysadmin but not region manager", async (done) => { + function getReviewedEditRequests(token: string) { + return editEndpoint.inject({ + method: "GET", + url: URL_REVIEWED_EDIT_REQUESTS, + headers: {authorization: `Bearer ${token}`} }); } + const postResponse1 = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); + expect(postResponse1.statusCode).toBe(201); + let {id: id1} = JSON.parse(postResponse1.payload); + + const differentRegionAdd = {...DummyAdd, regionId: `Not${DummyRegion.name}`}; + const postResponse2 = await submitEditRequest(differentRegionAdd, differentRegionAdd.regionId, dummyRegionManagerToken); + expect(postResponse2.statusCode).toBe(201); + let {id: id2} = JSON.parse(postResponse2.payload); + + await updateEditRequestStatus(id1, "Reviewed", dummyAdminToken); + await updateEditRequestStatus(id2, "Reviewed", dummyAdminToken); + + const adminResponse = await getReviewedEditRequests(dummyAdminToken); + expect(adminResponse.statusCode).toBe(200); + expect(JSON.parse(adminResponse.payload).editRequests).toStrictEqual( + arrayContaining([ + objectContaining({...DummyAdd, id: id1, status: "Reviewed", dateSubmitted: any(String), dateUpdated: any(String)}), + objectContaining({...differentRegionAdd, id: id2, status: "Reviewed", dateSubmitted: any(String), dateUpdated: any(String)}), + ]) + ); + + const regionManagerResponse = await getReviewedEditRequests(dummyRegionManagerToken); + expect(regionManagerResponse.statusCode).toBe(401); + + const unauthenticatedResponse = await getReviewedEditRequests(""); + expect(unauthenticatedResponse.statusCode).toBe(401); + + await testApp.close(); + done(); + }) + + it("Can only update edit request status as sysadmin", async(done) => { const submitResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(submitResponse.statusCode).toBe(201); let createdRequest = {...DummyAdd, id: JSON.parse(submitResponse.payload).id}; - const unauthorizedResponse = await updateEditRequestStatus(createdRequest, "Approved", ""); + const unauthorizedResponse = await updateEditRequestStatus(createdRequest.id, "Approved", ""); expect(unauthorizedResponse.statusCode).toBe(401); - const regionManagerResponse = await updateEditRequestStatus(createdRequest, "Approved", dummyRegionManagerToken); + const regionManagerResponse = await updateEditRequestStatus(createdRequest.id, "Approved", dummyRegionManagerToken); expect(regionManagerResponse.statusCode).toBe(401); - const updateResponse = await updateEditRequestStatus(createdRequest, "Approved", dummyAdminToken); + const updateResponse = await updateEditRequestStatus(createdRequest.id, "Approved", dummyAdminToken); expect(updateResponse.statusCode).toBe(200); const updatedRequest = asResponse({ @@ -181,7 +207,7 @@ describe("Edit Request unit tests", () => { }); } - async function submitEditRequest(request: EditRequest, regionId: string, token: string) { + async function submitEditRequest(request: any, regionId: string, token: string) { let postOptions = { method: <"POST">"POST", url: `/region/${regionId}/edits`, @@ -194,6 +220,17 @@ describe("Edit Request unit tests", () => { return await editEndpoint.inject(postOptions); } + async function updateEditRequestStatus(id: string, newStatus: string, token: string) : Promise { + return testApp.inject({ + method: "POST", + url: `/edits/${id}`, + payload: {status: newStatus}, + headers: { + authorization: `Bearer ${token}` + } + }); + } + async function getEditRequestsByRegion(regionId: string, token: string) { return await editEndpoint.inject({ method: "GET", @@ -212,6 +249,6 @@ describe("Edit Request unit tests", () => { } function asInitializedEditRequest(rawEdit: EditRequest, id: string) : EditRequest { - return {...rawEdit, id, status: "In Progress"}; + return {...rawEdit, id, status: "Pending"}; } }); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index cc96b86..898c70f 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -56,7 +56,7 @@ export class DummyDatalayer implements DataLayer { } async createEditRequest(editRequest: EditRequest): Promise { - const newRequest = {...editRequest, id: new Date().toISOString()} + const newRequest = {...editRequest, id: new Date().toISOString() + Math.random()} this.editRequests.push(newRequest); return {id: newRequest.id} ; } @@ -79,7 +79,12 @@ export class DummyDatalayer implements DataLayer { return this.editRequests[index]; } + async getEditRequestsByStatus(status: string): Promise { + return this.editRequests.filter((req) => req.status === status); + } + clearRegions() { this.regions = []; } + } From 2e2e0a3cc9ee7dbcc933d5e8fd16e094f1d0aead Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 8 Mar 2021 20:26:01 -0330 Subject: [PATCH 51/75] Fixing swagger docs for new edit request queries --- src/endpoints/docs/editRequestSchemas.ts | 23 +++++++++++++++++++++-- src/endpoints/editRequest.ts | 8 ++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index b678b9f..29583e8 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -74,8 +74,8 @@ export const getEditRequestsByRegionSchema = { } }; -export const getAllEditRequestsSchema = { - description: "Returns all edit requests. Only usable by system admins", +export const getPendingEditRequestsSchema = { + description: "Returns all pending edit requests. Only usable by system admins", response: { 200: { description: 'Successful response', @@ -91,6 +91,25 @@ export const getAllEditRequestsSchema = { } }; +export const getReviewedEditRequestsSchema = { + description: "Returns all reviewed edit requests. Only usable by system admins", + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + editRequests: { + type: "array", + items: editRequestSchema + } + } + } + } +}; + + + export const createEditRequestSchema = { description: "Creates the supplied edit request", body: editRequestSchema, diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 6f62fb4..55b38f6 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -6,8 +6,8 @@ import {AuthenticatedRequest, AuthenticatedRequestById, AuthenticatedRequestByRe import {isRegionManager} from "../utils"; import { createEditRequestSchema, - getAllEditRequestsSchema, getEditRequestByIdSchema, - getEditRequestsByRegionSchema, updateEditRequestSchema + getEditRequestByIdSchema, + getEditRequestsByRegionSchema, getPendingEditRequestsSchema, getReviewedEditRequestsSchema, updateEditRequestSchema } from "./docs/editRequestSchemas"; export const URL_PENDING_EDIT_REQUESTS = "/edits/status/pending"; @@ -93,7 +93,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v app.get( URL_PENDING_EDIT_REQUESTS, - {schema: getAllEditRequestsSchema}, + {schema: getPendingEditRequestsSchema}, async (request, reply) => { let { admin } = await verifyJwt(request); if(!admin) { @@ -111,7 +111,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v app.get( URL_REVIEWED_EDIT_REQUESTS, - {schema: getAllEditRequestsSchema}, + {schema: getReviewedEditRequestsSchema}, async (request, reply) => { let { admin } = await verifyJwt(request); if(!admin) { From 18d020d3685320a9702a236f54aa109026f4ef51 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Mon, 8 Mar 2021 21:58:27 -0330 Subject: [PATCH 52/75] Added edit request previews --- src/database/productionDataLayer.ts | 6 ++ src/endpoints/businesses.ts | 9 +++ src/endpoints/docs/editRequestSchemas.ts | 25 +++++++++ src/endpoints/editRequest.ts | 56 ++++++++++++++++--- tests/dataLayer.test.ts | 13 +++-- tests/editRequests.test.ts | 70 +++++++++++++++++++++++- tests/testUtils/dummyData.ts | 11 +++- tests/testUtils/testDataLayer.ts | 13 +++-- 8 files changed, 185 insertions(+), 18 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 2c8b907..6ab8c69 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -27,6 +27,7 @@ export interface Filters { export interface DataLayer { setBusiness(business: Business) : Promise; + getBusinessById(id: string): Promise; getBusinessesByRegion(region: string): Promise; getFilters(regionId: string) : Promise; getRegionsManagedBy(managerId: string) : Promise; @@ -42,6 +43,11 @@ export interface DataLayer { } export class ProductionDataLayer implements DataLayer { + async getBusinessById(id: string): Promise { + let businessSnapshot = await firestore.collection("businesses").doc(id).get(); + return {...businessSnapshot.data(), id: businessSnapshot.id}; + } + async getBusinessesByRegion(regionId: string) : Promise { let businessSnapshot = await firestore.collection("businesses").where("regionId", "==", regionId).get(); return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index a4a165b..260fae2 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -31,6 +31,15 @@ export interface Business { location?: GeoPoint | null | undefined } +export interface BusinessUpdate { + id: string, + name?: string, + employees?: number, + industry?: string, + year_added?: number; + location?: GeoPoint | null | undefined +} + export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { app.get( diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index 29583e8..7b5e932 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -42,6 +42,31 @@ export const getEditRequestByIdSchema = { } }; +export const getEditPreviewSchema = { + params: byIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + added: { + type: 'array', + items: businessSchema + }, + updated: { + type: 'array', + items: businessSchema + }, + deleted: { + type: 'array', + items: businessSchema + } + } + } + } +}; + export const updateEditRequestSchema = { params: byIdSchema, response: { diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 55b38f6..8e0cba9 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -1,11 +1,11 @@ import {FastifyInstance } from "fastify"; import {DataLayer} from "../database/productionDataLayer"; -import {Business} from "./businesses"; +import {Business, BusinessUpdate} from "./businesses"; import {Auth0JwtVerifier} from "../auth0"; import {AuthenticatedRequest, AuthenticatedRequestById, AuthenticatedRequestByRegionId} from "./endpointUtils"; import {isRegionManager} from "../utils"; import { - createEditRequestSchema, + createEditRequestSchema, getEditPreviewSchema, getEditRequestByIdSchema, getEditRequestsByRegionSchema, getPendingEditRequestsSchema, getReviewedEditRequestsSchema, updateEditRequestSchema } from "./docs/editRequestSchemas"; @@ -16,15 +16,16 @@ export const URL_REVIEWED_EDIT_REQUESTS = "/edits/status/reviewed"; export interface EditRequest { id?: string, regionId: string, - submitter: string, - dateSubmitted: Date | string, - dateUpdated: Date | string + submitter?: string, + dateSubmitted?: Date | string, + dateUpdated?: Date | string status?: string, adds?: Business[], - updates?: Business[] | undefined, - deletes?: string[] | undefined + updates?: BusinessUpdate[], + deletes?: string[] } + interface CreateEditRequest extends AuthenticatedRequestByRegionId { Body: EditRequest } @@ -127,6 +128,47 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } ); + app.get( + `/edits/:id/preview`, + {schema: getEditPreviewSchema}, + async (request, reply) => { + let { admin } = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Only admins may access this data!"); + return; + } else { + let addedBusinesses: Business[] = []; + let updatedBusinesses: Business[] = []; + let deletedBusinesses: Business[] = []; + let editRequest = await dataLayer.getEditRequestById(request.params.id); + for(const add of editRequest?.adds || []) { + addedBusinesses.push({...add, id: "some-id"}); + } + for (const update of editRequest?.updates || []) { + let biz = await dataLayer.getBusinessById(update.id) + if(!!biz) { + updatedBusinesses.push({...biz, ...update}); + } + } + for (const delId of editRequest?.deletes || []) { + let biz = await dataLayer.getBusinessById(delId); + if(!!biz) { + deletedBusinesses.push(biz); + } + } + + let response = { + status: "ok", + added: addedBusinesses, + updated: updatedBusinesses, + deleted: deletedBusinesses + } + + return JSON.stringify(response); + } + } + ); + app.post( '/region/:regionId/edits', {schema: createEditRequestSchema}, diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 6ae5f1b..c8ccab4 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -4,7 +4,7 @@ import {Business} from "../src/endpoints/businesses"; import objectContaining = jasmine.objectContaining; import {EditRequest} from "../src/endpoints/editRequest"; import arrayContaining = jasmine.arrayContaining; -import {DummyBiz} from "./testUtils/dummyData"; +import {DummyBiz, DummyBizUpdate} from "./testUtils/dummyData"; describe("Production Data Layer Integration Tests", () => { const DUMMY_REGION_1 = "DummyRegion"; @@ -39,8 +39,11 @@ describe("Production Data Layer Integration Tests", () => { let id = (await productionDataLayer.setBusiness(biz)).id; expect(id).toBeTruthy(); - let bizData = await productionDataLayer.getBusinessesByRegion(biz.regionId); - expect(bizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); + let byIdData = await productionDataLayer.getBusinessById(id); + expect(byIdData).toEqual(expect.objectContaining({...biz})); + + let byRegionData = await productionDataLayer.getBusinessesByRegion(biz.regionId); + expect(byRegionData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); let filters = await productionDataLayer.getFilters(biz.regionId); expect(filters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})) @@ -122,7 +125,7 @@ describe("Production Data Layer Integration Tests", () => { dateUpdated: new Date(), status: "Pending", adds:[DummyBiz], - updates: [DummyBiz], + updates: [DummyBizUpdate], deletes: [], }; @@ -133,7 +136,7 @@ describe("Production Data Layer Integration Tests", () => { dateUpdated: new Date(), status: "Pending", adds:[DummyBiz], - updates: [DummyBiz], + updates: [DummyBizUpdate], deletes: [], }; diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 6bc92c8..41a228d 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -12,6 +12,9 @@ import arrayContaining = jasmine.arrayContaining; import {FastifyInstance} from "fastify"; import objectContaining = jasmine.objectContaining; import any = jasmine.any; +import {Business, createBusinessesEndpoint} from "../src/endpoints/businesses"; +import {Region} from "../src/database/productionDataLayer"; +import createRegionsEndpoint from "../src/endpoints/regions"; const DummyAdd : EditRequest = { regionId: DummyRegion.name, @@ -199,6 +202,63 @@ describe("Edit Request unit tests", () => { done(); }); + it("Can show a preview of the records that would be changed by an edit request", async(done) => { + async function createRegion(regionApp: FastifyInstance, region: Region) { + let temp = await regionApp.inject({ + method: "POST", + url: `/regions/`, + payload: region, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + return temp; + } + + async function createBusiness(bizApp: FastifyInstance, biz: Business) { + let temp = await bizApp.inject({ + method: "POST", + url: `/regions/${biz.regionId}/businesses`, + payload: biz, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + return temp; + } + + const regionApp = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); + const bizApp = createBusinessesEndpoint(testApp, testDataLayer, dummyTokenVerifier); + await createRegion(regionApp, DummyRegion); + let biz1 = {...DummyBiz, name: "Deleting"}; + let biz2 = {...DummyBiz, name: `OriginalName`, employees: DummyBiz.employees + 10, industry: `OriginalIndustry}`}; + let {businessId: bizId1} = JSON.parse((await createBusiness(bizApp, biz1)).payload); + let {businessId: bizId2} = JSON.parse((await createBusiness(bizApp, biz2)).payload); + const request : EditRequest = { + ...DummyAdd, + updates: [{ + id: bizId2, + name: "UpdatedName", + industry: "UpdatedIndustry" + }], + deletes: [bizId1] + }; + + let submitResponse = await submitEditRequest(request, DummyRegion.name, dummyRegionManagerToken); + expect(submitResponse.statusCode).toBe(201); + let {id} = JSON.parse(submitResponse.payload); + + let previewResponse = await getEditPreview(id, dummyAdminToken); + expect(previewResponse.statusCode).toBe(200); + expect(JSON.parse(previewResponse.payload)).toStrictEqual( + objectContaining({ + added: arrayContaining([objectContaining({...DummyBiz, id: any(String)})]), + updated: arrayContaining([objectContaining({...biz2, name: "UpdatedName", industry: "UpdatedIndustry"})]), + deleted: arrayContaining([objectContaining({...biz1, id: any(String)})]) + }) + ); + + await testApp.close(); + done(); + }); + + async function getRequestById(requestId: string, token: string) : Promise { return await editEndpoint.inject({ method: "GET", @@ -207,7 +267,7 @@ describe("Edit Request unit tests", () => { }); } - async function submitEditRequest(request: any, regionId: string, token: string) { + async function submitEditRequest(request: EditRequest, regionId: string, token: string) { let postOptions = { method: <"POST">"POST", url: `/region/${regionId}/edits`, @@ -239,6 +299,14 @@ describe("Edit Request unit tests", () => { }); } + async function getEditPreview(id: string, token: string) { + return await editEndpoint.inject({ + method: "GET", + url: `/edits/${id}/preview`, + headers: {authorization: `Bearer ${token}`} + }); + } + /** * Simulates the standard data conversion when data is submitted in a request and then received in a response * @param rawEdit - the edit data in its original pre-submission diff --git a/tests/testUtils/dummyData.ts b/tests/testUtils/dummyData.ts index d43726d..22e4830 100644 --- a/tests/testUtils/dummyData.ts +++ b/tests/testUtils/dummyData.ts @@ -1,5 +1,5 @@ import {FastifyInstance, FastifyRequest} from "fastify"; -import {Business} from "../../src/endpoints/businesses"; +import {Business, BusinessUpdate} from "../../src/endpoints/businesses"; import {Region} from "../../src/database/productionDataLayer"; import {getMockToken} from "./testify"; @@ -20,6 +20,15 @@ export const DummyBiz: Business = { industry: "DummyIndustry" }; +export const DummyBizUpdate: BusinessUpdate = { + id: "DummyID", + name: "DummyBiz", + year_added: 2009, + employees: 1, + industry: "DummyIndustry" +}; + + export async function dummyTokenVerifier (req: FastifyRequest) { if(!req.headers.authorization || !req.headers.authorization.split("Bearer")[1].trim()) { return {userAppId: "", admin: false}; diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 898c70f..87e449d 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -13,7 +13,8 @@ export class DummyDatalayer implements DataLayer { } async setBusiness(business:Business): Promise { - this.businesses.push(business); + let id = `${Math.random()}`; + this.businesses.push({...business, id}); let regionIndex = this.regions.findIndex((r) => r.name == business.regionId); let bizRegion = this.regions[regionIndex] if(!bizRegion.filters) { @@ -28,7 +29,7 @@ export class DummyDatalayer implements DataLayer { } else { bizRegion.filters.industries[industryIndex].count++; } - return {id:"1"}; + return {id}; } async getFilters(regionId: string): Promise { @@ -70,7 +71,7 @@ export class DummyDatalayer implements DataLayer { } async getEditRequestById(id: string): Promise { - return this.editRequests.filter((req) => req.id === id)[0]; + return this.editRequests.find((req) => req.id === id) || null; } async updateEditRequest(body: EditRequest): Promise { @@ -83,8 +84,12 @@ export class DummyDatalayer implements DataLayer { return this.editRequests.filter((req) => req.status === status); } + async getBusinessById(id: string): Promise { + return this.businesses.find((b) => b.id === id) || null; + } + + clearRegions() { this.regions = []; } - } From ffee21e477c3227b5ec56a8d37a569d54262708e Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 12 Mar 2021 01:44:52 -0330 Subject: [PATCH 53/75] Added pagination and query-by-status on /edits/all Also finally got around to separating the test and prod firestore instances --- src/database/firestore.ts | 2 +- src/database/productionDataLayer.ts | 107 ++++++++++++++-------- src/endpoints/docs/editRequestSchemas.ts | 64 ++++++------- src/endpoints/editRequest.ts | 102 ++++++++++++--------- src/index.ts | 4 +- tests/dataLayer.test.ts | 69 ++++++++++++-- tests/editRequests.test.ts | 111 +++++++++++++---------- tests/testUtils/testDataLayer.ts | 30 +++--- tests/testUtils/testFirestore.ts | 13 +++ 9 files changed, 307 insertions(+), 195 deletions(-) create mode 100644 tests/testUtils/testFirestore.ts diff --git a/src/database/firestore.ts b/src/database/firestore.ts index 5f67331..ee4a346 100644 --- a/src/database/firestore.ts +++ b/src/database/firestore.ts @@ -11,4 +11,4 @@ let firebaseConfig = { }; export const app = firebase.initializeApp(firebaseConfig); -export const firestore = app.firestore(); +export const productionFirestore = app.firestore(); diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 6ab8c69..337e28c 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,6 +1,5 @@ import {Business} from "../endpoints/businesses"; -import {firestore} from "./firestore"; -import { EditRequest } from "../endpoints/editRequest"; +import {EditRequest, PAGE_SIZE} from "../endpoints/editRequest"; import firebase from "firebase"; import Timestamp = firebase.firestore.Timestamp; @@ -36,28 +35,33 @@ export interface DataLayer { getAllRegions(): Promise; createEditRequest(add: EditRequest): Promise; getEditRequestsForRegion(regionId: string): Promise; - getAllEditRequests(): Promise; + getAllEditRequests(afterId?: string): Promise; getEditRequestById(id: string): Promise; updateEditRequest(body: EditRequest): Promise; - getEditRequestsByStatus(pending: string): Promise; + getEditRequestsByStatus(status: string, afterId?: string): Promise; + getEditRequestsByUser(userAppId: string): Promise; } export class ProductionDataLayer implements DataLayer { + firestore: firebase.firestore.Firestore; + constructor(firestore: firebase.firestore.Firestore) { + this.firestore = firestore; + } async getBusinessById(id: string): Promise { - let businessSnapshot = await firestore.collection("businesses").doc(id).get(); + let businessSnapshot = await this.firestore.collection("businesses").doc(id).get(); return {...businessSnapshot.data(), id: businessSnapshot.id}; } async getBusinessesByRegion(regionId: string) : Promise { - let businessSnapshot = await firestore.collection("businesses").where("regionId", "==", regionId).get(); + let businessSnapshot = await this.firestore.collection("businesses").where("regionId", "==", regionId).get(); return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); } async setBusiness(newBusinessData: Business) : Promise { - const bc = firestore.collection("businesses"); + const bc = this.firestore.collection("businesses"); let businessRef = newBusinessData.id ? bc.doc(newBusinessData.id) : bc.doc(); - let regionRef = firestore.collection("regions").doc(newBusinessData.regionId); - return firestore.runTransaction(async transaction => { + let regionRef = this.firestore.collection("regions").doc(newBusinessData.regionId); + return this.firestore.runTransaction(async transaction => { let regionDoc = await transaction.get(regionRef); if(!regionDoc.exists) { throw "Bad Region"; @@ -89,35 +93,37 @@ export class ProductionDataLayer implements DataLayer { } async getFilters(region: string) : Promise{ - let regionData = (await firestore.collection("regions").doc(region).get()).data(); + let regionData = (await this.firestore.collection("regions").doc(region).get()).data(); regionData = !!regionData ? regionData : {}; return regionData.filters; } async getAllRegions() : Promise { - let regionsSnapshot = await firestore.collection("regions").get(); + let regionsSnapshot = await this.firestore.collection("regions").get(); return regionsSnapshot.docs.map((r) => ({id: r.id, name: r.data().name, manager: r.data().manager})); } async getRegionsManagedBy(managerId: string): Promise { - let regionsSnapshot = await firestore.collection("regions").where("manager", "==", managerId).get(); + let regionsSnapshot = await this.firestore.collection("regions").where("manager", "==", managerId).get(); return regionsSnapshot.docs.map((r) => ({id: r.id, name: r.data().name, manager: r.data().manager})); } async setRegion(region: Region): Promise { - let regionDoc = !!region.id ? firestore.collection("regions").doc(region.id) : firestore.collection("regions").doc(); + let regionDoc = !!region.id + ? this.firestore.collection("regions").doc(region.id) + : this.firestore.collection("regions").doc(); await regionDoc.set({name: region.name, "manager": region.manager}, {merge: true}); return {id: regionDoc.id}; } async deleteRegion(id: string): Promise { - await firestore.collection("regions").doc(id).delete(); + await this.firestore.collection("regions").doc(id).delete(); } async deleteBusiness(id: string) { - await firestore.runTransaction( + await this.firestore.runTransaction( async transaction => { - let businessRef = firestore.collection("businesses").doc(id); + let businessRef = this.firestore.collection("businesses").doc(id); let businessDoc = await transaction.get(businessRef); let businessData = businessDoc.data(); if (!!businessData) { @@ -136,14 +142,15 @@ export class ProductionDataLayer implements DataLayer { } async createEditRequest(editRequest: EditRequest): Promise { - let doc = firestore.collection("editRequests").doc(); - await doc.set(editRequest); + let doc = this.firestore.collection("editRequests").doc(); + await doc.set({...editRequest, createdDate: firebase.firestore.FieldValue.serverTimestamp()}); return {id : doc.id}; } async getEditRequestsForRegion(regionId: string) : Promise { let regionRequests : EditRequest[] = []; - (await firestore.collection("editRequests").where("regionId", "==", regionId).get()).docs.forEach( + let regionDocs = (await this.firestore.collection("editRequests").where("regionId", "==", regionId).get()).docs; + regionDocs.forEach( (req) => { let converted = this.convertToEditRequest(req.id, req.data()); if (!!converted) { @@ -154,38 +161,58 @@ export class ProductionDataLayer implements DataLayer { return regionRequests; } - async getAllEditRequests(): Promise { - let allRequests: EditRequest[] = []; - (await firestore.collection("editRequests").get()).docs.forEach( - (req) => { - let converted = this.convertToEditRequest(req.id, req.data()); - if (!!converted) { - allRequests.push(converted); - } - } - ); - return allRequests; - } - async getEditRequestById(id: string) : Promise { - let requestData = (await firestore.collection("editRequests").doc(id).get()).data(); + let requestData = (await this.firestore.collection("editRequests").doc(id).get()).data(); return this.convertToEditRequest(id, requestData); } async updateEditRequest(body: EditRequest): Promise { let id = !!body.id ? body.id : ""; - let requestData = await this.getEditRequestById(id); + let requestData = (await this.firestore.collection("editRequests").doc(id).get()).data(); let updatedRequestData = {...requestData, ...body}; - await firestore.collection("editRequests").doc(id).set(updatedRequestData) + await this.firestore.collection("editRequests").doc(id).update(updatedRequestData); return Promise.resolve(updatedRequestData); } - async getEditRequestsByStatus(status: string): Promise { + async getAllEditRequests(afterId?: string): Promise { + let query = this.firestore.collection("editRequests") + .orderBy("createdDate", 'asc') + .limit(PAGE_SIZE); + return await this.getPaginatedEditRequests(query, afterId); + } + + async getEditRequestsByStatus(status: string, afterId?: string): Promise { + let query = this.firestore.collection("editRequests") + .where("status", "==", status) + .orderBy("createdDate", 'asc') + .limit(PAGE_SIZE); + return await this.getPaginatedEditRequests(query, afterId); + } + + private async getPaginatedEditRequests(query: firebase.firestore.Query, afterId: string | undefined) { + let requests: EditRequest[] = []; + if (!!afterId) { + let afterRecord = await this.firestore.collection("editRequests").doc(afterId).get(); + query = query.startAfter(afterRecord); + } + let editRequestDocs = (await query.get()).docs; + editRequestDocs.forEach( + (doc) => { + let converted = this.convertToEditRequest(doc.id, doc.data()); + if (!!converted) { + requests.push(converted); + } + } + ); + return requests; + } + + async getEditRequestsByUser(userAppId: string): Promise { let requests = []; - (await firestore.collection("editRequests").where("status", "==", status).get()).docs.forEach( + (await this.firestore.collection("editRequests").where("submitter", "==", userAppId).get()).docs.forEach( (req) => { let converted = this.convertToEditRequest(req.id, req.data()); - if (!!converted) { + if(!!converted) { requests.push(converted); } } @@ -195,6 +222,7 @@ export class ProductionDataLayer implements DataLayer { convertToEditRequest(id: string, documentData: firebase.firestore.DocumentData | undefined) : EditRequest | null { if(!!documentData) { + delete documentData.createdDate; let request = documentData; request.dateSubmitted = ((documentData.dateSubmitted)).toDate(); request.dateUpdated = ((documentData.dateUpdated)).toDate(); @@ -207,7 +235,7 @@ export class ProductionDataLayer implements DataLayer { private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: firebase.firestore.Transaction) { - let regionRef = firestore.collection("regions").doc(regionId); + let regionRef = this.firestore.collection("regions").doc(regionId); let regionDoc = await transaction.get(regionRef); if (!!regionDoc) { let regionData = regionDoc.data(); @@ -267,4 +295,3 @@ export class ProductionDataLayer implements DataLayer { } } -export const productionDataLayer = new ProductionDataLayer(); diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index 7b5e932..22daa50 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -1,29 +1,29 @@ -import {byIdSchema, byRegionIdSchema} from "./basicSchemas"; -import { businessSchema} from "./businessesSchemas"; +import {byIdSchema, byRegionIdSchema} from './basicSchemas'; +import { businessSchema} from './businessesSchemas'; const editRequestSchema = { - type: "object", + type: 'object', properties: { id: { - type: "string", - description: "Omit when creating new edit requests" + type: 'string', + description: 'Omit when creating new edit requests' }, - regionId: {type: "string"}, - submitter: {type: "string"}, - dateSubmitted: {type: "string"}, - dateUpdated: {type: "string"}, - status: {type: "string"}, + regionId: {type: 'string'}, + submitter: {type: 'string'}, + dateSubmitted: {type: 'string'}, + dateUpdated: {type: 'string'}, + status: {type: 'string'}, adds: { - type: "array", + type: 'array', items: businessSchema }, updates: { - type: "array", + type: 'array', items: businessSchema }, deletes: { - type: "array", - items: {type: "string"} + type: 'array', + items: {type: 'string'} } } } @@ -91,7 +91,7 @@ export const getEditRequestsByRegionSchema = { properties: { status: {type: 'string'}, editRequests: { - type: "array", + type: 'array', items: editRequestSchema } } @@ -99,25 +99,15 @@ export const getEditRequestsByRegionSchema = { } }; -export const getPendingEditRequestsSchema = { - description: "Returns all pending edit requests. Only usable by system admins", - response: { - 200: { - description: 'Successful response', - type: 'object', - properties: { - status: {type: 'string'}, - editRequests: { - type: "array", - items: editRequestSchema - } - } +export const getAllEditRequestsByStatusSchema = { + description: 'Returns all edit requests. Query string may specify the status parameter to filter for a specific status. Only usable by system admins', + querystring: { + type: 'object', + properties: { + status: {type: 'string'}, + afterId: {type: 'string'} } - } -}; - -export const getReviewedEditRequestsSchema = { - description: "Returns all reviewed edit requests. Only usable by system admins", + }, response: { 200: { description: 'Successful response', @@ -125,7 +115,7 @@ export const getReviewedEditRequestsSchema = { properties: { status: {type: 'string'}, editRequests: { - type: "array", + type: 'array', items: editRequestSchema } } @@ -133,10 +123,8 @@ export const getReviewedEditRequestsSchema = { } }; - - export const createEditRequestSchema = { - description: "Creates the supplied edit request", + description: 'Creates the supplied edit request', body: editRequestSchema, response: { 201: { @@ -144,7 +132,7 @@ export const createEditRequestSchema = { type: 'object', properties: { status: {type: 'string'}, - id: {type: "string"} + id: {type: 'string'} } } } diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 8e0cba9..958b792 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -5,13 +5,11 @@ import {Auth0JwtVerifier} from "../auth0"; import {AuthenticatedRequest, AuthenticatedRequestById, AuthenticatedRequestByRegionId} from "./endpointUtils"; import {isRegionManager} from "../utils"; import { - createEditRequestSchema, getEditPreviewSchema, - getEditRequestByIdSchema, - getEditRequestsByRegionSchema, getPendingEditRequestsSchema, getReviewedEditRequestsSchema, updateEditRequestSchema + createEditRequestSchema, getAllEditRequestsByStatusSchema, getEditPreviewSchema, + getEditRequestByIdSchema, getEditRequestsByRegionSchema, updateEditRequestSchema } from "./docs/editRequestSchemas"; -export const URL_PENDING_EDIT_REQUESTS = "/edits/status/pending"; -export const URL_REVIEWED_EDIT_REQUESTS = "/edits/status/reviewed"; +export const PAGE_SIZE = 25; export interface EditRequest { id?: string, @@ -25,7 +23,6 @@ export interface EditRequest { deletes?: string[] } - interface CreateEditRequest extends AuthenticatedRequestByRegionId { Body: EditRequest } @@ -34,45 +31,58 @@ interface UpdateEditRequest extends AuthenticatedRequestById { Body: EditRequest } +interface GetAllEditsByStatusRequest extends AuthenticatedRequest { + Querystring: { + status: string, + afterId: string + } +} + export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { - app.get( - `/edits/:id`, + + app.get( + `/edits`, {schema: getEditRequestByIdSchema}, async (request, reply) => { - let {userAppId, admin} = await verifyJwt(request); - if(!admin && !userAppId) { - reply.unauthorized("Must be logged in to submit requests"); + let {userAppId} = await verifyJwt(request); + if(!userAppId) { + reply.unauthorized("Must be logged in!"); return; } else { let response = { status: "ok", - editRequest: null + editRequests: await dataLayer.getEditRequestsByUser(userAppId) } - response.editRequest = await dataLayer.getEditRequestById(request.params.id) return JSON.stringify(response); } } ); - app.post( - `/edits/:id`, - {schema: updateEditRequestSchema}, - async(request, reply) => { - let {userAppId, admin} = await verifyJwt(request); - if(!admin && !!request.body.status) { - reply.unauthorized("Only system administrators may update edit request statuses!"); - return; - } else if (!admin && !(await isRegionManager(userAppId, request.body.regionId, dataLayer))) { - reply.unauthorized("Only administrators and managers may update edit requests!"); + + app.get( + "/edits/all", + {schema: getAllEditRequestsByStatusSchema}, + async (request, reply) => { + let { admin } = await verifyJwt(request); + if(!admin) { + reply.unauthorized("Only admins may access this data!"); return; } else { - return { + let editRequests : EditRequest[]; + if(!request.query.status) { + editRequests = await dataLayer.getAllEditRequests(request.query.afterId); + } else { + editRequests = await dataLayer.getEditRequestsByStatus(request.query.status, request.query.afterId) + } + let response = { status: "ok", - editRequest: await dataLayer.updateEditRequest({...request.body, id: request.params.id}) - }; + editRequests: editRequests + } + return JSON.stringify(response); } } ); + app.get( `/region/:regionId/edits`, {schema: getEditRequestsByRegionSchema}, @@ -92,38 +102,40 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } ); - app.get( - URL_PENDING_EDIT_REQUESTS, - {schema: getPendingEditRequestsSchema}, + app.get( + `/edits/:id`, + {schema: getEditRequestByIdSchema}, async (request, reply) => { - let { admin } = await verifyJwt(request); - if(!admin) { - reply.unauthorized("Only admins may access this data!"); + let {userAppId, admin} = await verifyJwt(request); + if(!admin && !userAppId) { + reply.unauthorized("Must be logged in to submit requests"); return; } else { let response = { status: "ok", - editRequests: await dataLayer.getEditRequestsByStatus("Pending") + editRequest: null } + response.editRequest = await dataLayer.getEditRequestById(request.params.id) return JSON.stringify(response); } } ); - - app.get( - URL_REVIEWED_EDIT_REQUESTS, - {schema: getReviewedEditRequestsSchema}, - async (request, reply) => { - let { admin } = await verifyJwt(request); - if(!admin) { - reply.unauthorized("Only admins may access this data!"); + app.post( + `/edits/:id`, + {schema: updateEditRequestSchema}, + async(request, reply) => { + let {userAppId, admin} = await verifyJwt(request); + if(!admin && !!request.body.status) { + reply.unauthorized("Only system administrators may update edit request statuses!"); + return; + } else if (!admin && !(await isRegionManager(userAppId, request.body.regionId, dataLayer))) { + reply.unauthorized("Only administrators and managers may update edit requests!"); return; } else { - let response = { + return { status: "ok", - editRequests: await dataLayer.getEditRequestsByStatus("Reviewed") - } - return JSON.stringify(response); + editRequest: await dataLayer.updateEditRequest({...request.body, id: request.params.id}) + }; } } ); diff --git a/src/index.ts b/src/index.ts index 9162499..42bec97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +4,15 @@ import createPingEndpoint from './endpoints/ping'; import { addRoutes } from './utils'; import { createBusinessesEndpoint} from "./endpoints/businesses"; import createRegionsEndpoint from "./endpoints/regions"; -import {productionDataLayer} from "./database/productionDataLayer"; +import {ProductionDataLayer} from "./database/productionDataLayer"; import {createFiltersEndpoint} from "./endpoints/filters"; import {registerCorsHandler} from "./cors"; import {registerSwagger} from "./swagger"; import {verifyJwt} from "./auth0"; import {createEditEndpoint} from "./endpoints/editRequest"; +import {productionFirestore} from "./database/firestore"; +let productionDataLayer = new ProductionDataLayer(productionFirestore) const port = Number(process.env.PORT || 8080); const server = fastify({logger: true}); server.register(fastifySensible); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index c8ccab4..fd1d77e 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -1,25 +1,28 @@ -import {productionDataLayer, Region} from "../src/database/productionDataLayer"; -import {firestore} from "../src/database/firestore"; +import {ProductionDataLayer, Region} from "../src/database/productionDataLayer"; +import {testFirestore} from "./testUtils/testFirestore"; import {Business} from "../src/endpoints/businesses"; import objectContaining = jasmine.objectContaining; -import {EditRequest} from "../src/endpoints/editRequest"; +import {EditRequest, PAGE_SIZE} from "../src/endpoints/editRequest"; import arrayContaining = jasmine.arrayContaining; import {DummyBiz, DummyBizUpdate} from "./testUtils/dummyData"; +let productionDataLayer = new ProductionDataLayer(testFirestore); + describe("Production Data Layer Integration Tests", () => { const DUMMY_REGION_1 = "DummyRegion"; const DUMMY_REGION_2 = "DummyRegion2"; let regionId : string; async function deleteRegionsNamed(regionName: string) { - (await firestore.collection("regions").where("name", "==", regionName).get()).docs + (await testFirestore.collection("regions").where("name", "==", regionName).get()).docs .forEach((doc) => doc.ref.delete()); } beforeEach(async(done) => { - (await firestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs.forEach((d) => d.ref.delete()); - await firestore.collection("years").doc("2019").delete(); - (await firestore.collection("editRequests").get()).docs.forEach(req => req.ref.delete()); + (await testFirestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs + .forEach((d) => d.ref.delete()); + await testFirestore.collection("years").doc("2019").delete(); + (await testFirestore.collection("editRequests").get()).docs.forEach(req => req.ref.delete()); await deleteRegionsNamed(DUMMY_REGION_1); await deleteRegionsNamed(DUMMY_REGION_2); @@ -183,7 +186,8 @@ describe("Production Data Layer Integration Tests", () => { expect(requestAfterUpdate).toStrictEqual( objectContaining({ ...testRequest, - ...updateRequest + status: "Reviewed", + dateUpdated: updateRequest.dateUpdated }) ); @@ -192,7 +196,8 @@ describe("Production Data Layer Integration Tests", () => { expect(readRequestAfterUpdate).toStrictEqual( objectContaining({ ...testRequest, - ...updateRequest + status: "Reviewed", + dateUpdated: updateRequest.dateUpdated }) ); @@ -203,4 +208,50 @@ describe("Production Data Layer Integration Tests", () => { done(); }); + + it("Paginates edit requests", async(done) => { + let region: Region = { + name: DUMMY_REGION_1, + manager: "Dummy Manager" + }; + regionId = (await productionDataLayer.setRegion(region)).id; + + let testRequest: EditRequest = { + regionId: regionId, + submitter: "first", + dateSubmitted: new Date(), + dateUpdated: new Date(), + status: "Pending", + adds: [DummyBiz], + updates: [DummyBizUpdate], + deletes: [], + }; + + let firstPageEdits = []; + for(let i = 0; i < PAGE_SIZE; i++) { + let {id} = await productionDataLayer.createEditRequest(testRequest); + expect(id).toBeTruthy(); + testRequest.id = id; + firstPageEdits.push({...testRequest}); + } + let lastIdOnFirstPage = firstPageEdits[firstPageEdits.length - 1].id; + + let {id: id2} = await productionDataLayer.createEditRequest({...testRequest, submitter: "second"}); + expect(id2).toBeTruthy(); + let secondPageEdit = {...testRequest, id: id2, submitter: "second"}; + + let firstPageRecords = await productionDataLayer.getAllEditRequests(); + expect(firstPageRecords).toStrictEqual(firstPageEdits); + + let firstPagePending = await productionDataLayer.getEditRequestsByStatus("Pending"); + expect(firstPagePending).toStrictEqual(firstPageEdits); + + let secondPageRecords = await productionDataLayer.getAllEditRequests(lastIdOnFirstPage); + expect(secondPageRecords).toStrictEqual([secondPageEdit]); + + let secondPagePending = await productionDataLayer.getEditRequestsByStatus("Pending", lastIdOnFirstPage); + expect(secondPagePending).toStrictEqual([secondPageEdit]); + + done(); + }); }); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 41a228d..42ee47f 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -1,6 +1,6 @@ import {DummyDatalayer} from "./testUtils/testDataLayer"; import { testify} from "./testUtils/testify"; -import {createEditEndpoint, EditRequest, URL_PENDING_EDIT_REQUESTS, URL_REVIEWED_EDIT_REQUESTS} from "../src/endpoints/editRequest"; +import {createEditEndpoint, EditRequest, PAGE_SIZE} from "../src/endpoints/editRequest"; import { dummyAdminToken, DummyBiz, @@ -99,81 +99,79 @@ describe("Edit Request unit tests", () => { }); }); - it("Can view all pending edit requests as a system admin but not as region manager", async(done) => { - function getPendingEditRequests(token: string) { - return editEndpoint.inject({ - method: "GET", - url: URL_PENDING_EDIT_REQUESTS, - headers: {authorization: `Bearer ${token}`} - }); - } + it("Can view all edit requests as a system admin but not as region manager", async(done) => { const postResponse1 = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(postResponse1.statusCode).toBe(201); + let {id: id1} = JSON.parse(postResponse1.payload); const differentRegionAdd = {...DummyAdd, regionId: `Not${DummyRegion.name}`}; const postResponse2 = await submitEditRequest(differentRegionAdd, differentRegionAdd.regionId, dummyRegionManagerToken); expect(postResponse2.statusCode).toBe(201); - const regionManagerResponse = await getPendingEditRequests(dummyRegionManagerToken); + const regionManagerResponse = await getAllEditRequests(dummyRegionManagerToken); expect(regionManagerResponse.statusCode).toBe(401); - const byRegionResponse = await getEditRequestsByRegion(DummyRegion.name, dummyRegionManagerToken); - expect(JSON.parse(byRegionResponse.payload).editRequests.length).toBe(1); + const allResponse = await getAllEditRequests(dummyAdminToken); + expect(allResponse.statusCode).toBe(200); - const getResponse = await getPendingEditRequests(dummyAdminToken); - expect(getResponse.statusCode).toBe(200); let postedRequest1 = asResponse(asInitializedEditRequest(DummyAdd, JSON.parse(postResponse1.payload).id)); let postedRequest2 = asResponse(asInitializedEditRequest(differentRegionAdd, JSON.parse(postResponse2.payload).id)); - expect(JSON.parse(getResponse.payload).editRequests).toStrictEqual(arrayContaining([ + expect(JSON.parse(allResponse.payload).editRequests).toStrictEqual(arrayContaining([ postedRequest1, postedRequest2 ])); - await testApp.close(); - done(); - }); - - it("Can view all reviewed edit requests as sysadmin but not region manager", async (done) => { - function getReviewedEditRequests(token: string) { - return editEndpoint.inject({ - method: "GET", - url: URL_REVIEWED_EDIT_REQUESTS, - headers: {authorization: `Bearer ${token}`} - }); - } - - const postResponse1 = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); - expect(postResponse1.statusCode).toBe(201); - let {id: id1} = JSON.parse(postResponse1.payload); - - const differentRegionAdd = {...DummyAdd, regionId: `Not${DummyRegion.name}`}; - const postResponse2 = await submitEditRequest(differentRegionAdd, differentRegionAdd.regionId, dummyRegionManagerToken); - expect(postResponse2.statusCode).toBe(201); - let {id: id2} = JSON.parse(postResponse2.payload); + const pendingResponse = await getAllEditRequests(dummyAdminToken, {status: "Pending"}); + expect(pendingResponse.statusCode).toBe(200); + expect(JSON.parse(pendingResponse.payload).editRequests).toStrictEqual(arrayContaining([ + postedRequest1, + postedRequest2 + ])); await updateEditRequestStatus(id1, "Reviewed", dummyAdminToken); - await updateEditRequestStatus(id2, "Reviewed", dummyAdminToken); - const adminResponse = await getReviewedEditRequests(dummyAdminToken); + const adminResponse = await getAllEditRequests(dummyAdminToken, {status: "Reviewed"} ); expect(adminResponse.statusCode).toBe(200); expect(JSON.parse(adminResponse.payload).editRequests).toStrictEqual( arrayContaining([ objectContaining({...DummyAdd, id: id1, status: "Reviewed", dateSubmitted: any(String), dateUpdated: any(String)}), - objectContaining({...differentRegionAdd, id: id2, status: "Reviewed", dateSubmitted: any(String), dateUpdated: any(String)}), ]) ); - const regionManagerResponse = await getReviewedEditRequests(dummyRegionManagerToken); - expect(regionManagerResponse.statusCode).toBe(401); + await testApp.close(); + done(); + }); + + it("Can retrieve all edit requests with pagination", async(done) => { + + let firstPageObjects = []; + for(let i = 0; i < PAGE_SIZE; i++) { + const page1Response = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); + expect(page1Response.statusCode).toBe(201); + let {id} = JSON.parse(page1Response.payload); + firstPageObjects.push(asResponse(asInitializedEditRequest(DummyAdd, id))); + } + const page2Response = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); + expect(page2Response.statusCode).toBe(201); + let {id: id2} = JSON.parse(page2Response.payload); + let secondPageRequest = asResponse(asInitializedEditRequest(DummyAdd, id2)); - const unauthenticatedResponse = await getReviewedEditRequests(""); - expect(unauthenticatedResponse.statusCode).toBe(401); + const firstPageResponse = await getAllEditRequests(dummyAdminToken); + expect(firstPageResponse.statusCode).toBe(200); + expect(JSON.parse(firstPageResponse.payload).editRequests).toStrictEqual(firstPageObjects); + + let afterId = firstPageObjects[firstPageObjects.length-1].id; + afterId = !!afterId? afterId : ""; + const secondPageResponse = await getAllEditRequests(dummyAdminToken, {afterId}); + expect(secondPageResponse.statusCode).toBe(200); + expect(JSON.parse(secondPageResponse.payload).editRequests).toStrictEqual([secondPageRequest]); await testApp.close(); done(); }) + it("Can only update edit request status as sysadmin", async(done) => { const submitResponse = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(submitResponse.statusCode).toBe(201); @@ -226,18 +224,18 @@ describe("Edit Request unit tests", () => { const regionApp = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const bizApp = createBusinessesEndpoint(testApp, testDataLayer, dummyTokenVerifier); await createRegion(regionApp, DummyRegion); - let biz1 = {...DummyBiz, name: "Deleting"}; - let biz2 = {...DummyBiz, name: `OriginalName`, employees: DummyBiz.employees + 10, industry: `OriginalIndustry}`}; + let biz1 = {...DummyBiz, name: `UpdatingName`, employees: DummyBiz.employees + 10, industry: `OriginalIndustry}`}; + let biz2 = {...DummyBiz, name: "Deleting"}; let {businessId: bizId1} = JSON.parse((await createBusiness(bizApp, biz1)).payload); let {businessId: bizId2} = JSON.parse((await createBusiness(bizApp, biz2)).payload); const request : EditRequest = { ...DummyAdd, updates: [{ - id: bizId2, + id: bizId1, name: "UpdatedName", industry: "UpdatedIndustry" }], - deletes: [bizId1] + deletes: [bizId2] }; let submitResponse = await submitEditRequest(request, DummyRegion.name, dummyRegionManagerToken); @@ -249,8 +247,8 @@ describe("Edit Request unit tests", () => { expect(JSON.parse(previewResponse.payload)).toStrictEqual( objectContaining({ added: arrayContaining([objectContaining({...DummyBiz, id: any(String)})]), - updated: arrayContaining([objectContaining({...biz2, name: "UpdatedName", industry: "UpdatedIndustry"})]), - deleted: arrayContaining([objectContaining({...biz1, id: any(String)})]) + updated: arrayContaining([objectContaining({...biz1, id: bizId1, name: "UpdatedName", industry: "UpdatedIndustry"})]), + deleted: arrayContaining([objectContaining({...biz2, id: bizId2})]) }) ); @@ -307,6 +305,21 @@ describe("Edit Request unit tests", () => { }); } + function getAllEditRequests(token: string, params?: {status?: string, afterId?: string}) { + let statusParam, pageParam; + if (!!params) { + statusParam = !!params.status ? `status=${params.status}` : ""; + pageParam = !!params.afterId ? `afterId=${params.afterId}` : ""; + } + let querystring = !!statusParam || !!pageParam ? `?${statusParam}&${pageParam}` : ""; + let url = `/edits/all${querystring}`; + return editEndpoint.inject({ + method: "GET", + url, + headers: {authorization: `Bearer ${token}`} + }); + } + /** * Simulates the standard data conversion when data is submitted in a request and then received in a response * @param rawEdit - the edit data in its original pre-submission diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 87e449d..f85ecf3 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -1,6 +1,6 @@ import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; import {Business} from "../../src/endpoints/businesses"; -import { EditRequest } from "../../src/endpoints/editRequest"; +import {EditRequest, PAGE_SIZE} from "../../src/endpoints/editRequest"; export class DummyDatalayer implements DataLayer { @@ -66,30 +66,36 @@ export class DummyDatalayer implements DataLayer { return this.editRequests.filter((req) => req.regionId === regionId); } - async getAllEditRequests(): Promise { - return this.editRequests; + async getAllEditRequests(afterId?: string): Promise { + let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; + return this.editRequests.slice(startIndex, startIndex + PAGE_SIZE); + } + + async getEditRequestsByStatus(status: string, afterId?: string): Promise { + let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; + return this.editRequests.filter((req) => req.status === status).slice(startIndex, startIndex + PAGE_SIZE); } async getEditRequestById(id: string): Promise { return this.editRequests.find((req) => req.id === id) || null; } - async updateEditRequest(body: EditRequest): Promise { - let index = this.editRequests.findIndex((req) => req.id === body.id); - this.editRequests[index] = {...this.editRequests[index], ...body}; - return this.editRequests[index]; + async getBusinessById(id: string): Promise { + return this.businesses.find((b) => b.id === id) || null; } - async getEditRequestsByStatus(status: string): Promise { - return this.editRequests.filter((req) => req.status === status); + async getEditRequestsByUser(userAppId: string): Promise { + return this.editRequests.filter((r) => r.submitter === userAppId); } - async getBusinessById(id: string): Promise { - return this.businesses.find((b) => b.id === id) || null; + async updateEditRequest(body: EditRequest): Promise { + let index = this.editRequests.findIndex((req) => req.id === body.id); + this.editRequests[index] = {...this.editRequests[index], ...body}; + return this.editRequests[index]; } - clearRegions() { this.regions = []; } + } diff --git a/tests/testUtils/testFirestore.ts b/tests/testUtils/testFirestore.ts new file mode 100644 index 0000000..18b9041 --- /dev/null +++ b/tests/testUtils/testFirestore.ts @@ -0,0 +1,13 @@ +import firebase from "firebase"; + +var firebaseConfig = { + apiKey: "AIzaSyBhRqqU7RuMT72eFPP-VuswjMCA0uObj-s", + authDomain: "ranlab-test.firebaseapp.com", + projectId: "ranlab-test", + storageBucket: "ranlab-test.appspot.com", + messagingSenderId: "861002956013", + appId: "1:861002956013:web:3d9236b7e1e8a276bd20e4", + measurementId: "G-JPX391FT8E" +}; +export const testApp = firebase.initializeApp(firebaseConfig); +export const testFirestore = testApp.firestore(); From 00cbb928fb309c8e0199c1c2f8f72e44e24515e6 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 13 Mar 2021 21:16:01 -0330 Subject: [PATCH 54/75] Added CSV export (/businesses/export) --- src/database/productionDataLayer.ts | 108 +++++++++++++-------- src/endpoints/businesses.ts | 41 +++++++- src/endpoints/docs/businessesSchemas.ts | 33 +++++-- tests/businesses.test.ts | 45 ++++++++- tests/dataLayer.test.ts | 119 +++++++++++++++++------- tests/testUtils/testDataLayer.ts | 21 +++-- tests/testUtils/testFirestore.ts | 12 +++ 7 files changed, 281 insertions(+), 98 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 337e28c..89c6894 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,4 +1,4 @@ -import {Business} from "../endpoints/businesses"; +import {Business, CHUNK_SIZE} from "../endpoints/businesses"; import {EditRequest, PAGE_SIZE} from "../endpoints/editRequest"; import firebase from "firebase"; import Timestamp = firebase.firestore.Timestamp; @@ -34,12 +34,13 @@ export interface DataLayer { deleteRegion(regionId: string): Promise; getAllRegions(): Promise; createEditRequest(add: EditRequest): Promise; - getEditRequestsForRegion(regionId: string): Promise; + getEditRequestsForRegion(regionId: string, afterId?: string): Promise; getAllEditRequests(afterId?: string): Promise; getEditRequestById(id: string): Promise; updateEditRequest(body: EditRequest): Promise; getEditRequestsByStatus(status: string, afterId?: string): Promise; getEditRequestsByUser(userAppId: string): Promise; + getAllBusinesses(afterId?: string): Promise; } export class ProductionDataLayer implements DataLayer { @@ -47,6 +48,7 @@ export class ProductionDataLayer implements DataLayer { constructor(firestore: firebase.firestore.Firestore) { this.firestore = firestore; } + async getBusinessById(id: string): Promise { let businessSnapshot = await this.firestore.collection("businesses").doc(id).get(); return {...businessSnapshot.data(), id: businessSnapshot.id}; @@ -57,6 +59,20 @@ export class ProductionDataLayer implements DataLayer { return businessSnapshot.docs.map((b) => ({...b.data(), id: b.id})); } + async getAllBusinesses(afterId?: string): Promise { + let query = this.firestore.collection("businesses") + .orderBy("name") + .limit(CHUNK_SIZE); + if(!!afterId) { + let afterBiz = await this.firestore.collection("businesses").doc(afterId).get(); + if(afterBiz) { + query = query.startAfter(afterBiz); + } + } + return this.convertToBusinesses((await query.get()).docs); + } + + async setBusiness(newBusinessData: Business) : Promise { const bc = this.firestore.collection("businesses"); let businessRef = newBusinessData.id ? bc.doc(newBusinessData.id) : bc.doc(); @@ -143,24 +159,14 @@ export class ProductionDataLayer implements DataLayer { async createEditRequest(editRequest: EditRequest): Promise { let doc = this.firestore.collection("editRequests").doc(); - await doc.set({...editRequest, createdDate: firebase.firestore.FieldValue.serverTimestamp()}); + await doc.set({ + ...editRequest, + dateSubmitted: firebase.firestore.FieldValue.serverTimestamp(), + dateUpdated: firebase.firestore.FieldValue.serverTimestamp() + }); return {id : doc.id}; } - async getEditRequestsForRegion(regionId: string) : Promise { - let regionRequests : EditRequest[] = []; - let regionDocs = (await this.firestore.collection("editRequests").where("regionId", "==", regionId).get()).docs; - regionDocs.forEach( - (req) => { - let converted = this.convertToEditRequest(req.id, req.data()); - if (!!converted) { - regionRequests.push(converted); - } - } - ); - return regionRequests; - } - async getEditRequestById(id: string) : Promise { let requestData = (await this.firestore.collection("editRequests").doc(id).get()).data(); return this.convertToEditRequest(id, requestData); @@ -169,32 +175,48 @@ export class ProductionDataLayer implements DataLayer { async updateEditRequest(body: EditRequest): Promise { let id = !!body.id ? body.id : ""; let requestData = (await this.firestore.collection("editRequests").doc(id).get()).data(); - let updatedRequestData = {...requestData, ...body}; + let updatedRequestData = { + ...this.convertToEditRequest(id, requestData), + ...body, + dateUpdated: firebase.firestore.FieldValue.serverTimestamp() + }; await this.firestore.collection("editRequests").doc(id).update(updatedRequestData); - return Promise.resolve(updatedRequestData); + let updatedRequest = await this.getEditRequestById(id); + if(!updatedRequest) { + throw "Could not retrieve updated record!"; + } else { + return updatedRequest; + } } async getAllEditRequests(afterId?: string): Promise { - let query = this.firestore.collection("editRequests") - .orderBy("createdDate", 'asc') - .limit(PAGE_SIZE); + let query = this.firestore.collection("editRequests"); return await this.getPaginatedEditRequests(query, afterId); } + async getEditRequestsForRegion(regionId: string, afterId?: string) : Promise { + let query = this.firestore.collection("editRequests").where("regionId", "==", regionId); + return this.getPaginatedEditRequests(query, afterId); + } + async getEditRequestsByStatus(status: string, afterId?: string): Promise { - let query = this.firestore.collection("editRequests") - .where("status", "==", status) - .orderBy("createdDate", 'asc') - .limit(PAGE_SIZE); + let query = this.firestore.collection("editRequests").where("status", "==", status); return await this.getPaginatedEditRequests(query, afterId); } + async getEditRequestsByUser(userAppId: string, afterId?: string): Promise { + let query = this.firestore.collection("editRequests").where("submitter", "==", userAppId); + return this.getPaginatedEditRequests(query, afterId); + } + private async getPaginatedEditRequests(query: firebase.firestore.Query, afterId: string | undefined) { let requests: EditRequest[] = []; + query = query.orderBy("dateSubmitted", 'desc').limit(PAGE_SIZE); if (!!afterId) { let afterRecord = await this.firestore.collection("editRequests").doc(afterId).get(); query = query.startAfter(afterRecord); } + let editRequestDocs = (await query.get()).docs; editRequestDocs.forEach( (doc) => { @@ -207,22 +229,8 @@ export class ProductionDataLayer implements DataLayer { return requests; } - async getEditRequestsByUser(userAppId: string): Promise { - let requests = []; - (await this.firestore.collection("editRequests").where("submitter", "==", userAppId).get()).docs.forEach( - (req) => { - let converted = this.convertToEditRequest(req.id, req.data()); - if(!!converted) { - requests.push(converted); - } - } - ); - return requests; - } - convertToEditRequest(id: string, documentData: firebase.firestore.DocumentData | undefined) : EditRequest | null { if(!!documentData) { - delete documentData.createdDate; let request = documentData; request.dateSubmitted = ((documentData.dateSubmitted)).toDate(); request.dateUpdated = ((documentData.dateUpdated)).toDate(); @@ -233,6 +241,26 @@ export class ProductionDataLayer implements DataLayer { } } + convertToBusinesses(docs: Array>) : Business[] { + let businesses = []; + for(let doc of docs) { + let biz : Business = { + id: doc.id, + regionId: doc.data().regionId, + name: doc.data().name, + employees: doc.data().employees, + industry: doc.data().industry, + year_added: doc.data().year_added + } + if(!!doc.data().location) { + biz.location = doc.data().location + } + businesses.push(biz); + } + return businesses; + } + + private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: firebase.firestore.Transaction) { let regionRef = this.firestore.collection("regions").doc(regionId); diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 260fae2..8e1d38d 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -2,10 +2,21 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; import firebase from "firebase"; import GeoPoint = firebase.firestore.GeoPoint; -import {createBizSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; +import {createBizSchema, exportBusinessesSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; import {isRegionManager} from "../utils"; import {Auth0JwtVerifier} from "../auth0"; -import {AuthenticatedRequestByRegionId} from "./endpointUtils"; +import {AuthenticatedRequest, AuthenticatedRequestByRegionId} from "./endpointUtils"; + +export const CHUNK_SIZE = 100; +export const CSV_HEADER = "'id','name','location','regionId','industry','year_added','employees','location'\n"; + +export function convertToCSV(businesses: Business[]) { + let csvChunk = ""; + for(let biz of businesses) { + csvChunk += `'${biz.id}','${biz.name}','${biz.location}','${biz.regionId}','${biz.industry}',${biz.year_added},${biz.employees},${biz.location}\n`; + } + return csvChunk; +} interface CreateBusinessRequest extends RequestGenericInterface { Params: { @@ -110,5 +121,31 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa } ); + app.get( + '/businesses/export', + {schema: exportBusinessesSchema}, + async (request,reply) => { + let {admin} = await verifyJwt(request); + + if(!admin) { + reply.unauthorized("Only admin users can export all business rows!"); + return; + } else { + reply.raw.writeHead(200, { + 'Content-Type': 'text/csv', + 'Content-Disposition': 'attachment; filename=businesses.csx' + }); + + reply.raw.write(CSV_HEADER); + let chunk = await dataLayer.getAllBusinesses(); + while(chunk.length > 0) { + reply.raw.write(convertToCSV(chunk)); + chunk = await dataLayer.getAllBusinesses(chunk[chunk.length-1].id); + } + reply.raw.end(); + } + } + ); + return app; } diff --git a/src/endpoints/docs/businessesSchemas.ts b/src/endpoints/docs/businessesSchemas.ts index d8277ba..104ae3c 100644 --- a/src/endpoints/docs/businessesSchemas.ts +++ b/src/endpoints/docs/businessesSchemas.ts @@ -27,6 +27,13 @@ export const businessesSchema = { items: businessSchema }; +const bizByIdSchema = { + type: 'object', + properties: { + businessId: {type: "string"} + } +}; + export const getBizSchema = { description: "Returns all businesses located in the specified region", params: byRegionIdSchema, @@ -45,6 +52,25 @@ export const getBizSchema = { } }; +export const exportBusinessesSchema = { + description: "Exports all business records as a stream of csv text", + responses: { + 200: { + description: 'Successful response', + headers: { + schema: { + 'transfer-encoding': 'chunked' + } + }, + content: { + schema: { + 'text/csv': 'string' + } + } + } + } +}; + export const createBizSchema = { description: "Creates a business in the specified region", params: byRegionIdSchema, @@ -61,13 +87,6 @@ export const createBizSchema = { } }; -const bizByIdSchema = { - type: 'object', - properties: { - businessId: {type: "string"} - } -}; - export const updateBizSchema = { description: "Updates the specified business", params: bizByIdSchema, diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index d142040..69b63bc 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -1,32 +1,32 @@ import {DummyDatalayer} from "./testUtils/testDataLayer"; -import {createBusinessesEndpoint} from "../src/endpoints/businesses"; +import { CHUNK_SIZE, convertToCSV, createBusinessesEndpoint, CSV_HEADER} from "../src/endpoints/businesses"; import { createDummyBusiness, createDummyRegion, dummyAdminToken, - DummyBiz, DummyRegion, + DummyBiz, DummyRegion, dummyRegionManagerToken, dummyTokenVerifier, getDummyBusinesses } from "./testUtils/dummyData"; import {getTestJwtVerifier, setupAuth0TestEnv, testify} from "./testUtils/testify"; import createRegionsEndpoint from "../src/endpoints/regions"; +import {FastifyInstance} from "fastify"; describe("Business Endpoint Tests", () => { let testDataLayer: DummyDatalayer; - + let server: FastifyInstance; beforeAll(() => { setupAuth0TestEnv(); }); beforeEach(async (done) => { testDataLayer = new DummyDatalayer(); - const server = testify(); + server = testify(); const regionsApp = createRegionsEndpoint(server, testDataLayer, getTestJwtVerifier("admin", true)); await createDummyRegion(regionsApp); done(); }); it('Can create and retrieve a valid business', async(done) => { - const server = testify(); const bizApp = createBusinessesEndpoint(server, testDataLayer, getTestJwtVerifier(DummyRegion.manager, false)) const createResponse = await createDummyBusiness(bizApp); expect(createResponse.statusCode).toBe(200); @@ -60,4 +60,39 @@ describe("Business Endpoint Tests", () => { done(); }); + it("Can export all businesses if and only if admin", async(done) => { + const bizApp = createBusinessesEndpoint(testify(), testDataLayer, dummyTokenVerifier); + async function exportBusinesses(token: string) { + return await bizApp.inject({ + method: 'GET', + url: `/businesses/export`, + headers:{authorization: `Bearer ${token}`}, + simulate: { + end: true, + split: true, + error: true, + close: true + } + }); + } + let businesses = []; + for (let i=0; i <= CHUNK_SIZE; i++) { + let iBiz = {...DummyBiz, name: `biz_${i}`} + let {id} = await testDataLayer.setBusiness(iBiz); + businesses.push({...iBiz, id}); + } + let expectedCSV = CSV_HEADER + convertToCSV(businesses); + + let managerResponse = await exportBusinesses(dummyRegionManagerToken); + expect(managerResponse.statusCode).toBe(401); + + let adminResponse = await exportBusinesses(dummyAdminToken); + expect(adminResponse.statusCode).toBe(200); + expect(adminResponse.headers['transfer-encoding']).toBe("chunked"); + expect(adminResponse.payload).toBe(expectedCSV); + + await bizApp.close(); + done(); + }); + }); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index fd1d77e..a03649d 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -1,10 +1,11 @@ import {ProductionDataLayer, Region} from "../src/database/productionDataLayer"; import {testFirestore} from "./testUtils/testFirestore"; -import {Business} from "../src/endpoints/businesses"; +import {Business, CHUNK_SIZE} from "../src/endpoints/businesses"; import objectContaining = jasmine.objectContaining; import {EditRequest, PAGE_SIZE} from "../src/endpoints/editRequest"; import arrayContaining = jasmine.arrayContaining; import {DummyBiz, DummyBizUpdate} from "./testUtils/dummyData"; +import any = jasmine.any; let productionDataLayer = new ProductionDataLayer(testFirestore); @@ -13,18 +14,11 @@ describe("Production Data Layer Integration Tests", () => { const DUMMY_REGION_2 = "DummyRegion2"; let regionId : string; - async function deleteRegionsNamed(regionName: string) { - (await testFirestore.collection("regions").where("name", "==", regionName).get()).docs - .forEach((doc) => doc.ref.delete()); - } - beforeEach(async(done) => { - (await testFirestore.collection("businesses").where("name", "==", "DummyBiz").get()).docs - .forEach((d) => d.ref.delete()); - await testFirestore.collection("years").doc("2019").delete(); + (await testFirestore.collection("businesses").get()).docs.forEach((biz) => biz.ref.delete()); + (await testFirestore.collection("years").get()).docs.forEach(yr => yr.ref.delete()); (await testFirestore.collection("editRequests").get()).docs.forEach(req => req.ref.delete()); - await deleteRegionsNamed(DUMMY_REGION_1); - await deleteRegionsNamed(DUMMY_REGION_2); + (await testFirestore.collection("regions").get()).docs.forEach(req => req.ref.delete()); done(); }); @@ -124,8 +118,6 @@ describe("Production Data Layer Integration Tests", () => { let testRequest : EditRequest = { regionId: regionId, submitter: "", - dateSubmitted: new Date(), - dateUpdated: new Date(), status: "Pending", adds:[DummyBiz], updates: [DummyBizUpdate], @@ -135,8 +127,6 @@ describe("Production Data Layer Integration Tests", () => { let spoilerRequest : EditRequest = { regionId: `Not${regionId}`, submitter: "", - dateSubmitted: new Date(), - dateUpdated: new Date(), status: "Pending", adds:[DummyBiz], updates: [DummyBizUpdate], @@ -151,43 +141,49 @@ describe("Production Data Layer Integration Tests", () => { expect(spoilerId).toBeTruthy(); spoilerRequest.id = spoilerId; + let matchForTestRequest : any = {...testRequest, dateSubmitted: any(Date), dateUpdated: any(Date)}; let editRequests = await productionDataLayer.getEditRequestsForRegion(regionId); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(1); - expect(editRequests).toStrictEqual(arrayContaining([testRequest])); + expect(editRequests).toStrictEqual(arrayContaining([matchForTestRequest])); + matchForTestRequest = editRequests[0]; let singleRequest = await productionDataLayer.getEditRequestById(id); expect(singleRequest).toBeTruthy(); if(!!singleRequest) { - expect(singleRequest).toStrictEqual(testRequest); + expect(singleRequest).toStrictEqual(matchForTestRequest); expect(singleRequest.regionId).toBe(regionId); } + let matchForSpoilerRequest = {...spoilerRequest, dateSubmitted: any(Date), dateUpdated: any(Date)} editRequests = await productionDataLayer.getAllEditRequests(); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(2); - expect(editRequests).toStrictEqual(arrayContaining([testRequest, spoilerRequest])) + expect(editRequests).toStrictEqual(arrayContaining([matchForTestRequest, matchForSpoilerRequest])) editRequests = await productionDataLayer.getEditRequestsByStatus("Pending"); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(2); - expect(editRequests).toStrictEqual(arrayContaining([testRequest, spoilerRequest])) + expect(editRequests).toStrictEqual(arrayContaining([matchForTestRequest, matchForSpoilerRequest])) let updateRequest = { id: testRequest.id, status: "Reviewed", regionId: testRequest.regionId, - dateSubmitted: testRequest.dateSubmitted, - dateUpdated: new Date(), submitter: testRequest.submitter }; let requestAfterUpdate = await productionDataLayer.updateEditRequest(updateRequest); + + function dateGreaterThan(date: Date) { + return (date2: Date) => date2 > date; + } + expect(requestAfterUpdate).toStrictEqual( objectContaining({ - ...testRequest, + ...matchForTestRequest, status: "Reviewed", - dateUpdated: updateRequest.dateUpdated + dateUpdated: dateGreaterThan(matchForTestRequest.dateUpdated) }) ); @@ -197,7 +193,7 @@ describe("Production Data Layer Integration Tests", () => { objectContaining({ ...testRequest, status: "Reviewed", - dateUpdated: updateRequest.dateUpdated + dateUpdated: dateGreaterThan(matchForTestRequest.dateUpdated) }) ); @@ -219,39 +215,90 @@ describe("Production Data Layer Integration Tests", () => { let testRequest: EditRequest = { regionId: regionId, submitter: "first", - dateSubmitted: new Date(), - dateUpdated: new Date(), status: "Pending", adds: [DummyBiz], updates: [DummyBizUpdate], deletes: [], }; + let {id: id2} = await productionDataLayer.createEditRequest({...testRequest, submitter: "second"}); + expect(id2).toBeTruthy(); + let secondPageEdit = {...testRequest, id: id2, submitter: "second"}; + let firstPageEdits = []; for(let i = 0; i < PAGE_SIZE; i++) { let {id} = await productionDataLayer.createEditRequest(testRequest); expect(id).toBeTruthy(); testRequest.id = id; - firstPageEdits.push({...testRequest}); + firstPageEdits.push({...testRequest, dateSubmitted: any(Date), dateUpdated: any(Date)}); } - let lastIdOnFirstPage = firstPageEdits[firstPageEdits.length - 1].id; - - let {id: id2} = await productionDataLayer.createEditRequest({...testRequest, submitter: "second"}); - expect(id2).toBeTruthy(); - let secondPageEdit = {...testRequest, id: id2, submitter: "second"}; + let expectedFirstPageEdits = firstPageEdits.reverse(); let firstPageRecords = await productionDataLayer.getAllEditRequests(); - expect(firstPageRecords).toStrictEqual(firstPageEdits); + expect(firstPageRecords).toStrictEqual(expectedFirstPageEdits); let firstPagePending = await productionDataLayer.getEditRequestsByStatus("Pending"); - expect(firstPagePending).toStrictEqual(firstPageEdits); + expect(firstPagePending).toStrictEqual(expectedFirstPageEdits); + let firstPageByRegion = await productionDataLayer.getEditRequestsForRegion(testRequest.regionId); + expect(firstPageByRegion).toStrictEqual(expectedFirstPageEdits); + + let lastIdOnFirstPage = firstPageRecords[firstPageRecords.length-1].id; + let matchForSecondPage = {...secondPageEdit, dateSubmitted: any(Date), dateUpdated: any(Date)}; let secondPageRecords = await productionDataLayer.getAllEditRequests(lastIdOnFirstPage); - expect(secondPageRecords).toStrictEqual([secondPageEdit]); + expect(secondPageRecords).toStrictEqual([matchForSecondPage]); let secondPagePending = await productionDataLayer.getEditRequestsByStatus("Pending", lastIdOnFirstPage); - expect(secondPagePending).toStrictEqual([secondPageEdit]); + expect(secondPagePending).toStrictEqual([matchForSecondPage]); + + let secondPageForRegion = await productionDataLayer.getEditRequestsForRegion(testRequest.regionId, lastIdOnFirstPage); + expect(secondPageForRegion).toStrictEqual([matchForSecondPage]); + + await productionDataLayer.updateEditRequest({id: id2, regionId: testRequest.regionId, submitter: "first"}); + let firstPageForUser = await productionDataLayer.getEditRequestsByUser("first"); + expect(firstPageForUser).toStrictEqual(expectedFirstPageEdits); + + let secondPageForUser = await productionDataLayer.getEditRequestsByUser("first", lastIdOnFirstPage); + expect(secondPageForUser).toStrictEqual([{...matchForSecondPage, submitter: "first"}]); done(); }); + + it("Retrieves businesses in chunks", async(done) => { + async function addBiz(i: number) { + let iBiz = {...DummyBiz, regionId, name: `biz_${i}`}; + let doc = testFirestore.collection("businesses").doc(); + await doc.set(iBiz); + expect(doc.id).toBeTruthy(); + return {...iBiz, id: doc.id}; + } + jest.setTimeout(120000); + + let region: Region = { + name: DUMMY_REGION_1, + manager: "Dummy Manager" + }; + regionId = (await productionDataLayer.setRegion(region)).id; + + let firstChunkPromises: Promise[] = []; + let firstChunkBusinesses = []; + + for(let i = 0; i < CHUNK_SIZE; i++) { + firstChunkPromises.push(addBiz(i)); + firstChunkBusinesses.push(...(await Promise.all(firstChunkPromises))); + firstChunkPromises = []; + } + + let secondChunkBiz = {...DummyBiz, regionId, name: `biz_999`}; + let{id: id2} = await productionDataLayer.setBusiness(secondChunkBiz); + expect(id2).toBeTruthy(); + + let firstChunk = await productionDataLayer.getAllBusinesses(); + expect(firstChunk).toStrictEqual(arrayContaining(firstChunkBusinesses)); + + let secondChunk = await productionDataLayer.getAllBusinesses(firstChunk[CHUNK_SIZE -1].id); + expect(secondChunk).toStrictEqual([{...secondChunkBiz, id: id2}]) + + done(); + }) }); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index f85ecf3..2c52cb6 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -1,5 +1,5 @@ import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; -import {Business} from "../../src/endpoints/businesses"; +import {Business, CHUNK_SIZE} from "../../src/endpoints/businesses"; import {EditRequest, PAGE_SIZE} from "../../src/endpoints/editRequest"; export class DummyDatalayer implements DataLayer { @@ -8,10 +8,16 @@ export class DummyDatalayer implements DataLayer { regions: Region[] = []; editRequests: EditRequest[] = []; - getBusinessesByRegion(_:string): Promise { + async getBusinessesByRegion(_:string): Promise { return Promise.resolve(this.businesses); } + async getAllBusinesses(afterId?: string): Promise { + let startIndex = this.businesses.findIndex(biz => biz.id === afterId); + startIndex = startIndex > 0 ? startIndex + 1 : 0; + return this.businesses.slice(startIndex, startIndex + CHUNK_SIZE); + } + async setBusiness(business:Business): Promise { let id = `${Math.random()}`; this.businesses.push({...business, id}); @@ -62,18 +68,18 @@ export class DummyDatalayer implements DataLayer { return {id: newRequest.id} ; } - async getEditRequestsForRegion(regionId: string): Promise { + async getEditRequestsForRegion(regionId: string, _?: string): Promise { return this.editRequests.filter((req) => req.regionId === regionId); } async getAllEditRequests(afterId?: string): Promise { - let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; - return this.editRequests.slice(startIndex, startIndex + PAGE_SIZE); + let startIndex = !!afterId? this.editRequests.reverse().findIndex((r) => r.id === afterId) + 1 : 0; + return this.editRequests.reverse().slice(startIndex, startIndex + PAGE_SIZE); } async getEditRequestsByStatus(status: string, afterId?: string): Promise { let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; - return this.editRequests.filter((req) => req.status === status).slice(startIndex, startIndex + PAGE_SIZE); + return this.editRequests.filter((req) => req.status === status).reverse().slice(startIndex, startIndex + PAGE_SIZE); } async getEditRequestById(id: string): Promise { @@ -85,7 +91,7 @@ export class DummyDatalayer implements DataLayer { } async getEditRequestsByUser(userAppId: string): Promise { - return this.editRequests.filter((r) => r.submitter === userAppId); + return this.editRequests.filter((r) => r.submitter === userAppId).reverse(); } async updateEditRequest(body: EditRequest): Promise { @@ -97,5 +103,4 @@ export class DummyDatalayer implements DataLayer { clearRegions() { this.regions = []; } - } diff --git a/tests/testUtils/testFirestore.ts b/tests/testUtils/testFirestore.ts index 18b9041..d27a5ff 100644 --- a/tests/testUtils/testFirestore.ts +++ b/tests/testUtils/testFirestore.ts @@ -1,5 +1,7 @@ import firebase from "firebase"; +// Because of some fun with quotas, I now have two possible test firestore targets, whee. +/* var firebaseConfig = { apiKey: "AIzaSyBhRqqU7RuMT72eFPP-VuswjMCA0uObj-s", authDomain: "ranlab-test.firebaseapp.com", @@ -8,6 +10,16 @@ var firebaseConfig = { messagingSenderId: "861002956013", appId: "1:861002956013:web:3d9236b7e1e8a276bd20e4", measurementId: "G-JPX391FT8E" +};*/ +var firebaseConfig = { + apiKey: "AIzaSyAyaNrOrfio5hL3DfNuexMQOpdefys7PSA", + authDomain: "temp-test-b8f04.firebaseapp.com", + projectId: "temp-test-b8f04", + storageBucket: "temp-test-b8f04.appspot.com", + messagingSenderId: "829656614445", + appId: "1:829656614445:web:bcb7083f650653310f1aa4", + measurementId: "G-180MVEJXN2" }; + export const testApp = firebase.initializeApp(firebaseConfig); export const testFirestore = testApp.firestore(); From 1b9a361ca711957a0898a6720270f35ae8021d26 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 13 Mar 2021 22:41:15 -0330 Subject: [PATCH 55/75] Fixing up a bit of pagination, fixing related tests, and rearranging things a bit. --- src/database/productionDataLayer.ts | 2 +- src/endpoints/docs/editRequestSchemas.ts | 75 ++++++++-------- src/endpoints/editRequest.ts | 25 ++++-- tests/businesses.test.ts | 6 +- tests/dataLayer.test.ts | 106 ++++++++++++++--------- tests/editRequests.test.ts | 14 +-- tests/testUtils/testDataLayer.ts | 38 ++++---- 7 files changed, 157 insertions(+), 109 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 89c6894..571a72e 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -39,7 +39,7 @@ export interface DataLayer { getEditRequestById(id: string): Promise; updateEditRequest(body: EditRequest): Promise; getEditRequestsByStatus(status: string, afterId?: string): Promise; - getEditRequestsByUser(userAppId: string): Promise; + getEditRequestsByUser(userAppId: string, afterId?: string): Promise; getAllBusinesses(afterId?: string): Promise; } diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index 22daa50..af6db4d 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -42,86 +42,91 @@ export const getEditRequestByIdSchema = { } }; -export const getEditPreviewSchema = { - params: byIdSchema, +export const getEditRequestsByRegionSchema = { + params: byRegionIdSchema, + querystring: { + type: 'object', + properties: { + afterId: {type: 'string'} + } + }, response: { 200: { description: 'Successful response', type: 'object', properties: { status: {type: 'string'}, - added: { - type: 'array', - items: businessSchema - }, - updated: { - type: 'array', - items: businessSchema - }, - deleted: { + editRequests: { type: 'array', - items: businessSchema + items: editRequestSchema } } } } }; -export const updateEditRequestSchema = { - params: byIdSchema, +export const getAllEditRequestsByStatusSchema = { + description: 'Returns all edit requests. Query string may specify the status parameter to filter for a specific status. Only usable by system admins', + querystring: { + type: 'object', + properties: { + status: {type: 'string'}, + afterId: {type: 'string'} + } + }, response: { 200: { description: 'Successful response', type: 'object', properties: { status: {type: 'string'}, - editRequest: editRequestSchema + editRequests: { + type: 'array', + items: editRequestSchema + } } } } -} - +}; -export const getEditRequestsByRegionSchema = { - params: byRegionIdSchema, +export const getEditPreviewSchema = { + params: byIdSchema, response: { 200: { description: 'Successful response', type: 'object', properties: { status: {type: 'string'}, - editRequests: { + added: { type: 'array', - items: editRequestSchema + items: businessSchema + }, + updated: { + type: 'array', + items: businessSchema + }, + deleted: { + type: 'array', + items: businessSchema } } } } }; -export const getAllEditRequestsByStatusSchema = { - description: 'Returns all edit requests. Query string may specify the status parameter to filter for a specific status. Only usable by system admins', - querystring: { - type: 'object', - properties: { - status: {type: 'string'}, - afterId: {type: 'string'} - } - }, +export const updateEditRequestSchema = { + params: byIdSchema, response: { 200: { description: 'Successful response', type: 'object', properties: { status: {type: 'string'}, - editRequests: { - type: 'array', - items: editRequestSchema - } + editRequest: editRequestSchema } } } -}; +} export const createEditRequestSchema = { description: 'Creates the supplied edit request', diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 958b792..8dc0fbe 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -31,16 +31,29 @@ interface UpdateEditRequest extends AuthenticatedRequestById { Body: EditRequest } -interface GetAllEditsByStatusRequest extends AuthenticatedRequest { +interface GetPaginatedEditsByStatusRequest extends AuthenticatedRequest { Querystring: { status: string, afterId: string } } +interface PaginatedQueryString { + afterId: string +} + +interface GetPaginatedEditsRequest extends AuthenticatedRequest { + Querystring: PaginatedQueryString +} + +interface GetPaginatedEditsByRegionId extends AuthenticatedRequestByRegionId { + Querystring: PaginatedQueryString +} + + export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { - app.get( + app.get( `/edits`, {schema: getEditRequestByIdSchema}, async (request, reply) => { @@ -51,14 +64,14 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } else { let response = { status: "ok", - editRequests: await dataLayer.getEditRequestsByUser(userAppId) + editRequests: await dataLayer.getEditRequestsByUser(userAppId, request.query.afterId) } return JSON.stringify(response); } } ); - app.get( + app.get( "/edits/all", {schema: getAllEditRequestsByStatusSchema}, async (request, reply) => { @@ -83,7 +96,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v ); - app.get( + app.get( `/region/:regionId/edits`, {schema: getEditRequestsByRegionSchema}, async (request, reply) => { @@ -96,7 +109,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v status: "ok", editRequests: [] } - response.editRequests = await dataLayer.getEditRequestsForRegion(request.params.regionId) + response.editRequests = await dataLayer.getEditRequestsForRegion(request.params.regionId, request.query.afterId) return JSON.stringify(response); } } diff --git a/tests/businesses.test.ts b/tests/businesses.test.ts index 69b63bc..fb2735f 100644 --- a/tests/businesses.test.ts +++ b/tests/businesses.test.ts @@ -9,25 +9,23 @@ import { } from "./testUtils/dummyData"; import {getTestJwtVerifier, setupAuth0TestEnv, testify} from "./testUtils/testify"; import createRegionsEndpoint from "../src/endpoints/regions"; -import {FastifyInstance} from "fastify"; describe("Business Endpoint Tests", () => { let testDataLayer: DummyDatalayer; - let server: FastifyInstance; beforeAll(() => { setupAuth0TestEnv(); }); beforeEach(async (done) => { testDataLayer = new DummyDatalayer(); - server = testify(); + const server = testify(); const regionsApp = createRegionsEndpoint(server, testDataLayer, getTestJwtVerifier("admin", true)); await createDummyRegion(regionsApp); done(); }); it('Can create and retrieve a valid business', async(done) => { - const bizApp = createBusinessesEndpoint(server, testDataLayer, getTestJwtVerifier(DummyRegion.manager, false)) + const bizApp = createBusinessesEndpoint(testify(), testDataLayer, getTestJwtVerifier(DummyRegion.manager, false)) const createResponse = await createDummyBusiness(bizApp); expect(createResponse.statusCode).toBe(200); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index a03649d..b27f7a8 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -8,6 +8,24 @@ import {DummyBiz, DummyBizUpdate} from "./testUtils/dummyData"; import any = jasmine.any; let productionDataLayer = new ProductionDataLayer(testFirestore); +function toBeLaterThan(expectedEarlierDate: Date | string | undefined, expectedLaterDate: Date) { + if(!expectedEarlierDate) { + return { + pass: false, + message: `Expected ${expectedEarlierDate} to be a date` + } + } else { + return new Date(expectedEarlierDate) < expectedLaterDate + ? { + pass: true, + message: () => `Expected ${expectedLaterDate} to be after ${expectedEarlierDate}` + } + : { + pass: false, + message: () => `Expected ${expectedLaterDate} to be after ${expectedEarlierDate}` + }; + } +} describe("Production Data Layer Integration Tests", () => { const DUMMY_REGION_1 = "DummyRegion"; @@ -175,17 +193,14 @@ describe("Production Data Layer Integration Tests", () => { let requestAfterUpdate = await productionDataLayer.updateEditRequest(updateRequest); - function dateGreaterThan(date: Date) { - return (date2: Date) => date2 > date; - } - expect(requestAfterUpdate).toStrictEqual( objectContaining({ ...matchForTestRequest, status: "Reviewed", - dateUpdated: dateGreaterThan(matchForTestRequest.dateUpdated) + dateUpdated: any(Date) }) ); + expect(toBeLaterThan(requestAfterUpdate.dateUpdated, matchForTestRequest.dateUpdated)); let readRequestAfterUpdate = await productionDataLayer.getEditRequestById(testRequest.id); expect(readRequestAfterUpdate).toBeTruthy(); @@ -193,9 +208,10 @@ describe("Production Data Layer Integration Tests", () => { objectContaining({ ...testRequest, status: "Reviewed", - dateUpdated: dateGreaterThan(matchForTestRequest.dateUpdated) + dateUpdated: any(Date) }) ); + expect(toBeLaterThan(readRequestAfterUpdate?.dateUpdated, matchForTestRequest.dateUpdated)); editRequests = await productionDataLayer.getEditRequestsByStatus("Reviewed"); expect(editRequests).toBeTruthy(); @@ -206,6 +222,7 @@ describe("Production Data Layer Integration Tests", () => { }); it("Paginates edit requests", async(done) => { + jest.setTimeout(30000); let region: Region = { name: DUMMY_REGION_1, manager: "Dummy Manager" @@ -264,41 +281,48 @@ describe("Production Data Layer Integration Tests", () => { done(); }); - it("Retrieves businesses in chunks", async(done) => { - async function addBiz(i: number) { - let iBiz = {...DummyBiz, regionId, name: `biz_${i}`}; - let doc = testFirestore.collection("businesses").doc(); - await doc.set(iBiz); - expect(doc.id).toBeTruthy(); - return {...iBiz, id: doc.id}; - } - jest.setTimeout(120000); - - let region: Region = { - name: DUMMY_REGION_1, - manager: "Dummy Manager" - }; - regionId = (await productionDataLayer.setRegion(region)).id; - - let firstChunkPromises: Promise[] = []; - let firstChunkBusinesses = []; + describe("Long running test", ()=> { + afterEach(async(done) => { + (await testFirestore.collection("businesses").get()).docs.forEach((biz) => biz.ref.delete()); + done(); + }); + it("Retrieves businesses in chunks", async(done) => { + async function addBiz(i: number) { + let iBiz = {...DummyBiz, regionId, name: `biz_${i}`}; + let doc = testFirestore.collection("businesses").doc(); + await doc.set(iBiz); + expect(doc.id).toBeTruthy(); + return {...iBiz, id: doc.id}; + } + jest.setTimeout(120000); + + let region: Region = { + name: DUMMY_REGION_1, + manager: "Dummy Manager" + }; + regionId = (await productionDataLayer.setRegion(region)).id; + + let firstChunkPromises: Promise[] = []; + let firstChunkBusinesses = []; + + for(let i = 0; i < CHUNK_SIZE; i++) { + firstChunkPromises.push(addBiz(i)); + firstChunkBusinesses.push(...(await Promise.all(firstChunkPromises))); + firstChunkPromises = []; + } + + let secondChunkBiz = {...DummyBiz, regionId, name: `biz_999`}; + let{id: id2} = await productionDataLayer.setBusiness(secondChunkBiz); + expect(id2).toBeTruthy(); + + let firstChunk = await productionDataLayer.getAllBusinesses(); + expect(firstChunk).toStrictEqual(arrayContaining(firstChunkBusinesses)); + + let secondChunk = await productionDataLayer.getAllBusinesses(firstChunk[CHUNK_SIZE -1].id); + expect(secondChunk).toStrictEqual([{...secondChunkBiz, id: id2}]) + + done(); + }); - for(let i = 0; i < CHUNK_SIZE; i++) { - firstChunkPromises.push(addBiz(i)); - firstChunkBusinesses.push(...(await Promise.all(firstChunkPromises))); - firstChunkPromises = []; - } - - let secondChunkBiz = {...DummyBiz, regionId, name: `biz_999`}; - let{id: id2} = await productionDataLayer.setBusiness(secondChunkBiz); - expect(id2).toBeTruthy(); - - let firstChunk = await productionDataLayer.getAllBusinesses(); - expect(firstChunk).toStrictEqual(arrayContaining(firstChunkBusinesses)); - - let secondChunk = await productionDataLayer.getAllBusinesses(firstChunk[CHUNK_SIZE -1].id); - expect(secondChunk).toStrictEqual([{...secondChunkBiz, id: id2}]) - - done(); }) }); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 42ee47f..103a08a 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -145,6 +145,11 @@ describe("Edit Request unit tests", () => { it("Can retrieve all edit requests with pagination", async(done) => { + const page2Response = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); + expect(page2Response.statusCode).toBe(201); + let {id: id2} = JSON.parse(page2Response.payload); + let secondPageRequest = asResponse(asInitializedEditRequest(DummyAdd, id2)); + let firstPageObjects = []; for(let i = 0; i < PAGE_SIZE; i++) { const page1Response = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); @@ -152,16 +157,13 @@ describe("Edit Request unit tests", () => { let {id} = JSON.parse(page1Response.payload); firstPageObjects.push(asResponse(asInitializedEditRequest(DummyAdd, id))); } - const page2Response = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); - expect(page2Response.statusCode).toBe(201); - let {id: id2} = JSON.parse(page2Response.payload); - let secondPageRequest = asResponse(asInitializedEditRequest(DummyAdd, id2)); const firstPageResponse = await getAllEditRequests(dummyAdminToken); expect(firstPageResponse.statusCode).toBe(200); - expect(JSON.parse(firstPageResponse.payload).editRequests).toStrictEqual(firstPageObjects); + let firstResponseEdits = JSON.parse(firstPageResponse.payload).editRequests; + expect(firstResponseEdits).toStrictEqual(arrayContaining(firstPageObjects)); - let afterId = firstPageObjects[firstPageObjects.length-1].id; + let afterId = firstResponseEdits[firstResponseEdits.length-1].id; afterId = !!afterId? afterId : ""; const secondPageResponse = await getAllEditRequests(dummyAdminToken, {afterId}); expect(secondPageResponse.statusCode).toBe(200); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 2c52cb6..37b5345 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -8,6 +8,10 @@ export class DummyDatalayer implements DataLayer { regions: Region[] = []; editRequests: EditRequest[] = []; + async getBusinessById(id: string): Promise { + return this.businesses.find((b) => b.id === id) || null; + } + async getBusinessesByRegion(_:string): Promise { return Promise.resolve(this.businesses); } @@ -68,30 +72,24 @@ export class DummyDatalayer implements DataLayer { return {id: newRequest.id} ; } - async getEditRequestsForRegion(regionId: string, _?: string): Promise { - return this.editRequests.filter((req) => req.regionId === regionId); - } - - async getAllEditRequests(afterId?: string): Promise { - let startIndex = !!afterId? this.editRequests.reverse().findIndex((r) => r.id === afterId) + 1 : 0; - return this.editRequests.reverse().slice(startIndex, startIndex + PAGE_SIZE); + async getEditRequestById(id: string): Promise { + return this.editRequests.find((req) => req.id === id) || null; } - async getEditRequestsByStatus(status: string, afterId?: string): Promise { - let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; - return this.editRequests.filter((req) => req.status === status).reverse().slice(startIndex, startIndex + PAGE_SIZE); + async getEditRequestsForRegion(regionId: string, afterId?: string): Promise { + return this.getPaginatedEditRequests(afterId, req => req.regionId === regionId); } - async getEditRequestById(id: string): Promise { - return this.editRequests.find((req) => req.id === id) || null; + async getAllEditRequests(afterId?: string): Promise { + return this.getPaginatedEditRequests(afterId, () => true); } - async getBusinessById(id: string): Promise { - return this.businesses.find((b) => b.id === id) || null; + async getEditRequestsByStatus(status: string, afterId?: string): Promise { + return this.getPaginatedEditRequests(afterId, (req) => req.status === status); } - async getEditRequestsByUser(userAppId: string): Promise { - return this.editRequests.filter((r) => r.submitter === userAppId).reverse(); + async getEditRequestsByUser(userAppId: string, afterId?: string): Promise { + return this.getPaginatedEditRequests(afterId, (r) => r.submitter === userAppId); } async updateEditRequest(body: EditRequest): Promise { @@ -100,6 +98,14 @@ export class DummyDatalayer implements DataLayer { return this.editRequests[index]; } + getPaginatedEditRequests(afterId: string | undefined, filter: (r: EditRequest) => boolean) { + this.editRequests.reverse(); + let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; + let ret = this.editRequests.filter(filter).slice(startIndex, startIndex + PAGE_SIZE); + this.editRequests.reverse(); + return ret; + } + clearRegions() { this.regions = []; } From 1389a798cdc54e6984e0a56bf6c54921a58d0f19 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 14 Mar 2021 21:31:32 -0230 Subject: [PATCH 56/75] Adding Auth0 user endpoints and custom page sizes for paginated requests --- src/auth0.ts | 111 +++++++++++++++++++++++++--- src/database/productionDataLayer.ts | 34 ++++----- src/endpoints/docs/userSchema.ts | 72 ++++++++++++++++++ src/endpoints/editRequest.ts | 18 +++-- src/endpoints/users.ts | 81 ++++++++++++++++++++ src/index.ts | 4 +- tests/auth0.test.ts | 38 +--------- tests/dataLayer.test.ts | 28 +++---- tests/editRequests.test.ts | 4 +- tests/testUtils/testDataLayer.ts | 22 +++--- tests/testUtils/testify.ts | 34 +++++++++ tests/users.test.ts | 102 +++++++++++++++++++++++++ 12 files changed, 452 insertions(+), 96 deletions(-) create mode 100644 src/endpoints/docs/userSchema.ts create mode 100644 src/endpoints/users.ts create mode 100644 tests/users.test.ts diff --git a/src/auth0.ts b/src/auth0.ts index a5222b5..b48200a 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -16,7 +16,8 @@ async function getAdminToken(refresh : boolean) { "grant_type": "client_credentials", "client_id": process.env.AUTH0_MGMT_CLIENT_ID, "client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET, - "audience": `https://${mgmtDomain}/api/v2/` + "audience": `https://${mgmtDomain}/api/v2/`, + "scope": "read:users update:users read:users_app_metadata update:users_app_metadata" }) }); let refreshJson = await refreshResponse.json(); @@ -25,25 +26,116 @@ async function getAdminToken(refresh : boolean) { return moduleAdminToken; } -async function getUserRole(userId: string, refresh = false) : Promise<{role: string}> { +export interface Auth0UserInfo { + user_id: string, // Should be URL encoded since it may contain characters that do not work well in a URL. + username?: string, + email?: string + email_verified?: boolean, + phone_number?: string + phone_verified?: boolean, + created_at?: string, + updated_at?: string, + identities?: [ + { + connection?: string, + user_id: string, + provider: string, + isSocial?: boolean + } + ], + app_metadata?: any, + user_metadata?: any, + picture?: string, + name?: string, + nickname?: string, + multifactor?: string[], + last_ip?: string, + last_login?: string, + logins_count?: number, + blocked?: boolean, + given_name?: string, + family_name?: string +} + +export interface UserInfoPatch { + blocked?: boolean, + email_verified?: boolean, + email?: string, + phone_number?: string, + phone_verified?: boolean, + user_metadata?: any, + app_metadata?: any, + given_name?: string, + family_name?: string, + name?: string, + nickname?: string, + picture?: string, + verify_email?: boolean, + verify_phone_number?: boolean, + password?: string, + connection?: string, + client_id?: string, + username?: string +} + +async function getUserRole(userId: string) { + let app_metadata = (await getUserById(userId)).app_metadata; + return {role: app_metadata.role}; +} + +/* + Get user by ID: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id + Get all users: https://auth0.com/docs/api/management/v2#!/Users/get_users + Update a user: https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id + */ + +export async function getUserById(userId: string) : Promise { + userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; + return callManagementApi(`/users/${userId}`); +} + +export async function getAllUsers(per_page? : number, page?: number) { + let querystring = ""; + if(!!per_page || !!page) { + querystring = "?"; + let perPageClause = !per_page ? "" : `per_page=${per_page}` + let pageClause = !page ? "" : `page=${page}`; + querystring = `?${perPageClause}&${pageClause}`; + } + return callManagementApi(`/users${querystring}`); +} + +export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) { + userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; + return callManagementApi(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch)); +} + +async function callManagementApi(path: string, method = "GET", body = "", refresh = false) : Promise { let mgmtDomain = process.env.AUTH0_DOMAIN; let adminToken = await getAdminToken(refresh); - let url = `https://${mgmtDomain}/api/v2/users/${userId.replace("|", "%7C")}`; - let metaResponse = await fetch(url, { + let url = encodeURI(`https://${mgmtDomain}/api/v2${path}`); + let options : any = { + method, "headers": { - "Authorization": `Bearer ${adminToken}`, + "Authorization": `Bearer ${adminToken}` } - }); + }; + if(!!body) { + options.headers["Content-Type"] = "application/json"; + options.body = body; + } + let metaResponse = await fetch(url, options); if(metaResponse.status === 200) { - let metadata = await metaResponse.json(); - return {role: metadata.app_metadata.role}; + let json = await metaResponse.json() + return json; } else if (!refresh && metaResponse.status === 401) { - return await getUserRole(userId, true); + return await callManagementApi(path, method, body,true); } else { throw new Error(JSON.stringify(metaResponse)); } } + export async function getUserInfo(authHeader: string) { try { let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, { @@ -59,7 +151,6 @@ export async function getUserInfo(authHeader: string) { } export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<{userAppId: string, admin: boolean}>; - export async function verifyJwt(request: FastifyRequest) { let authHeader = !request.headers.authorization ? "" : request.headers.authorization if(!authHeader) { diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 571a72e..e2b27c2 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,5 +1,5 @@ import {Business, CHUNK_SIZE} from "../endpoints/businesses"; -import {EditRequest, PAGE_SIZE} from "../endpoints/editRequest"; +import {EditRequest} from "../endpoints/editRequest"; import firebase from "firebase"; import Timestamp = firebase.firestore.Timestamp; @@ -27,20 +27,20 @@ export interface Filters { export interface DataLayer { setBusiness(business: Business) : Promise; getBusinessById(id: string): Promise; + getAllBusinesses(afterId?: string): Promise; getBusinessesByRegion(region: string): Promise; getFilters(regionId: string) : Promise; getRegionsManagedBy(managerId: string) : Promise; setRegion(region: Region): Promise; deleteRegion(regionId: string): Promise; getAllRegions(): Promise; - createEditRequest(add: EditRequest): Promise; - getEditRequestsForRegion(regionId: string, afterId?: string): Promise; - getAllEditRequests(afterId?: string): Promise; getEditRequestById(id: string): Promise; + getAllEditRequests(pageSize: number, afterId?: string): Promise; + getEditRequestsForRegion(regionId: string, pageSize: number, afterId?: string): Promise; + getEditRequestsByStatus(status: string, pageSize: number, afterId?: string): Promise; + getEditRequestsByUser(userAppId: string, pageSize: number, afterId?: string): Promise; + createEditRequest(add: EditRequest): Promise; updateEditRequest(body: EditRequest): Promise; - getEditRequestsByStatus(status: string, afterId?: string): Promise; - getEditRequestsByUser(userAppId: string, afterId?: string): Promise; - getAllBusinesses(afterId?: string): Promise; } export class ProductionDataLayer implements DataLayer { @@ -189,29 +189,29 @@ export class ProductionDataLayer implements DataLayer { } } - async getAllEditRequests(afterId?: string): Promise { + async getAllEditRequests(pageSize: number, afterId?: string): Promise { let query = this.firestore.collection("editRequests"); - return await this.getPaginatedEditRequests(query, afterId); + return await this.getPaginatedEditRequests(query, pageSize, afterId); } - async getEditRequestsForRegion(regionId: string, afterId?: string) : Promise { + async getEditRequestsForRegion(regionId: string, pageSize: number,afterId?: string) : Promise { let query = this.firestore.collection("editRequests").where("regionId", "==", regionId); - return this.getPaginatedEditRequests(query, afterId); + return this.getPaginatedEditRequests(query, pageSize, afterId); } - async getEditRequestsByStatus(status: string, afterId?: string): Promise { + async getEditRequestsByStatus(status: string, pageSize: number, afterId?: string): Promise { let query = this.firestore.collection("editRequests").where("status", "==", status); - return await this.getPaginatedEditRequests(query, afterId); + return await this.getPaginatedEditRequests(query, pageSize, afterId); } - async getEditRequestsByUser(userAppId: string, afterId?: string): Promise { + async getEditRequestsByUser(userAppId: string, pageSize: number, afterId?: string): Promise { let query = this.firestore.collection("editRequests").where("submitter", "==", userAppId); - return this.getPaginatedEditRequests(query, afterId); + return this.getPaginatedEditRequests(query, pageSize, afterId); } - private async getPaginatedEditRequests(query: firebase.firestore.Query, afterId: string | undefined) { + private async getPaginatedEditRequests(query: firebase.firestore.Query, pageSize: number, afterId: string | undefined) { let requests: EditRequest[] = []; - query = query.orderBy("dateSubmitted", 'desc').limit(PAGE_SIZE); + query = query.orderBy("dateSubmitted", 'desc').limit(pageSize); if (!!afterId) { let afterRecord = await this.firestore.collection("editRequests").doc(afterId).get(); query = query.startAfter(afterRecord); diff --git a/src/endpoints/docs/userSchema.ts b/src/endpoints/docs/userSchema.ts new file mode 100644 index 0000000..48df145 --- /dev/null +++ b/src/endpoints/docs/userSchema.ts @@ -0,0 +1,72 @@ +export const userInfoSchema = { + type: 'object', + properties: { + sub: {type: 'string'}, + } +}; + +export const byUserIdSchema = { + type: 'object', + properties: { + id: { + description: "userAppId - this is the second half of the user's auth0 'sub' claim (ie for auth0|123456, the userAppId is 123456)", + type: 'string' + } + } +} + +export const getUserInfoRequestSchema = { + description: 'Returns a single user specified by userAppId', + securitySchemes: [], + params: byUserIdSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + userInfo: userInfoSchema + } + } + } +}; + +export const getAllUsersRequestSchema = { + description: 'Returns a single user specified by userAppId', + securitySchemes: [], + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + userInfo: { + type: 'array', + items: userInfoSchema + } + } + } + } +}; + +export const updateUserRequestSchema = { + description: 'Updates the specified user', + params: byUserIdSchema, + securitySchemes: [], + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + date: {type: 'string'}, + userInfo: { + type: 'array', + items: userInfoSchema + } + } + } + } +}; diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 8dc0fbe..f9d31b4 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -9,7 +9,7 @@ import { getEditRequestByIdSchema, getEditRequestsByRegionSchema, updateEditRequestSchema } from "./docs/editRequestSchemas"; -export const PAGE_SIZE = 25; +export const DEFAULT_PAGE_SIZE = 25; export interface EditRequest { id?: string, @@ -34,12 +34,14 @@ interface UpdateEditRequest extends AuthenticatedRequestById { interface GetPaginatedEditsByStatusRequest extends AuthenticatedRequest { Querystring: { status: string, + pageSize: number, afterId: string } } interface PaginatedQueryString { - afterId: string + afterId: string, + pageSize: number } interface GetPaginatedEditsRequest extends AuthenticatedRequest { @@ -62,9 +64,10 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v reply.unauthorized("Must be logged in!"); return; } else { + let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; let response = { status: "ok", - editRequests: await dataLayer.getEditRequestsByUser(userAppId, request.query.afterId) + editRequests: await dataLayer.getEditRequestsByUser(userAppId, pageSize, request.query.afterId) } return JSON.stringify(response); } @@ -81,10 +84,11 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v return; } else { let editRequests : EditRequest[]; + let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; if(!request.query.status) { - editRequests = await dataLayer.getAllEditRequests(request.query.afterId); + editRequests = await dataLayer.getAllEditRequests(pageSize, request.query.afterId); } else { - editRequests = await dataLayer.getEditRequestsByStatus(request.query.status, request.query.afterId) + editRequests = await dataLayer.getEditRequestsByStatus(request.query.status, pageSize, request.query.afterId) } let response = { status: "ok", @@ -95,7 +99,6 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } ); - app.get( `/region/:regionId/edits`, {schema: getEditRequestsByRegionSchema}, @@ -105,11 +108,12 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v reply.unauthorized("Must be logged in to submit requests"); return; } else { + let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; let response = { status: "ok", editRequests: [] } - response.editRequests = await dataLayer.getEditRequestsForRegion(request.params.regionId, request.query.afterId) + response.editRequests = await dataLayer.getEditRequestsForRegion(request.params.regionId, pageSize, request.query.afterId) return JSON.stringify(response); } } diff --git a/src/endpoints/users.ts b/src/endpoints/users.ts new file mode 100644 index 0000000..6c4e20b --- /dev/null +++ b/src/endpoints/users.ts @@ -0,0 +1,81 @@ +import {FastifyInstance} from "fastify"; +import {Auth0JwtVerifier, Auth0UserInfo, getAllUsers, getUserById, updateUser, UserInfoPatch} from "../auth0"; +import {AuthenticatedRequest, AuthenticatedRequestById} from "./endpointUtils"; +import {getAllUsersRequestSchema, getUserInfoRequestSchema, updateUserRequestSchema} from "./docs/userSchema"; + +interface GetPaginatedUsersRequest extends AuthenticatedRequest { + Querystring: { + per_page?: number, + page?: number + } +} + +interface UpdateUserRequest extends AuthenticatedRequestById { + Body: UserInfoPatch +} + +export default function createUsersEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier) { + app.get( + '/users/:id', + {schema: getUserInfoRequestSchema}, + async (request, reply) => { + let {userAppId, admin} = await verifyJwt(request); + if (!userAppId) { + reply.unauthorized("Only authenticated users can access this information"); + return; + } else { + let response = { + status: "ok", + date: Date.now(), + userInfo: {} + }; + if (admin || userAppId == request.params.id) { + response.userInfo = await getUserById(userAppId); + } + return JSON.stringify(response); + } + } + ); + + app.get( + '/users', + {schema: getAllUsersRequestSchema}, + async (request, reply) => { + let {userAppId} = await verifyJwt(request); + if (!userAppId) { + reply.unauthorized("Only authenticated users can access this information"); + return; + } else { + let response = { + status: "ok", + date: Date.now(), + users: [] + }; + response.users = await getAllUsers(request.query.per_page, request.query.page); + return JSON.stringify(response); + } + } + ); + + app.post( + '/users/:id', + {schema: updateUserRequestSchema}, + async (request, reply) => { + let {admin} = await verifyJwt(request); + if (!admin) { + reply.unauthorized("Only system administrators can update user information"); + return; + } else { + let response = { + status: "ok", + date: Date.now(), + user: {} + }; + response.user = await updateUser(request.params.id, request.body); + return JSON.stringify(response); + } + } + ); + + return app; +} diff --git a/src/index.ts b/src/index.ts index 42bec97..803dbe2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import {registerSwagger} from "./swagger"; import {verifyJwt} from "./auth0"; import {createEditEndpoint} from "./endpoints/editRequest"; import {productionFirestore} from "./database/firestore"; +import createUsersEndpoint from "./endpoints/users"; let productionDataLayer = new ProductionDataLayer(productionFirestore) const port = Number(process.env.PORT || 8080); @@ -24,7 +25,8 @@ addRoutes( (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer, verifyJwt), (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer, verifyJwt), (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, verifyJwt), - (app: FastifyInstance) => createEditEndpoint(app, productionDataLayer, verifyJwt) + (app: FastifyInstance) => createEditEndpoint(app, productionDataLayer, verifyJwt), + (app: FastifyInstance) => createUsersEndpoint(app, verifyJwt) ); server.listen(port, '::', (err, address) => { diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index c7a876c..cfbbfd3 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -2,10 +2,9 @@ import fastify, {FastifyInstance} from "fastify"; import {addRoutes} from "../src/utils"; import createRegionsEndpoint from "../src/endpoints/regions"; import {DummyDatalayer} from "./testUtils/testDataLayer"; -import fetch from "node-fetch"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import fastifySensible from "fastify-sensible"; -import { setupAuth0TestEnv} from "./testUtils/testify"; +import {authenticateToTestDomain, setupAuth0TestEnv} from "./testUtils/testify"; import {DummyRegion} from "./testUtils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; import {getUserInfo, verifyJwt} from "../src/auth0"; @@ -18,7 +17,9 @@ describe("Auth0 integration tests", () => { beforeAll(async (done) => { setupAuth0TestEnv(); - await authenticateToTestDomain(); + let authTokens = await authenticateToTestDomain(); + userAccessToken = authTokens.userAccessToken; + adminAccessToken = authTokens.adminAccessToken; let userInfo = await getUserInfo(`Bearer ${userAccessToken}`); userAppId = userInfo.userId.split("|")[1]; done(); @@ -112,35 +113,4 @@ describe("Auth0 integration tests", () => { done(); }); }); - - async function authenticateToTestDomain() { - let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { - method: "POST", - headers: {'content-type': 'application/json'}, - body: JSON.stringify({ - client_id: process.env.AUTH0_CLIENT_ID, - client_secret: process.env.AUTH0_CLIENT_SECRET, - username: process.env.TEST_AUTH0_USERNAME, - password: process.env.TEST_AUTH0_PASSWORD, - scope: 'openid', - grant_type: "password" - }) - }); - let userJson = await userResponse.json() - userAccessToken = userJson.access_token; - let adminResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { - method: "POST", - headers: {'content-type': 'application/json'}, - body: JSON.stringify({ - client_id: process.env.AUTH0_CLIENT_ID, - client_secret: process.env.AUTH0_CLIENT_SECRET, - username: process.env.TEST_AUTH0_ADMIN_USERNAME, - password: process.env.TEST_AUTH0_ADMIN_PASSWORD, - scope: 'openid', - grant_type: "password" - }) - }); - let adminJson = await adminResponse.json(); - adminAccessToken = adminJson.access_token; - } }); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index b27f7a8..95717fc 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -2,7 +2,7 @@ import {ProductionDataLayer, Region} from "../src/database/productionDataLayer"; import {testFirestore} from "./testUtils/testFirestore"; import {Business, CHUNK_SIZE} from "../src/endpoints/businesses"; import objectContaining = jasmine.objectContaining; -import {EditRequest, PAGE_SIZE} from "../src/endpoints/editRequest"; +import {EditRequest, DEFAULT_PAGE_SIZE} from "../src/endpoints/editRequest"; import arrayContaining = jasmine.arrayContaining; import {DummyBiz, DummyBizUpdate} from "./testUtils/dummyData"; import any = jasmine.any; @@ -160,7 +160,7 @@ describe("Production Data Layer Integration Tests", () => { spoilerRequest.id = spoilerId; let matchForTestRequest : any = {...testRequest, dateSubmitted: any(Date), dateUpdated: any(Date)}; - let editRequests = await productionDataLayer.getEditRequestsForRegion(regionId); + let editRequests = await productionDataLayer.getEditRequestsForRegion(regionId, DEFAULT_PAGE_SIZE); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(1); expect(editRequests).toStrictEqual(arrayContaining([matchForTestRequest])); @@ -174,12 +174,12 @@ describe("Production Data Layer Integration Tests", () => { } let matchForSpoilerRequest = {...spoilerRequest, dateSubmitted: any(Date), dateUpdated: any(Date)} - editRequests = await productionDataLayer.getAllEditRequests(); + editRequests = await productionDataLayer.getAllEditRequests(DEFAULT_PAGE_SIZE); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(2); expect(editRequests).toStrictEqual(arrayContaining([matchForTestRequest, matchForSpoilerRequest])) - editRequests = await productionDataLayer.getEditRequestsByStatus("Pending"); + editRequests = await productionDataLayer.getEditRequestsByStatus("Pending", DEFAULT_PAGE_SIZE); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(2); expect(editRequests).toStrictEqual(arrayContaining([matchForTestRequest, matchForSpoilerRequest])) @@ -213,7 +213,7 @@ describe("Production Data Layer Integration Tests", () => { ); expect(toBeLaterThan(readRequestAfterUpdate?.dateUpdated, matchForTestRequest.dateUpdated)); - editRequests = await productionDataLayer.getEditRequestsByStatus("Reviewed"); + editRequests = await productionDataLayer.getEditRequestsByStatus("Reviewed", DEFAULT_PAGE_SIZE); expect(editRequests).toBeTruthy(); expect(editRequests.length).toBe(1); expect(editRequests).toStrictEqual(arrayContaining([objectContaining({...testRequest, ...updateRequest, status: "Reviewed"})])) @@ -243,7 +243,7 @@ describe("Production Data Layer Integration Tests", () => { let secondPageEdit = {...testRequest, id: id2, submitter: "second"}; let firstPageEdits = []; - for(let i = 0; i < PAGE_SIZE; i++) { + for(let i = 0; i < DEFAULT_PAGE_SIZE; i++) { let {id} = await productionDataLayer.createEditRequest(testRequest); expect(id).toBeTruthy(); testRequest.id = id; @@ -251,31 +251,31 @@ describe("Production Data Layer Integration Tests", () => { } let expectedFirstPageEdits = firstPageEdits.reverse(); - let firstPageRecords = await productionDataLayer.getAllEditRequests(); + let firstPageRecords = await productionDataLayer.getAllEditRequests(DEFAULT_PAGE_SIZE); expect(firstPageRecords).toStrictEqual(expectedFirstPageEdits); - let firstPagePending = await productionDataLayer.getEditRequestsByStatus("Pending"); + let firstPagePending = await productionDataLayer.getEditRequestsByStatus("Pending", DEFAULT_PAGE_SIZE); expect(firstPagePending).toStrictEqual(expectedFirstPageEdits); - let firstPageByRegion = await productionDataLayer.getEditRequestsForRegion(testRequest.regionId); + let firstPageByRegion = await productionDataLayer.getEditRequestsForRegion(testRequest.regionId, DEFAULT_PAGE_SIZE); expect(firstPageByRegion).toStrictEqual(expectedFirstPageEdits); let lastIdOnFirstPage = firstPageRecords[firstPageRecords.length-1].id; let matchForSecondPage = {...secondPageEdit, dateSubmitted: any(Date), dateUpdated: any(Date)}; - let secondPageRecords = await productionDataLayer.getAllEditRequests(lastIdOnFirstPage); + let secondPageRecords = await productionDataLayer.getAllEditRequests(DEFAULT_PAGE_SIZE, lastIdOnFirstPage); expect(secondPageRecords).toStrictEqual([matchForSecondPage]); - let secondPagePending = await productionDataLayer.getEditRequestsByStatus("Pending", lastIdOnFirstPage); + let secondPagePending = await productionDataLayer.getEditRequestsByStatus("Pending", DEFAULT_PAGE_SIZE, lastIdOnFirstPage); expect(secondPagePending).toStrictEqual([matchForSecondPage]); - let secondPageForRegion = await productionDataLayer.getEditRequestsForRegion(testRequest.regionId, lastIdOnFirstPage); + let secondPageForRegion = await productionDataLayer.getEditRequestsForRegion(testRequest.regionId, DEFAULT_PAGE_SIZE, lastIdOnFirstPage); expect(secondPageForRegion).toStrictEqual([matchForSecondPage]); await productionDataLayer.updateEditRequest({id: id2, regionId: testRequest.regionId, submitter: "first"}); - let firstPageForUser = await productionDataLayer.getEditRequestsByUser("first"); + let firstPageForUser = await productionDataLayer.getEditRequestsByUser("first", DEFAULT_PAGE_SIZE); expect(firstPageForUser).toStrictEqual(expectedFirstPageEdits); - let secondPageForUser = await productionDataLayer.getEditRequestsByUser("first", lastIdOnFirstPage); + let secondPageForUser = await productionDataLayer.getEditRequestsByUser("first", DEFAULT_PAGE_SIZE, lastIdOnFirstPage); expect(secondPageForUser).toStrictEqual([{...matchForSecondPage, submitter: "first"}]); done(); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 103a08a..3ae2922 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -1,6 +1,6 @@ import {DummyDatalayer} from "./testUtils/testDataLayer"; import { testify} from "./testUtils/testify"; -import {createEditEndpoint, EditRequest, PAGE_SIZE} from "../src/endpoints/editRequest"; +import {createEditEndpoint, EditRequest, DEFAULT_PAGE_SIZE} from "../src/endpoints/editRequest"; import { dummyAdminToken, DummyBiz, @@ -151,7 +151,7 @@ describe("Edit Request unit tests", () => { let secondPageRequest = asResponse(asInitializedEditRequest(DummyAdd, id2)); let firstPageObjects = []; - for(let i = 0; i < PAGE_SIZE; i++) { + for(let i = 0; i < DEFAULT_PAGE_SIZE; i++) { const page1Response = await submitEditRequest(DummyAdd, DummyRegion.name, dummyRegionManagerToken); expect(page1Response.statusCode).toBe(201); let {id} = JSON.parse(page1Response.payload); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 37b5345..12ef2a3 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -1,6 +1,6 @@ import {DataLayer, Filters, IdObject, Region} from "../../src/database/productionDataLayer"; import {Business, CHUNK_SIZE} from "../../src/endpoints/businesses"; -import {EditRequest, PAGE_SIZE} from "../../src/endpoints/editRequest"; +import {EditRequest} from "../../src/endpoints/editRequest"; export class DummyDatalayer implements DataLayer { @@ -76,20 +76,20 @@ export class DummyDatalayer implements DataLayer { return this.editRequests.find((req) => req.id === id) || null; } - async getEditRequestsForRegion(regionId: string, afterId?: string): Promise { - return this.getPaginatedEditRequests(afterId, req => req.regionId === regionId); + async getEditRequestsForRegion(regionId: string, pageSize: number, afterId?: string): Promise { + return this.getPaginatedEditRequests(pageSize, afterId, req => req.regionId === regionId); } - async getAllEditRequests(afterId?: string): Promise { - return this.getPaginatedEditRequests(afterId, () => true); + async getAllEditRequests(pageSize: number, afterId?: string): Promise { + return this.getPaginatedEditRequests(pageSize, afterId, () => true); } - async getEditRequestsByStatus(status: string, afterId?: string): Promise { - return this.getPaginatedEditRequests(afterId, (req) => req.status === status); + async getEditRequestsByStatus(status: string, pageSize: number, afterId?: string): Promise { + return this.getPaginatedEditRequests(pageSize, afterId, (req) => req.status === status); } - async getEditRequestsByUser(userAppId: string, afterId?: string): Promise { - return this.getPaginatedEditRequests(afterId, (r) => r.submitter === userAppId); + async getEditRequestsByUser(userAppId: string, pageSize: number, afterId?: string): Promise { + return this.getPaginatedEditRequests(pageSize, afterId, (r) => r.submitter === userAppId); } async updateEditRequest(body: EditRequest): Promise { @@ -98,10 +98,10 @@ export class DummyDatalayer implements DataLayer { return this.editRequests[index]; } - getPaginatedEditRequests(afterId: string | undefined, filter: (r: EditRequest) => boolean) { + getPaginatedEditRequests(pageSize: number, afterId: string | undefined, filter: (r: EditRequest) => boolean) { this.editRequests.reverse(); let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; - let ret = this.editRequests.filter(filter).slice(startIndex, startIndex + PAGE_SIZE); + let ret = this.editRequests.filter(filter).slice(startIndex, startIndex + pageSize); this.editRequests.reverse(); return ret; } diff --git a/tests/testUtils/testify.ts b/tests/testUtils/testify.ts index 567fcaa..83087ec 100644 --- a/tests/testUtils/testify.ts +++ b/tests/testUtils/testify.ts @@ -2,6 +2,7 @@ import fastify, {FastifyRequest} from "fastify"; import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; import jwt from "jsonwebtoken"; import fastifySensible from "fastify-sensible"; +import fetch from "node-fetch"; export const AUTH0_CLAIMS_NAMESPACE = "https://mun.ca"; export const mockSecret = 'dummy'; @@ -19,6 +20,39 @@ export function setupAuth0TestEnv() { // process.env.TEST_AUTH0_CLIENT_SECRET must be set in local runtime environment } +export async function authenticateToTestDomain() { + let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { + method: "POST", + headers: {'content-type': 'application/json'}, + body: JSON.stringify({ + client_id: process.env.AUTH0_CLIENT_ID, + client_secret: process.env.AUTH0_CLIENT_SECRET, + username: process.env.TEST_AUTH0_USERNAME, + password: process.env.TEST_AUTH0_PASSWORD, + scope: 'openid', + grant_type: "password" + }) + }); + let userJson = await userResponse.json() + let userAccessToken = userJson.access_token; + let adminResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { + method: "POST", + headers: {'content-type': 'application/json'}, + body: JSON.stringify({ + client_id: process.env.AUTH0_CLIENT_ID, + client_secret: process.env.AUTH0_CLIENT_SECRET, + username: process.env.TEST_AUTH0_ADMIN_USERNAME, + password: process.env.TEST_AUTH0_ADMIN_PASSWORD, + scope: 'openid', + grant_type: "password" + }) + }); + let adminJson = await adminResponse.json(); + let adminAccessToken = adminJson.access_token; + return {userAccessToken, adminAccessToken}; +} + + export function getMockToken(payload: {userAppId: string, admin: boolean }) { let token : any = {}; token['sub'] = `auth0|${payload.userAppId}`; diff --git a/tests/users.test.ts b/tests/users.test.ts new file mode 100644 index 0000000..8acab80 --- /dev/null +++ b/tests/users.test.ts @@ -0,0 +1,102 @@ +import {authenticateToTestDomain, setupAuth0TestEnv, testify} from "./testUtils/testify"; +import createUsersEndpoint from "../src/endpoints/users"; +import { verifyJwt } from "../src/auth0"; + +describe("Auth0 user endpoint tests", () => { + let userAccessToken: string; + let adminAccessToken: string; + it("Can get all users as regular or admin user", async (done) => { + async function getAllUsers(token:string) { + return await userApp.inject({ + method: "GET", + url: "/users", + headers: {authorization: `Bearer ${token}`} + }); + } + let server = testify(); + let userApp = createUsersEndpoint(server, verifyJwt); + setupAuth0TestEnv(); + let authTokens = await authenticateToTestDomain(); + userAccessToken = authTokens.userAccessToken; + adminAccessToken = authTokens.adminAccessToken; + + let userResponse = await getAllUsers(userAccessToken); + expect(userResponse.statusCode).toBe(200); + let userPayload = JSON.parse(userResponse.payload); + expect(userPayload.users).toBeTruthy(); + expect(userPayload.users.length).toBeGreaterThanOrEqual(1); + + let adminResponse = await getAllUsers(adminAccessToken); + expect(adminResponse.statusCode).toBe(200); + let adminPayload = JSON.parse(adminResponse.payload); + expect(adminPayload.users).toBeTruthy(); + expect(adminPayload.users.length).toBeGreaterThanOrEqual(1); + + done(); + }); + + it("Can get single user by ID as regular or admin user", async (done) => { + async function getUserById(id: string, token:string) { + return await userApp.inject({ + method: "GET", + url: `/users/${id}`, + headers: {authorization: `Bearer ${token}`} + }); + } + let server = testify(); + let userApp = createUsersEndpoint(server, verifyJwt); + setupAuth0TestEnv(); + let authTokens = await authenticateToTestDomain(); + userAccessToken = authTokens.userAccessToken; + adminAccessToken = authTokens.adminAccessToken; + + const TEST_USER_ID = "601e3f8c531b71006cb088a2"; + + let userResponse = await getUserById(TEST_USER_ID, userAccessToken); + expect(userResponse.statusCode).toBe(200); + expect(userResponse.payload).toBeTruthy(); + + let adminResponse = await getUserById(TEST_USER_ID, adminAccessToken); + expect(adminResponse.statusCode).toBe(200); + expect(adminResponse.payload).toBeTruthy(); + + done(); + }); + + it("Can update a user as an admin but not a regular user", async (done) => { + async function updateUser(id: string, updated: string, token:string) { + return await userApp.inject({ + method: "POST", + url: `/users/${id}`, + payload: {user_metadata: {updated: updated}}, + headers: {authorization: `Bearer ${token}`} + }); + } + let server = testify(); + let userApp = createUsersEndpoint(server, verifyJwt); + setupAuth0TestEnv(); + let authTokens = await authenticateToTestDomain(); + userAccessToken = authTokens.userAccessToken; + adminAccessToken = authTokens.adminAccessToken; + + const TEST_USER_ID = "601e3f8c531b71006cb088a2"; + const updatedDate = new Date().toISOString(); + + let userResponse = await updateUser(TEST_USER_ID, updatedDate, userAccessToken); + expect(userResponse.statusCode).toBe(401); + + let adminResponse = await updateUser(TEST_USER_ID, updatedDate, adminAccessToken); + expect(adminResponse.statusCode).toBe(200); + let adminPayload = JSON.parse(adminResponse.payload); + expect(adminPayload.user).toBeTruthy(); + expect(adminPayload.user.user_metadata.updated).toBe(updatedDate); + + done(); + }); + + /* + let userInfo = await getUserInfo(`Bearer ${userAccessToken}`); + let userAppId = userInfo.userId.split("|")[1]; + + */ +}); From 795579ef197c71a2b68253e8efb91453503f1177 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 14 Mar 2021 21:36:07 -0230 Subject: [PATCH 57/75] Fixing documentation --- src/endpoints/docs/userSchema.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/endpoints/docs/userSchema.ts b/src/endpoints/docs/userSchema.ts index 48df145..0870f1d 100644 --- a/src/endpoints/docs/userSchema.ts +++ b/src/endpoints/docs/userSchema.ts @@ -33,8 +33,21 @@ export const getUserInfoRequestSchema = { }; export const getAllUsersRequestSchema = { - description: 'Returns a single user specified by userAppId', + description: 'Returns all users, with optional pagination', securitySchemes: [], + querystring: { + type: 'object', + properties: { + per_page: { + type: 'number', + description: 'Number of users per page' + }, + page: { + type: 'number', + description: 'Page number to retrieve' + } + } + }, response: { 200: { description: 'Successful response', @@ -62,10 +75,7 @@ export const updateUserRequestSchema = { properties: { status: {type: 'string'}, date: {type: 'string'}, - userInfo: { - type: 'array', - items: userInfoSchema - } + userInfo: userInfoSchema } } } From cfd85f252432efb4e2c98d366cbce4e53f275da5 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 18 Mar 2021 20:16:29 -0230 Subject: [PATCH 58/75] Adding reviewer --- src/endpoints/editRequest.ts | 17 +++++++++++++++-- tests/dataLayer.test.ts | 4 ++++ tests/editRequests.test.ts | 15 ++++++++++++--- tests/testUtils/dummyData.ts | 5 +++-- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index f9d31b4..4fb2111 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -15,6 +15,7 @@ export interface EditRequest { id?: string, regionId: string, submitter?: string, + reviewer?: string, dateSubmitted?: Date | string, dateUpdated?: Date | string status?: string, @@ -149,10 +150,22 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v reply.unauthorized("Only administrators and managers may update edit requests!"); return; } else { - return { + if(!!request.body.status) { + let currentRequest = await dataLayer.getEditRequestById(request.params.id); + if (currentRequest?.status !== request.body.status) { + if (!currentRequest?.reviewer) { + request.body.reviewer = userAppId; + } else { + reply.forbidden("Cannot edit the status of a request after review has been started!") + return; + } + } + } + let response = { status: "ok", editRequest: await dataLayer.updateEditRequest({...request.body, id: request.params.id}) - }; + } + return JSON.stringify(response); } } ); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 95717fc..32c30e5 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -33,6 +33,7 @@ describe("Production Data Layer Integration Tests", () => { let regionId : string; beforeEach(async(done) => { + jest.setTimeout(30000); (await testFirestore.collection("businesses").get()).docs.forEach((biz) => biz.ref.delete()); (await testFirestore.collection("years").get()).docs.forEach(yr => yr.ref.delete()); (await testFirestore.collection("editRequests").get()).docs.forEach(req => req.ref.delete()); @@ -42,6 +43,7 @@ describe("Production Data Layer Integration Tests", () => { }); it("Creates, retrieves, updates, and deletes businesses while maintaining correct region filter data", async (done) => { + jest.setTimeout(10000); let biz : Business = { employees: 1, name: "DummyBiz", @@ -86,6 +88,7 @@ describe("Production Data Layer Integration Tests", () => { }); it("Creates, retrieves, updates, and deletes regions", async (done) => { + jest.setTimeout(10000); let region: Region = { name: DUMMY_REGION_1, manager: "Dummy Manager" @@ -127,6 +130,7 @@ describe("Production Data Layer Integration Tests", () => { }); it("Creates, retrieves, and updates edit requests", async(done) => { + jest.setTimeout(10000); let region: Region = { name: DUMMY_REGION_1, manager: "Dummy Manager" diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 3ae2922..a2d9999 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -2,6 +2,7 @@ import {DummyDatalayer} from "./testUtils/testDataLayer"; import { testify} from "./testUtils/testify"; import {createEditEndpoint, EditRequest, DEFAULT_PAGE_SIZE} from "../src/endpoints/editRequest"; import { + dummyAdminId, dummyAdminToken, DummyBiz, DummyRegion, @@ -135,7 +136,13 @@ describe("Edit Request unit tests", () => { expect(adminResponse.statusCode).toBe(200); expect(JSON.parse(adminResponse.payload).editRequests).toStrictEqual( arrayContaining([ - objectContaining({...DummyAdd, id: id1, status: "Reviewed", dateSubmitted: any(String), dateUpdated: any(String)}), + objectContaining({ + ...DummyAdd, + id: id1, + status: "Reviewed", + reviewer: dummyAdminId, + dateSubmitted: any(String), + dateUpdated: any(String)}) ]) ); @@ -190,7 +197,8 @@ describe("Edit Request unit tests", () => { const updatedRequest = asResponse({ ...createdRequest, - status: "Approved" + status: "Approved", + reviewer: dummyAdminId }); expect(JSON.parse(updateResponse.payload).editRequest).toStrictEqual(objectContaining(updatedRequest)); @@ -281,7 +289,7 @@ describe("Edit Request unit tests", () => { } async function updateEditRequestStatus(id: string, newStatus: string, token: string) : Promise { - return testApp.inject({ + let temp = await testApp.inject({ method: "POST", url: `/edits/${id}`, payload: {status: newStatus}, @@ -289,6 +297,7 @@ describe("Edit Request unit tests", () => { authorization: `Bearer ${token}` } }); + return temp; } async function getEditRequestsByRegion(regionId: string, token: string) { diff --git a/tests/testUtils/dummyData.ts b/tests/testUtils/dummyData.ts index 22e4830..db543ca 100644 --- a/tests/testUtils/dummyData.ts +++ b/tests/testUtils/dummyData.ts @@ -4,8 +4,9 @@ import {Region} from "../../src/database/productionDataLayer"; import {getMockToken} from "./testify"; const dummyManager = "DummyManagerId"; +export const dummyAdminId = "admin"; export const dummyRegionManagerToken = getMockToken({userAppId: dummyManager, admin: false}) -export const dummyAdminToken = getMockToken({userAppId: "admin", admin: true}); +export const dummyAdminToken = getMockToken({userAppId: dummyAdminId, admin: true}); export const DummyRegion: Region = { name: "DummyRegion", @@ -34,7 +35,7 @@ export async function dummyTokenVerifier (req: FastifyRequest) { return {userAppId: "", admin: false}; } if(req.headers.authorization.indexOf(dummyAdminToken) > 0) { - return {userAppId: "admin", admin: true}; + return {userAppId: dummyAdminId, admin: true}; } else if (req.headers.authorization?.indexOf(dummyRegionManagerToken) > 0) { return {userAppId: DummyRegion.manager, admin: false}; } else { From 3742ac9efe10801ef7b21192f1eb4820e31059e8 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 18 Mar 2021 22:47:00 -0230 Subject: [PATCH 59/75] Implemented Approval flow for edit requests --- src/database/productionDataLayer.ts | 49 +++++++++--------- src/endpoints/editRequest.ts | 61 +++++++++++++++++++++- tests/editRequests.test.ts | 79 ++++++++++++++++++++++------- tests/testUtils/testDataLayer.ts | 17 +++++-- 4 files changed, 161 insertions(+), 45 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index e2b27c2..ef9a55b 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -25,10 +25,11 @@ export interface Filters { } export interface DataLayer { - setBusiness(business: Business) : Promise; getBusinessById(id: string): Promise; getAllBusinesses(afterId?: string): Promise; getBusinessesByRegion(region: string): Promise; + setBusiness(business: Business) : Promise; + deleteBusiness(id: string): Promise; getFilters(regionId: string) : Promise; getRegionsManagedBy(managerId: string) : Promise; setRegion(region: Region): Promise; @@ -41,6 +42,7 @@ export interface DataLayer { getEditRequestsByUser(userAppId: string, pageSize: number, afterId?: string): Promise; createEditRequest(add: EditRequest): Promise; updateEditRequest(body: EditRequest): Promise; + } export class ProductionDataLayer implements DataLayer { @@ -72,7 +74,6 @@ export class ProductionDataLayer implements DataLayer { return this.convertToBusinesses((await query.get()).docs); } - async setBusiness(newBusinessData: Business) : Promise { const bc = this.firestore.collection("businesses"); let businessRef = newBusinessData.id ? bc.doc(newBusinessData.id) : bc.doc(); @@ -108,6 +109,29 @@ export class ProductionDataLayer implements DataLayer { }).then(() => {id: businessRef.id}); } + async deleteBusiness(id: string) { + await this.firestore.runTransaction( + async transaction => { + let businessRef = this.firestore.collection("businesses").doc(id); + let businessDoc = await transaction.get(businessRef); + let businessData = businessDoc.data(); + if (!!businessData) { + if (!!businessData.regionId) { + let deleteBizFromFilter = { + years: [{year: businessData.year_added, count: -1}], + industries: [{industry: businessData.industry, count: -1}] + } + + await this.updateRegionFilters(businessData.regionId, deleteBizFromFilter, transaction); + } + await transaction.delete(businessRef); + } + } + ); + } + + + async getFilters(region: string) : Promise{ let regionData = (await this.firestore.collection("regions").doc(region).get()).data(); regionData = !!regionData ? regionData : {}; @@ -136,27 +160,6 @@ export class ProductionDataLayer implements DataLayer { await this.firestore.collection("regions").doc(id).delete(); } - async deleteBusiness(id: string) { - await this.firestore.runTransaction( - async transaction => { - let businessRef = this.firestore.collection("businesses").doc(id); - let businessDoc = await transaction.get(businessRef); - let businessData = businessDoc.data(); - if (!!businessData) { - if (!!businessData.regionId) { - let deleteBizFromFilter = { - years: [{year: businessData.year_added, count: -1}], - industries: [{industry: businessData.industry, count: -1}] - } - - await this.updateRegionFilters(businessData.regionId, deleteBizFromFilter, transaction); - } - await transaction.delete(businessRef); - } - } - ); - } - async createEditRequest(editRequest: EditRequest): Promise { let doc = this.firestore.collection("editRequests").doc(); await doc.set({ diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 4fb2111..909848a 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -10,6 +10,7 @@ import { } from "./docs/editRequestSchemas"; export const DEFAULT_PAGE_SIZE = 25; +export const previewAddId = "[preview-id]" export interface EditRequest { id?: string, @@ -138,6 +139,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } } ); + app.post( `/edits/:id`, {schema: updateEditRequestSchema}, @@ -159,6 +161,19 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v reply.forbidden("Cannot edit the status of a request after review has been started!") return; } + if(request.body.status.toLowerCase() === "approved") { + let applied = await approveAndApplyEditRequest(request.params.id, userAppId); + if (applied) { + let response = { + status: "ok", + ...applied + }; + return JSON.stringify(response); + } else { + reply.badRequest("Unable to process approval"); + return; + } + } } } let response = { @@ -184,7 +199,7 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v let deletedBusinesses: Business[] = []; let editRequest = await dataLayer.getEditRequestById(request.params.id); for(const add of editRequest?.adds || []) { - addedBusinesses.push({...add, id: "some-id"}); + addedBusinesses.push({...add, id: previewAddId}); } for (const update of editRequest?.updates || []) { let biz = await dataLayer.getBusinessById(update.id) @@ -231,5 +246,49 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } } ); + + async function approveAndApplyEditRequest(id: string, reviewer: string) { + let addedBusinesses: Business[] = []; + let updatedBusinesses: Business[] = []; + let deletedBusinesses: Business[] = []; + let editRequest = await dataLayer.getEditRequestById(id); + if(!!editRequest) { + let editPromises = []>[]; + for (const add of editRequest?.adds || []) { + editPromises.push(dataLayer.setBusiness(add)); + addedBusinesses.push({...add, id: previewAddId}); + } + for (const update of editRequest?.updates || []) { + let biz = await dataLayer.getBusinessById(update.id) + if (!!biz) { + let updatedBiz = {...biz, ...update}; + editPromises.push(dataLayer.setBusiness(updatedBiz)); + updatedBusinesses.push(updatedBiz); + } + } + for (const delId of editRequest?.deletes || []) { + let biz = await dataLayer.getBusinessById(delId); + if (!!biz) { + editPromises.push(dataLayer.deleteBusiness(delId)); + deletedBusinesses.push(biz); + } + } + + let updatedEditRequest = {...editRequest, status: "Approved", reviewer}; + editPromises.push(dataLayer.updateEditRequest(updatedEditRequest)) + await Promise.all(editPromises); + return { + added: addedBusinesses, + updated: updatedBusinesses, + deleted: deletedBusinesses, + editRequest: updatedEditRequest + } + } else { + return false; + } + } + + return app; + } diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index a2d9999..42fc5e9 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -1,6 +1,6 @@ import {DummyDatalayer} from "./testUtils/testDataLayer"; import { testify} from "./testUtils/testify"; -import {createEditEndpoint, EditRequest, DEFAULT_PAGE_SIZE} from "../src/endpoints/editRequest"; +import {createEditEndpoint, EditRequest, DEFAULT_PAGE_SIZE, previewAddId} from "../src/endpoints/editRequest"; import { dummyAdminId, dummyAdminToken, @@ -186,18 +186,18 @@ describe("Edit Request unit tests", () => { expect(submitResponse.statusCode).toBe(201); let createdRequest = {...DummyAdd, id: JSON.parse(submitResponse.payload).id}; - const unauthorizedResponse = await updateEditRequestStatus(createdRequest.id, "Approved", ""); + const unauthorizedResponse = await updateEditRequestStatus(createdRequest.id, "Reviewed", ""); expect(unauthorizedResponse.statusCode).toBe(401); - const regionManagerResponse = await updateEditRequestStatus(createdRequest.id, "Approved", dummyRegionManagerToken); + const regionManagerResponse = await updateEditRequestStatus(createdRequest.id, "Reviewed", dummyRegionManagerToken); expect(regionManagerResponse.statusCode).toBe(401); - const updateResponse = await updateEditRequestStatus(createdRequest.id, "Approved", dummyAdminToken); + const updateResponse = await updateEditRequestStatus(createdRequest.id, "Reviewed", dummyAdminToken); expect(updateResponse.statusCode).toBe(200); const updatedRequest = asResponse({ ...createdRequest, - status: "Approved", + status: "Reviewed", reviewer: dummyAdminId }); expect(JSON.parse(updateResponse.payload).editRequest).toStrictEqual(objectContaining(updatedRequest)); @@ -210,7 +210,7 @@ describe("Edit Request unit tests", () => { done(); }); - it("Can show a preview of the records that would be changed by an edit request", async(done) => { + it("Implements Approval preview and workflow", async(done) => { async function createRegion(regionApp: FastifyInstance, region: Region) { let temp = await regionApp.inject({ method: "POST", @@ -234,38 +234,83 @@ describe("Edit Request unit tests", () => { const regionApp = createRegionsEndpoint(testApp, testDataLayer, dummyTokenVerifier); const bizApp = createBusinessesEndpoint(testApp, testDataLayer, dummyTokenVerifier); await createRegion(regionApp, DummyRegion); - let biz1 = {...DummyBiz, name: `UpdatingName`, employees: DummyBiz.employees + 10, industry: `OriginalIndustry}`}; - let biz2 = {...DummyBiz, name: "Deleting"}; - let {businessId: bizId1} = JSON.parse((await createBusiness(bizApp, biz1)).payload); - let {businessId: bizId2} = JSON.parse((await createBusiness(bizApp, biz2)).payload); + let updatedBiz = {...DummyBiz, name: `UpdatingName`, employees: DummyBiz.employees + 10, industry: `OriginalIndustry}`}; + let deletedBiz = {...DummyBiz, name: "Deleting"}; + let {businessId: updatedBizId} = JSON.parse((await createBusiness(bizApp, updatedBiz)).payload); + let {businessId: deletedBizId} = JSON.parse((await createBusiness(bizApp, deletedBiz)).payload); + updatedBiz.id = updatedBizId; + deletedBiz.id = deletedBizId; const request : EditRequest = { ...DummyAdd, updates: [{ - id: bizId1, + id: updatedBizId, name: "UpdatedName", industry: "UpdatedIndustry" }], - deletes: [bizId2] + deletes: [deletedBizId] }; let submitResponse = await submitEditRequest(request, DummyRegion.name, dummyRegionManagerToken); expect(submitResponse.statusCode).toBe(201); - let {id} = JSON.parse(submitResponse.payload); + let {id: editRequestId} = JSON.parse(submitResponse.payload); - let previewResponse = await getEditPreview(id, dummyAdminToken); + let previewResponse = await getEditPreview(editRequestId, dummyAdminToken); expect(previewResponse.statusCode).toBe(200); expect(JSON.parse(previewResponse.payload)).toStrictEqual( objectContaining({ - added: arrayContaining([objectContaining({...DummyBiz, id: any(String)})]), - updated: arrayContaining([objectContaining({...biz1, id: bizId1, name: "UpdatedName", industry: "UpdatedIndustry"})]), - deleted: arrayContaining([objectContaining({...biz2, id: bizId2})]) + added: arrayContaining([objectContaining({...DummyBiz, id: previewAddId})]), + updated: arrayContaining([objectContaining({...updatedBiz, name: "UpdatedName", industry: "UpdatedIndustry"})]), + deleted: arrayContaining([objectContaining({...deletedBiz})]) }) ); + let postPreviewResponse = await getBusinessesByRegion(bizApp, DummyBiz.regionId); + expect(JSON.parse(postPreviewResponse.payload).businesses).toStrictEqual(arrayContaining([ + updatedBiz, + deletedBiz + ])); + + let approvalResponse = await updateEditRequestStatus(editRequestId, "Approved", dummyAdminToken); + let approvalPayload = JSON.parse(approvalResponse.payload); + expect(approvalPayload).toStrictEqual( + objectContaining({ + added: any(Object), + updated: any(Object), + deleted: any(Object), + editRequest: objectContaining({ + status: "Approved", + submitter: DummyRegion.manager, + reviewer: dummyAdminId, + dateSubmitted: any(String), + dateUpdated: any(String) + }) + }) + ); + expect(approvalPayload.added).toStrictEqual(arrayContaining([objectContaining({...DummyBiz, id: any(String)})])); + expect(approvalPayload.updated).toStrictEqual(arrayContaining([objectContaining({...updatedBiz, name: "UpdatedName", industry: "UpdatedIndustry"})])); + expect(approvalPayload.deleted).toStrictEqual(arrayContaining([objectContaining({...deletedBiz})])); + + let postApprovalResponse = await getBusinessesByRegion(regionApp, DummyBiz.regionId); + let businesses = JSON.parse(postApprovalResponse.payload).businesses; + expect(businesses).toStrictEqual(arrayContaining([ + {...updatedBiz, name: "UpdatedName", industry: "UpdatedIndustry"}, + {...DummyBiz, id: any(String)} + ])); + expect(businesses.find((b:Business) => b.name === DummyBiz.name).id).not.toBe(previewAddId); + await testApp.close(); done(); }); + async function getBusinessesByRegion(bizEndpoint: FastifyInstance, regionId: string) { + return await bizEndpoint.inject({ + method: "GET", + url: `/regions/${regionId}/businesses`, + headers: { Authorization: `Bearer ${dummyRegionManagerToken}` } + }); + } + + async function getRequestById(requestId: string, token: string) : Promise { return await editEndpoint.inject({ diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 12ef2a3..1bc717a 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -3,7 +3,6 @@ import {Business, CHUNK_SIZE} from "../../src/endpoints/businesses"; import {EditRequest} from "../../src/endpoints/editRequest"; export class DummyDatalayer implements DataLayer { - businesses: Business[] = []; regions: Region[] = []; editRequests: EditRequest[] = []; @@ -22,9 +21,19 @@ export class DummyDatalayer implements DataLayer { return this.businesses.slice(startIndex, startIndex + CHUNK_SIZE); } + async deleteBusiness(id: string): Promise { + let startIndex = this.businesses.findIndex(biz => biz.id === id); + startIndex = startIndex > 0 ? startIndex : 0; + this.businesses.splice(startIndex, 1); + } + async setBusiness(business:Business): Promise { - let id = `${Math.random()}`; - this.businesses.push({...business, id}); + if(!business.id) { + business.id = `${Math.random()}`; + this.businesses.push({...business}); + } else { + this.businesses[this.businesses.findIndex(b => b.id == business.id)] = business; + } let regionIndex = this.regions.findIndex((r) => r.name == business.regionId); let bizRegion = this.regions[regionIndex] if(!bizRegion.filters) { @@ -39,7 +48,7 @@ export class DummyDatalayer implements DataLayer { } else { bizRegion.filters.industries[industryIndex].count++; } - return {id}; + return {id: business.id}; } async getFilters(regionId: string): Promise { From 3b7859cacc432cc4519db5e5df31007a2d89753e Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 18 Apr 2021 04:42:26 -0230 Subject: [PATCH 60/75] Fixes: Firestore should be working again Admins should get all regions when requesting their own region data Region admins should be able to see all user data Edit Requests should accept the full business instead of just the ID for deletes. --- package.json | 2 +- src/auth0.ts | 8 +- src/database/firestore.ts | 15 +- src/database/productionDataLayer.ts | 4 +- src/endpoints/businesses.ts | 6 +- src/endpoints/docs/editRequestSchemas.ts | 2 +- src/endpoints/editRequest.ts | 12 +- src/endpoints/regions.ts | 6 +- src/endpoints/users.ts | 4 +- tests/editRequests.test.ts | 11 +- tests/regions.test.ts | 9 +- tests/testUtils/dummyData.ts | 6 +- tests/testUtils/testify.ts | 2 +- tests/users.test.ts | 3 + yarn.lock | 430 ++++++----------------- 15 files changed, 153 insertions(+), 367 deletions(-) diff --git a/package.json b/package.json index f4a0f22..2dd2783 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@auth0/auth0-spa-js": "^1.13.6", + "@google-cloud/firestore": "^4.9.9", "@types/node-fetch": "^2.5.8", "body-parser": "^1.19.0", "fastify": "^3.5.1", @@ -42,7 +43,6 @@ "@types/jest": "^26.0.15", "@types/node": "^12.12.67", "baretest": "^2.0.0", - "firebase": "^8.0.2", "jest": "^26.6.3", "ts-jest": "^26.4.4", "typescript": "^4.0.3" diff --git a/src/auth0.ts b/src/auth0.ts index b48200a..2c1c9ab 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -78,7 +78,7 @@ export interface UserInfoPatch { username?: string } -async function getUserRole(userId: string) { +async function getUserRole(userId: string) : Promise<{role:string}> { let app_metadata = (await getUserById(userId)).app_metadata; return {role: app_metadata.role}; } @@ -150,16 +150,16 @@ export async function getUserInfo(authHeader: string) { } } -export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<{userAppId: string, admin: boolean}>; +export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<{userAppId: string, admin: boolean, role: string}>; export async function verifyJwt(request: FastifyRequest) { let authHeader = !request.headers.authorization ? "" : request.headers.authorization if(!authHeader) { - return {userAppId: "", admin: false}; + return {userAppId: "", admin: false, role: ""}; } else { let {userId} = await getUserInfo(authHeader); let {role} = await getUserRole(userId); let admin: boolean = role === "admin" let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId; - return {userAppId, admin}; + return {userAppId, admin, role}; } } diff --git a/src/database/firestore.ts b/src/database/firestore.ts index ee4a346..45d73fd 100644 --- a/src/database/firestore.ts +++ b/src/database/firestore.ts @@ -1,14 +1,3 @@ -import firebase from "firebase"; +import {Firestore} from "@google-cloud/firestore"; -let firebaseConfig = { - apiKey: "AIzaSyA6GO5fwNJrBklVKvYQ9LJSIkznxB6oy4M", - authDomain: "ranlab-mvp.firebaseapp.com", - databaseURL: "https://ranlab-mvp.firebaseio.com", - projectId: "ranlab-mvp", - storageBucket: "ranlab-mvp.appspot.com", - messagingSenderId: "234026995986", - appId: "1:234026995986:web:ed82d23d535394775563cb" -}; - -export const app = firebase.initializeApp(firebaseConfig); -export const productionFirestore = app.firestore(); +export const productionFirestore = new Firestore(); diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index ef9a55b..fd5653f 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -32,9 +32,9 @@ export interface DataLayer { deleteBusiness(id: string): Promise; getFilters(regionId: string) : Promise; getRegionsManagedBy(managerId: string) : Promise; + getAllRegions(): Promise; setRegion(region: Region): Promise; deleteRegion(regionId: string): Promise; - getAllRegions(): Promise; getEditRequestById(id: string): Promise; getAllEditRequests(pageSize: number, afterId?: string): Promise; getEditRequestsForRegion(regionId: string, pageSize: number, afterId?: string): Promise; @@ -130,8 +130,6 @@ export class ProductionDataLayer implements DataLayer { ); } - - async getFilters(region: string) : Promise{ let regionData = (await this.firestore.collection("regions").doc(region).get()).data(); regionData = !!regionData ? regionData : {}; diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 8e1d38d..9d4e238 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -1,7 +1,6 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; -import firebase from "firebase"; -import GeoPoint = firebase.firestore.GeoPoint; +import {GeoPoint} from "@google-cloud/firestore"; import {createBizSchema, exportBusinessesSchema, getBizSchema, updateBizSchema} from "./docs/businessesSchemas"; import {isRegionManager} from "../utils"; import {Auth0JwtVerifier} from "../auth0"; @@ -56,7 +55,7 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa app.get( '/regions/:regionId/businesses', {schema: getBizSchema}, - async (request, reply) => { + async (request , reply) => { let {userAppId, admin} = await verifyJwt(request) if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { @@ -78,7 +77,6 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa } ); - app.post( '/regions/:regionId/businesses', {schema: createBizSchema}, diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index af6db4d..54c40ce 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -23,7 +23,7 @@ import { businessSchema} from './businessesSchemas'; }, deletes: { type: 'array', - items: {type: 'string'} + items: businessSchema } } } diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 909848a..3c275c1 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -22,7 +22,7 @@ export interface EditRequest { status?: string, adds?: Business[], updates?: BusinessUpdate[], - deletes?: string[] + deletes?: BusinessUpdate[] } interface CreateEditRequest extends AuthenticatedRequestByRegionId { @@ -207,8 +207,8 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v updatedBusinesses.push({...biz, ...update}); } } - for (const delId of editRequest?.deletes || []) { - let biz = await dataLayer.getBusinessById(delId); + for (const deleted of editRequest?.deletes || []) { + let biz = await dataLayer.getBusinessById(deleted.id); if(!!biz) { deletedBusinesses.push(biz); } @@ -266,10 +266,10 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v updatedBusinesses.push(updatedBiz); } } - for (const delId of editRequest?.deletes || []) { - let biz = await dataLayer.getBusinessById(delId); + for (const deleted of editRequest?.deletes || []) { + let biz = await dataLayer.getBusinessById(deleted.id); if (!!biz) { - editPromises.push(dataLayer.deleteBusiness(delId)); + editPromises.push(dataLayer.deleteBusiness(deleted.id)); deletedBusinesses.push(biz); } } diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index eb10bde..0854f82 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -54,8 +54,10 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : date: Date.now(), regions: [] }; - if (admin || userAppId == request.params.managerId) { - response.regions.push(...(await dataLayer.getRegionsManagedBy(request.params.managerId))); + if (admin && userAppId == request.params.managerId) { + response.regions.push(...(await dataLayer.getAllRegions())); + } else if (admin || userAppId == request.params.managerId) { + response.regions.push(...(await dataLayer.getRegionsManagedBy(request.params.managerId))); } return JSON.stringify(response); } diff --git a/src/endpoints/users.ts b/src/endpoints/users.ts index 6c4e20b..1cd18c9 100644 --- a/src/endpoints/users.ts +++ b/src/endpoints/users.ts @@ -19,7 +19,7 @@ export default function createUsersEndpoint(app: FastifyInstance, verifyJwt: Aut '/users/:id', {schema: getUserInfoRequestSchema}, async (request, reply) => { - let {userAppId, admin} = await verifyJwt(request); + let {userAppId, admin, role} = await verifyJwt(request); if (!userAppId) { reply.unauthorized("Only authenticated users can access this information"); return; @@ -29,7 +29,7 @@ export default function createUsersEndpoint(app: FastifyInstance, verifyJwt: Aut date: Date.now(), userInfo: {} }; - if (admin || userAppId == request.params.id) { + if (admin || role == "region") { response.userInfo = await getUserById(userAppId); } return JSON.stringify(response); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index 42fc5e9..c8084a2 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -247,7 +247,10 @@ describe("Edit Request unit tests", () => { name: "UpdatedName", industry: "UpdatedIndustry" }], - deletes: [deletedBizId] + deletes: [{ + id: deletedBizId, + ...deletedBiz + }] }; let submitResponse = await submitEditRequest(request, DummyRegion.name, dummyRegionManagerToken); @@ -310,8 +313,6 @@ describe("Edit Request unit tests", () => { }); } - - async function getRequestById(requestId: string, token: string) : Promise { return await editEndpoint.inject({ method: "GET", @@ -326,9 +327,9 @@ describe("Edit Request unit tests", () => { url: `/region/${regionId}/edits`, payload: request, headers: {} - } + }; if(!!token) { - postOptions.headers = {authorization: `Bearer ${token}`} + postOptions.headers = {authorization: `Bearer ${token}`}; } return await editEndpoint.inject(postOptions); } diff --git a/tests/regions.test.ts b/tests/regions.test.ts index 53b9032..d36baa8 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -9,7 +9,7 @@ import { DummyRegion, dummyRegionManagerToken, getRegionsByDummyManager, - dummyTokenVerifier + dummyTokenVerifier, dummyAdminId } from "./testUtils/dummyData"; import {Region} from "../src/database/productionDataLayer"; import {FastifyInstance} from "fastify"; @@ -42,6 +42,13 @@ describe("Region Endpoint Tests", () => { expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions.filter(r => r.manager == region.manager)])); } + const response = await app.inject({ + method: 'GET', + headers: {authorization: `Bearer ${dummyAdminToken}`}, + url: `/regions/manager/${dummyAdminId}` + }); + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions])); await app.close(); done(); }); diff --git a/tests/testUtils/dummyData.ts b/tests/testUtils/dummyData.ts index db543ca..2d09353 100644 --- a/tests/testUtils/dummyData.ts +++ b/tests/testUtils/dummyData.ts @@ -32,12 +32,12 @@ export const DummyBizUpdate: BusinessUpdate = { export async function dummyTokenVerifier (req: FastifyRequest) { if(!req.headers.authorization || !req.headers.authorization.split("Bearer")[1].trim()) { - return {userAppId: "", admin: false}; + return {userAppId: "", admin: false, role: ""}; } if(req.headers.authorization.indexOf(dummyAdminToken) > 0) { - return {userAppId: dummyAdminId, admin: true}; + return {userAppId: dummyAdminId, admin: true, role: "admin"}; } else if (req.headers.authorization?.indexOf(dummyRegionManagerToken) > 0) { - return {userAppId: DummyRegion.manager, admin: false}; + return {userAppId: DummyRegion.manager, admin: false, role: "region"}; } else { throw new Error("Unrecognized token"); } diff --git a/tests/testUtils/testify.ts b/tests/testUtils/testify.ts index 83087ec..dee7f34 100644 --- a/tests/testUtils/testify.ts +++ b/tests/testUtils/testify.ts @@ -61,7 +61,7 @@ export function getMockToken(payload: {userAppId: string, admin: boolean }) { } export function getTestJwtVerifier(userAppId: string, admin: boolean) { - return async(_: FastifyRequest) => ({userAppId, admin}); + return async(_: FastifyRequest) => ({userAppId, admin, role: admin ? "admin" : "region" }); } export const testify = () => { diff --git a/tests/users.test.ts b/tests/users.test.ts index 8acab80..cdc503b 100644 --- a/tests/users.test.ts +++ b/tests/users.test.ts @@ -6,6 +6,7 @@ describe("Auth0 user endpoint tests", () => { let userAccessToken: string; let adminAccessToken: string; it("Can get all users as regular or admin user", async (done) => { + jest.setTimeout(15000); async function getAllUsers(token:string) { return await userApp.inject({ method: "GET", @@ -36,6 +37,7 @@ describe("Auth0 user endpoint tests", () => { }); it("Can get single user by ID as regular or admin user", async (done) => { + jest.setTimeout(15000); async function getUserById(id: string, token:string) { return await userApp.inject({ method: "GET", @@ -64,6 +66,7 @@ describe("Auth0 user endpoint tests", () => { }); it("Can update a user as an admin but not a regular user", async (done) => { + jest.setTimeout(15000); async function updateUser(id: string, updated: string, token:string) { return await userApp.inject({ method: "POST", diff --git a/yarn.lock b/yarn.lock index 5b80099..ca919b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -292,243 +292,29 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@firebase/analytics-types@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz" - integrity sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA== - -"@firebase/analytics@0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.6.2.tgz" - integrity sha512-4Ceov+rPfOEPIdbjlpTim/wbcUUneIesHag4UOzvmFsRRXqbxLwQpyZQWEbTSriUeU8uTKj9yOW32hsskV9Klg== - dependencies: - "@firebase/analytics-types" "0.4.0" - "@firebase/component" "0.1.21" - "@firebase/installations" "0.4.19" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.4" - tslib "^1.11.1" - -"@firebase/app-types@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.1.tgz" - integrity sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg== - -"@firebase/app@0.6.13": - version "0.6.13" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.6.13.tgz" - integrity sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ== - dependencies: - "@firebase/app-types" "0.6.1" - "@firebase/component" "0.1.21" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.4" - dom-storage "2.1.0" - tslib "^1.11.1" - xmlhttprequest "1.8.0" - -"@firebase/auth-interop-types@0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz" - integrity sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw== - -"@firebase/auth-types@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.1.tgz" - integrity sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw== - -"@firebase/auth@0.15.2": - version "0.15.2" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.2.tgz" - integrity sha512-2n32PBi6x9jVhc0E/ewKLUCYYTzFEXL4PNkvrrlGKbzeTBEkkyzfgUX7OV9UF5wUOG+gurtUthuur1zspZ/9hg== - dependencies: - "@firebase/auth-types" "0.10.1" - -"@firebase/component@0.1.21": - version "0.1.21" - resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.1.21.tgz" - integrity sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg== - dependencies: - "@firebase/util" "0.3.4" - tslib "^1.11.1" - -"@firebase/database-types@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.6.0.tgz" - integrity sha512-ljpU7/uboCGqFSe9CNgwd3+Xu5N8YCunzfPpeueuj2vjnmmypUi4QWxgC3UKtGbuv1q+crjeudZGLxnUoO0h7w== - dependencies: - "@firebase/app-types" "0.6.1" - -"@firebase/database@0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.7.1.tgz" - integrity sha512-8j3KwksaYMSbIsEjOIarZD3vj4jGJjIlLGIAiO/4P4XyOtrlnxIiH7G0UdIZlcvKU4Gsgg0nthT2+EapROmHWA== - dependencies: - "@firebase/auth-interop-types" "0.1.5" - "@firebase/component" "0.1.21" - "@firebase/database-types" "0.6.0" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.4" - faye-websocket "0.11.3" - tslib "^1.11.1" - -"@firebase/firestore-types@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.0.0.tgz" - integrity sha512-ZGb7p1SSQJP0Z+kc9GAUi+Fx5rJatFddBrS1ikkayW+QHfSIz0omU23OgSHcBGTxe8dJCeKiKA2Yf+tkDKO/LA== - -"@firebase/firestore@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.0.2.tgz" - integrity sha512-6kO/vWUmTOANA/ql+i16DFMc63gamU76Nycyt7k0r8QfcdXu93Cwizw4ff4DNMnpnkAJkTk36fPAxBxEvBXkzw== - dependencies: - "@firebase/component" "0.1.21" - "@firebase/firestore-types" "2.0.0" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.4" - "@firebase/webchannel-wrapper" "0.4.0" - "@grpc/grpc-js" "^1.0.0" - "@grpc/proto-loader" "^0.5.0" - node-fetch "2.6.1" - tslib "^1.11.1" - -"@firebase/functions-types@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.4.0.tgz" - integrity sha512-3KElyO3887HNxtxNF1ytGFrNmqD+hheqjwmT3sI09FaDCuaxGbOnsXAXH2eQ049XRXw9YQpHMgYws/aUNgXVyQ== - -"@firebase/functions@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.6.1.tgz" - integrity sha512-xNCAY3cLlVWE8Azf+/84OjnaXMoyUstJ3vwVRG0ie22QhsdQuPa1tXTiPX4Tmm+Hbbd/Aw0A/7dkEnuW+zYzaQ== - dependencies: - "@firebase/component" "0.1.21" - "@firebase/functions-types" "0.4.0" - "@firebase/messaging-types" "0.5.0" - node-fetch "2.6.1" - tslib "^1.11.1" - -"@firebase/installations-types@0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.3.4.tgz" - integrity sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q== - -"@firebase/installations@0.4.19": - version "0.4.19" - resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.4.19.tgz" - integrity sha512-QqAQzosKVVqIx7oMt5ujF4NsIXgtlTnej4JXGJ8sQQuJoMnt3T+PFQRHbr7uOfVaBiHYhEaXCcmmhfKUHwKftw== - dependencies: - "@firebase/component" "0.1.21" - "@firebase/installations-types" "0.3.4" - "@firebase/util" "0.3.4" - idb "3.0.2" - tslib "^1.11.1" - -"@firebase/logger@0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.6.tgz" - integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== - -"@firebase/messaging-types@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.5.0.tgz" - integrity sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg== - -"@firebase/messaging@0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.7.3.tgz" - integrity sha512-63nOP2SmQJrj9jrhV3K96L5MRKS6AqmFVLX1XbGk6K6lz38ZC4LIoCcHxzUBXY7fCAuZvNmh/YB3pE8B2mTs8A== - dependencies: - "@firebase/component" "0.1.21" - "@firebase/installations" "0.4.19" - "@firebase/messaging-types" "0.5.0" - "@firebase/util" "0.3.4" - idb "3.0.2" - tslib "^1.11.1" - -"@firebase/performance-types@0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.13.tgz" - integrity sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA== - -"@firebase/performance@0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.4.4.tgz" - integrity sha512-CY/fzz7qGQ9hUkvOow22MeJhayHSjXmI4+0AqcxaUC4CWk4oQubyIC4pk62aH+yCwZNNeC7JJUEDbtqI/0rGkQ== - dependencies: - "@firebase/component" "0.1.21" - "@firebase/installations" "0.4.19" - "@firebase/logger" "0.2.6" - "@firebase/performance-types" "0.0.13" - "@firebase/util" "0.3.4" - tslib "^1.11.1" - -"@firebase/polyfill@0.3.36": - version "0.3.36" - resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.36.tgz" - integrity sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg== - dependencies: - core-js "3.6.5" - promise-polyfill "8.1.3" - whatwg-fetch "2.0.4" - -"@firebase/remote-config-types@0.1.9": - version "0.1.9" - resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz" - integrity sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA== - -"@firebase/remote-config@0.1.30": - version "0.1.30" - resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.30.tgz" - integrity sha512-LAfLDcp1AN0V/7AkxBuTKy+Qnq9fKYKxbA5clrXRNVzJbTVnF5eFGsaUOlkes0ESG6lbqKy5ZcDgdl73zBIhAA== - dependencies: - "@firebase/component" "0.1.21" - "@firebase/installations" "0.4.19" - "@firebase/logger" "0.2.6" - "@firebase/remote-config-types" "0.1.9" - "@firebase/util" "0.3.4" - tslib "^1.11.1" - -"@firebase/storage-types@0.3.13": - version "0.3.13" - resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.13.tgz" - integrity sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog== - -"@firebase/storage@0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.4.2.tgz" - integrity sha512-87CrvKrf8kijVekRBmUs8htsNz7N5X/pDhv3BvJBqw8K65GsUolpyjx0f4QJRkCRUYmh3MSkpa5P08lpVbC6nQ== +"@google-cloud/firestore@^4.9.9": + version "4.9.9" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-4.9.9.tgz#1bcf66a4f5a6c54c938489eb08f4ac97908835e8" + integrity sha512-M/Ts8oyJP1Ig8uMrUr/pNGGwBDPkB9ALqmH182T5aY5HW00yzLcHiDcKlfk9PoBINakAMk15GJZ1ov+I17HcnQ== dependencies: - "@firebase/component" "0.1.21" - "@firebase/storage-types" "0.3.13" - "@firebase/util" "0.3.4" - tslib "^1.11.1" - -"@firebase/util@0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.3.4.tgz" - integrity sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ== - dependencies: - tslib "^1.11.1" - -"@firebase/webchannel-wrapper@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz" - integrity sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ== + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^2.9.2" + protobufjs "^6.8.6" -"@grpc/grpc-js@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.0.tgz" - integrity sha512-09H50V7rmz0gFrGz6IbP49z9A8+2p4yZYcNDEb7bytr90vWn52VBQE1a+LMBlrucmNN0wSsiCr3TJx+dStHTng== +"@grpc/grpc-js@~1.2.0": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.12.tgz#0153f27512acf69184bb52c0a1035ca91d6c14b0" + integrity sha512-+gPCklP1eqIgrNPyzddYQdt9+GvZqPlLpIjIo+TveE+gbtp74VV1A2ju8ExeO8ma8f7MbpaGZx/KJPYVWL9eDw== dependencies: - "@types/node" "^12.12.47" + "@types/node" ">=12.12.47" google-auth-library "^6.1.1" semver "^6.2.0" -"@grpc/proto-loader@^0.5.0": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.5.tgz" - integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== +"@grpc/proto-loader@^0.5.1": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.6.tgz#1dea4b8a6412b05e2d58514d507137b63a52a98d" + integrity sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ== dependencies: lodash.camelcase "^4.3.0" protobufjs "^6.8.6" @@ -915,7 +701,7 @@ dependencies: "@types/node" "*" -"@types/long@^4.0.1": +"@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== @@ -938,10 +724,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz" integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== -"@types/node@^12.12.47": - version "12.19.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz" - integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== +"@types/node@>=12.12.47": + version "14.14.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615" + integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g== "@types/node@^12.12.67": version "12.12.67" @@ -1581,11 +1367,6 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@3.6.5: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== - core-js@^3.8.0: version "3.8.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" @@ -1735,11 +1516,6 @@ diff-sequences@^26.6.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== -dom-storage@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz" - integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q== - domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz" @@ -1747,6 +1523,16 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +duplexify@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" + integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" @@ -1782,7 +1568,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2166,13 +1952,6 @@ fastseries@^1.7.0: reusify "^1.0.0" xtend "^4.0.0" -faye-websocket@0.11.3: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz" @@ -2214,26 +1993,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -firebase@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.0.2.tgz" - integrity sha512-tPtXQ8sifo82f7bOxYcR/yEdJ4IbL4/fpQrophRHFAaYCsYGp2Q/c1zz0voX9cLap8MH2uwh5LIVBqZ8nyT5ZQ== - dependencies: - "@firebase/analytics" "0.6.2" - "@firebase/app" "0.6.13" - "@firebase/app-types" "0.6.1" - "@firebase/auth" "0.15.2" - "@firebase/database" "0.7.1" - "@firebase/firestore" "2.0.2" - "@firebase/functions" "0.6.1" - "@firebase/installations" "0.4.19" - "@firebase/messaging" "0.7.3" - "@firebase/performance" "0.4.4" - "@firebase/polyfill" "0.3.36" - "@firebase/remote-config" "0.1.30" - "@firebase/storage" "0.4.2" - "@firebase/util" "0.3.4" - flatstr@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz" @@ -2304,6 +2063,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + gaxios@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.0.1.tgz" @@ -2396,6 +2160,38 @@ google-auth-library@^6.1.1: jws "^4.0.0" lru-cache "^6.0.0" +google-auth-library@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.0.4.tgz#610cb010de71435dca47dfbe8dc7fbff23055d2c" + integrity sha512-o8irYyeijEiecTXeoEe8UKNEzV1X+uhR4b2oNdapDMZixypp0J+eHimGOyx5Joa3UAeokGngdtDLXtq9vDqG2Q== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-gax@^2.9.2: + version "2.11.2" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-2.11.2.tgz#9ef7773b94aaa61c4588fb2408d62e8444995026" + integrity sha512-PNqXv7Oi5XBMgoMWVxLZHUidfMv7cPHrDSDXqLyEd6kY6pqFnVKC8jt2T1df4JPSc2+VLPdeo6L7X9mbdQG8Xw== + dependencies: + "@grpc/grpc-js" "~1.2.0" + "@grpc/proto-loader" "^0.5.1" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^7.0.2" + is-stream-ended "^0.1.4" + node-fetch "^2.6.1" + protobufjs "^6.10.2" + retry-request "^4.0.0" + google-p12-pem@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz" @@ -2534,11 +2330,6 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-parser-js@>=0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz" - integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== - http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" @@ -2577,11 +2368,6 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -idb@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz" - integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw== - import-local@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz" @@ -2744,6 +2530,11 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-stream-ended@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" + integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz" @@ -3730,7 +3521,7 @@ node-cache@^5.0.1: dependencies: clone "2.x" -node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.1: +node-fetch@^2.3.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -4004,11 +3795,6 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" -promise-polyfill@8.1.3: - version "8.1.3" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz" - integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== - promise-polyfill@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" @@ -4022,6 +3808,25 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +protobufjs@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b" + integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" "^13.7.0" + long "^4.0.0" + protobufjs@^6.8.6: version "6.10.1" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz" @@ -4136,7 +3941,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -4255,6 +4060,13 @@ ret@~0.2.0: resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz" integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== +retry-request@^4.0.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" + integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== + dependencies: + debug "^4.1.1" + reusify@^1.0.0, reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz" @@ -4277,7 +4089,7 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4612,6 +4424,11 @@ steed@^1.1.3: fastseries "^1.7.0" reusify "^1.0.0" +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz" @@ -4805,11 +4622,6 @@ ts-jest@^26.4.4: semver "7.x" yargs-parser "20.x" -tslib@^1.11.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz" @@ -4991,20 +4803,6 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -websocket-driver@>=0.5.1: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" @@ -5012,11 +4810,6 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" @@ -5094,11 +4887,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmlhttprequest@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz" - integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= - xtend@^4.0.0, xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz" From ea6d8f0a9775bdbfdb9a737f82aba3ce639e794b Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sun, 18 Apr 2021 04:53:47 -0230 Subject: [PATCH 61/75] Fixing compile errors --- src/database/productionDataLayer.ts | 31 ++++++++++++++++++----------- tests/testUtils/testFirestore.ts | 5 ++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index fd5653f..fd43ff3 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,7 +1,14 @@ import {Business, CHUNK_SIZE} from "../endpoints/businesses"; import {EditRequest} from "../endpoints/editRequest"; -import firebase from "firebase"; -import Timestamp = firebase.firestore.Timestamp; +import { + DocumentData, DocumentSnapshot, + FieldValue, + Firestore, + Query, + QueryDocumentSnapshot, + Timestamp, + Transaction +} from "@google-cloud/firestore"; export interface IdObject { id: string @@ -46,8 +53,8 @@ export interface DataLayer { } export class ProductionDataLayer implements DataLayer { - firestore: firebase.firestore.Firestore; - constructor(firestore: firebase.firestore.Firestore) { + firestore: Firestore; + constructor(firestore: Firestore) { this.firestore = firestore; } @@ -162,8 +169,8 @@ export class ProductionDataLayer implements DataLayer { let doc = this.firestore.collection("editRequests").doc(); await doc.set({ ...editRequest, - dateSubmitted: firebase.firestore.FieldValue.serverTimestamp(), - dateUpdated: firebase.firestore.FieldValue.serverTimestamp() + dateSubmitted: FieldValue.serverTimestamp(), + dateUpdated: FieldValue.serverTimestamp() }); return {id : doc.id}; } @@ -179,7 +186,7 @@ export class ProductionDataLayer implements DataLayer { let updatedRequestData = { ...this.convertToEditRequest(id, requestData), ...body, - dateUpdated: firebase.firestore.FieldValue.serverTimestamp() + dateUpdated: FieldValue.serverTimestamp() }; await this.firestore.collection("editRequests").doc(id).update(updatedRequestData); let updatedRequest = await this.getEditRequestById(id); @@ -210,7 +217,7 @@ export class ProductionDataLayer implements DataLayer { return this.getPaginatedEditRequests(query, pageSize, afterId); } - private async getPaginatedEditRequests(query: firebase.firestore.Query, pageSize: number, afterId: string | undefined) { + private async getPaginatedEditRequests(query: Query, pageSize: number, afterId: string | undefined) { let requests: EditRequest[] = []; query = query.orderBy("dateSubmitted", 'desc').limit(pageSize); if (!!afterId) { @@ -230,7 +237,7 @@ export class ProductionDataLayer implements DataLayer { return requests; } - convertToEditRequest(id: string, documentData: firebase.firestore.DocumentData | undefined) : EditRequest | null { + convertToEditRequest(id: string, documentData: DocumentData | undefined) : EditRequest | null { if(!!documentData) { let request = documentData; request.dateSubmitted = ((documentData.dateSubmitted)).toDate(); @@ -242,7 +249,7 @@ export class ProductionDataLayer implements DataLayer { } } - convertToBusinesses(docs: Array>) : Business[] { + convertToBusinesses(docs: Array) : Business[] { let businesses = []; for(let doc of docs) { let biz : Business = { @@ -263,7 +270,7 @@ export class ProductionDataLayer implements DataLayer { - private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: firebase.firestore.Transaction) { + private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: Transaction) { let regionRef = this.firestore.collection("regions").doc(regionId); let regionDoc = await transaction.get(regionRef); if (!!regionDoc) { @@ -314,7 +321,7 @@ export class ProductionDataLayer implements DataLayer { return {years, industries}; } - private static getCurrentFilters(regionDoc: firebase.firestore.DocumentSnapshot) : RegionFilters{ + private static getCurrentFilters(regionDoc: DocumentSnapshot) : RegionFilters{ let regionData = !!regionDoc && !!regionDoc.data() ? regionDoc.data() : {filters: {}}; if(!!regionData) { return !!regionData.filters ? regionData.filters : {}; diff --git a/tests/testUtils/testFirestore.ts b/tests/testUtils/testFirestore.ts index d27a5ff..1c4ee94 100644 --- a/tests/testUtils/testFirestore.ts +++ b/tests/testUtils/testFirestore.ts @@ -1,4 +1,4 @@ -import firebase from "firebase"; +import {Firestore} from "@google-cloud/firestore"; // Because of some fun with quotas, I now have two possible test firestore targets, whee. /* @@ -21,5 +21,4 @@ var firebaseConfig = { measurementId: "G-180MVEJXN2" }; -export const testApp = firebase.initializeApp(firebaseConfig); -export const testFirestore = testApp.firestore(); +export const testFirestore = new Firestore(firebaseConfig); From a96f9142459296daa4fe02a247fc0fee7254295e Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 29 Apr 2021 11:55:41 -0230 Subject: [PATCH 62/75] Adding global list of industries in addition to regional ones --- src/database/productionDataLayer.ts | 38 +++++++++++---- src/endpoints/docs/filterSchemas.ts | 45 +++++++++++++++++- src/endpoints/filters.ts | 73 ++++++++++++++++++++++++----- tests/dataLayer.test.ts | 29 +++++++++++- tests/filters.test.ts | 72 +++++++++++++++++++++++----- tests/testUtils/testDataLayer.ts | 23 +++++++-- 6 files changed, 241 insertions(+), 39 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index fd43ff3..7f6f407 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -37,7 +37,7 @@ export interface DataLayer { getBusinessesByRegion(region: string): Promise; setBusiness(business: Business) : Promise; deleteBusiness(id: string): Promise; - getFilters(regionId: string) : Promise; + getFilters(regionId?: string) : Promise; getRegionsManagedBy(managerId: string) : Promise; getAllRegions(): Promise; setRegion(region: Region): Promise; @@ -49,7 +49,8 @@ export interface DataLayer { getEditRequestsByUser(userAppId: string, pageSize: number, afterId?: string): Promise; createEditRequest(add: EditRequest): Promise; updateEditRequest(body: EditRequest): Promise; - + addIndustries(industries: string[]): Promise; + deleteIndustries(industries: string[]) : Promise; } export class ProductionDataLayer implements DataLayer { @@ -137,10 +138,17 @@ export class ProductionDataLayer implements DataLayer { ); } - async getFilters(region: string) : Promise{ - let regionData = (await this.firestore.collection("regions").doc(region).get()).data(); - regionData = !!regionData ? regionData : {}; - return regionData.filters; + async getFilters(region?: string) : Promise{ + if(!!region) { + let regionData = (await this.firestore.collection("regions").doc(region).get()).data(); + regionData = !!regionData ? regionData : {}; + return regionData.filters; + } else { + let industries: string[] = []; + let industryData = (await this.firestore.collection("industries").get()).docs; + industryData.forEach(i => industries.push(i.id)); + return {industries}; + } } async getAllRegions() : Promise { @@ -217,6 +225,21 @@ export class ProductionDataLayer implements DataLayer { return this.getPaginatedEditRequests(query, pageSize, afterId); } + async addIndustries(industries: string[]): Promise { + industries.forEach( + i => this.firestore.collection("industries").doc(i).set({active: true}) + ); + } + + async deleteIndustries(industries: string[]) : Promise{ + let promises: Promise[] = [] + for(let i = 0; i < industries.length; i++) { + promises.push(this.firestore.collection("industries").doc(industries[i]).delete()); + } + return Promise.all(promises); + } + + private async getPaginatedEditRequests(query: Query, pageSize: number, afterId: string | undefined) { let requests: EditRequest[] = []; query = query.orderBy("dateSubmitted", 'desc').limit(pageSize); @@ -268,8 +291,6 @@ export class ProductionDataLayer implements DataLayer { return businesses; } - - private async updateRegionFilters(regionId: string, filterUpdate: RegionFilters, transaction: Transaction) { let regionRef = this.firestore.collection("regions").doc(regionId); let regionDoc = await transaction.get(regionRef); @@ -329,5 +350,6 @@ export class ProductionDataLayer implements DataLayer { return {}; } } + } diff --git a/src/endpoints/docs/filterSchemas.ts b/src/endpoints/docs/filterSchemas.ts index 00522cf..b223fcd 100644 --- a/src/endpoints/docs/filterSchemas.ts +++ b/src/endpoints/docs/filterSchemas.ts @@ -15,6 +15,18 @@ export const filtersSchema = { } } +let industryListSchema = { + type: 'object', + properties: { + industries: { + type: 'array', + items: { + type: 'string' + } + } + } +}; + export const getFilterSchema = { description: 'Endpoint for interacting directly with filters for a particular region', params: byRegionIdSchema, @@ -32,7 +44,7 @@ export const getFilterSchema = { }; export const getAllIndustriesSchema = { - description: 'Admin-only endpoint allowing access to the global list of industries', + description: 'Endpoint returning the global list of industries', security: [], response: { 200: { @@ -50,3 +62,34 @@ export const getAllIndustriesSchema = { } } }; + +export const addIndustriesSchema = { + description: 'Adds one or more industries to the global list of industries that can be filtered on', + body: industryListSchema, + response: { + 201: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + id: {type: 'string'} + } + } + } +}; + +export const deleteIndustriesSchema = { + description: 'Deletes one or more industries from the global list of industries', + body: industryListSchema, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + id: {type: 'string'} + } + } + } +}; + diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index 99ce0a3..7193a7c 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -1,7 +1,13 @@ import type {FastifyInstance, RequestGenericInterface} from 'fastify'; import {DataLayer, Filters} from "../database/productionDataLayer"; -import {getAllIndustriesSchema, getFilterSchema} from "./docs/filterSchemas"; +import { + addIndustriesSchema, + deleteIndustriesSchema, + getAllIndustriesSchema, + getFilterSchema +} from "./docs/filterSchemas"; import {Auth0JwtVerifier} from "../auth0"; +import {AuthenticatedRequest} from "./endpointUtils"; interface GetFiltersRequest extends RequestGenericInterface { Params: { @@ -9,6 +15,12 @@ interface GetFiltersRequest extends RequestGenericInterface { } } +interface IndustriesRequest extends AuthenticatedRequest { + Body: { + industries: string[] + } +} + export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { app.get('/regions/:regionId/filters', {schema: getFilterSchema}, async (request, reply) => { @@ -30,9 +42,9 @@ export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer app.get("/filters/industries", {schema: getAllIndustriesSchema}, async (request, reply) =>{ - let {admin} = await verifyJwt(request); - if(!admin) { - reply.unauthorized("Only admins have access to this data"); + let {userAppId} = await verifyJwt(request); + if(!userAppId) { + reply.unauthorized("Must be logged in to access this data"); return; } else { let response = { @@ -41,22 +53,59 @@ export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer industries: [] }; let regions = await dataLayer.getAllRegions(); + let industries: string[] = []; regions.forEach((r) => { if (!!r.filters && !!r.filters.industries) { - r.filters.industries.forEach((f_i) => { - if(!!f_i.industry) { - if (!response.industries.find((r_i) => r_i === f_i.industry)) { - response.industries.push(f_i.industry); - } - } - }) + industries.push(...r.filters.industries.map(i => i.industry)); } }); + let globalIndustries = (await dataLayer.getFilters()).industries ?? []; + industries.push(...globalIndustries); + // Get just unique values + response.industries = Array.from(new Set(industries)); return JSON.stringify(response); } } ) - return app; + app.post( + "/filters/industries", + {schema: addIndustriesSchema}, + async (request, reply) => { + let {userAppId} = await verifyJwt(request); + if (!userAppId) { + reply.unauthorized("Must be logged in to access this data"); + return; + } else { + let response = { + status: "ok", + date: Date.now() + }; + + await dataLayer.addIndustries(request.body.industries); + return JSON.stringify(response); + } + }); + + app.delete( + "/filters/industries", + {schema: deleteIndustriesSchema}, + async (request, reply) => { + let {userAppId} = await verifyJwt(request); + if (!userAppId) { + reply.unauthorized("Must be logged in to perform this operation"); + return; + } else { + let response = { + status: "ok", + date: Date.now() + }; + + await dataLayer.deleteIndustries(request.body.industries); + return JSON.stringify(response); + } + }); + + return app; } diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 32c30e5..a4c64e3 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -285,6 +285,32 @@ describe("Production Data Layer Integration Tests", () => { done(); }); + describe("Filter data layer tests", () => { + beforeEach(async (done) => { + let industryRecords = (await productionDataLayer.firestore.collection("industries").get()).docs; + for(let i = 0; i < industryRecords.length; i++) { + await industryRecords[i].ref.delete(); + } + done(); + }); + + it("Creates and deletes industries", async(done) => { + const TEST_INDUSTRY = 'testIndustry'; + await productionDataLayer.addIndustries([TEST_INDUSTRY]); + let {industries: industriesAfterCreate} = await productionDataLayer.getFilters(); + expect(industriesAfterCreate).toStrictEqual( + expect.arrayContaining([TEST_INDUSTRY]) + ); + + await productionDataLayer.deleteIndustries([TEST_INDUSTRY]); + let {industries: industriesAfterDelete} = await productionDataLayer.getFilters(); + expect(industriesAfterDelete).toStrictEqual([]); + + done(); + }) + + }) + describe("Long running test", ()=> { afterEach(async(done) => { (await testFirestore.collection("businesses").get()).docs.forEach((biz) => biz.ref.delete()); @@ -327,6 +353,5 @@ describe("Production Data Layer Integration Tests", () => { done(); }); - - }) + }); }); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index cbaac3b..860b9b5 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -11,9 +11,12 @@ import { } from "./testUtils/dummyData"; import createRegionsEndpoint from "../src/endpoints/regions"; import {setupAuth0TestEnv, testify} from "./testUtils/testify"; +import {FastifyInstance} from "fastify"; describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; + const CREATED_INDUSTRY = 'Created Industry'; + let DEFAULT_INDUSTRIES = [DummyBiz.industry, `Not ${DummyBiz.industry}`, CREATED_INDUSTRY]; beforeAll(async (done) => { setupAuth0TestEnv(); @@ -21,6 +24,7 @@ describe("Filter Endpoint Tests", () => { const server = testify(); const regionsApp = createRegionsEndpoint(server, testDataLayer, dummyTokenVerifier); const bizApp = createBusinessesEndpoint(server, testDataLayer, dummyTokenVerifier); + const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); await createDummyRegion(regionsApp); let notDummyRegion = {...DummyRegion}; @@ -40,10 +44,11 @@ describe("Filter Endpoint Tests", () => { industry: `Not ${DummyBiz.industry}`, year_added: DummyBiz.year_added + 1 }); + await createGlobalIndustry(filterApp, CREATED_INDUSTRY); done(); }); - it('Returns only the region-specific filter data', async (done) => { + it('Region-specific filter data is correct', async (done) => { const server = testify(); const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); const filterResponse = await filterApp.inject({ @@ -64,35 +69,78 @@ describe("Filter Endpoint Tests", () => { done(); }); - it("Gets all industries for admin users", async (done) => { + it("Global list contains all industries for admin and non-admin users", async (done) => { const server = testify(); const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); - const industriesResponse = await filterApp.inject({ + const adminResponse = await filterApp.inject({ method: 'GET', url: `/filters/industries`, headers: {authorization: `Bearer ${dummyAdminToken}`} }); - expect(industriesResponse.statusCode).toBe(200); - expect(JSON.parse(industriesResponse.payload).industries).toStrictEqual( + expect(adminResponse.statusCode).toBe(200); + expect(JSON.parse(adminResponse.payload).industries).toStrictEqual(DEFAULT_INDUSTRIES); + + const nonAdminResponse = await filterApp.inject({ + method: 'GET', + url: `/filters/industries`, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(nonAdminResponse.statusCode).toBe(200); + expect(JSON.parse(nonAdminResponse.payload).industries).toStrictEqual( expect.arrayContaining([ - DummyBiz.industry, - `Not ${DummyBiz.industry}` - ]) - ); + DummyBiz.industry, + `Not ${DummyBiz.industry}` + ]) + ); await filterApp.close(); done(); }); - it("Denies all industries for non-admin users", async (done) => { + it("Can add and remove industries from the global list", async (done) => { const server = testify(); const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); - const industriesResponse = await filterApp.inject({ + const SECOND_INDUSTRY = "SECOND"; + let createResponse = await createGlobalIndustry(filterApp, SECOND_INDUSTRY); + + expect(createResponse.statusCode).toBe(200); + const getAfterCreate = await filterApp.inject({ method: 'GET', url: `/filters/industries`, headers: {authorization: `Bearer ${dummyRegionManagerToken}`} }); - expect(industriesResponse.statusCode).toBe(401); + expect(getAfterCreate.statusCode).toBe(200); + expect(JSON.parse(getAfterCreate.payload).industries).toStrictEqual(expect.arrayContaining([CREATED_INDUSTRY, SECOND_INDUSTRY ])); + + const deleteResponse = await filterApp.inject({ + method: 'DELETE', + url: `/filters/industries`, + payload: {industries: [CREATED_INDUSTRY]}, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + + expect(deleteResponse.statusCode).toBe(200); + const getAfterDelete = await filterApp.inject({ + method: 'GET', + url: `/filters/industries`, + headers: {authorization: `Bearer ${dummyRegionManagerToken}`} + }); + expect(getAfterDelete.statusCode).toBe(200); + expect(JSON.parse(getAfterDelete.payload).industries).toStrictEqual( + expect.not.arrayContaining([ + CREATED_INDUSTRY + ]) + ); + await filterApp.close(); done(); }); + + async function createGlobalIndustry(filterApp: FastifyInstance, globalIndustry: string) { + return await filterApp.inject({ + method: 'POST', + url: `/filters/industries`, + payload: {industries: [globalIndustry]}, + headers: {authorization: `Bearer ${dummyAdminToken}`} + }); + } }); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 1bc717a..942055b 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -6,6 +6,7 @@ export class DummyDatalayer implements DataLayer { businesses: Business[] = []; regions: Region[] = []; editRequests: EditRequest[] = []; + industries: string[] = []; async getBusinessById(id: string): Promise { return this.businesses.find((b) => b.id === id) || null; @@ -52,10 +53,14 @@ export class DummyDatalayer implements DataLayer { } async getFilters(regionId: string): Promise { - return { - years: this.businesses.filter(b => b.regionId=== regionId).map((b) => b.year_added), - industries: this.businesses.filter(b => b.regionId=== regionId).map((b) => b.industry) - }; + if(!regionId) { + return {industries: this.industries}; + } else { + return { + years: this.businesses.filter(b => b.regionId === regionId).map((b) => b.year_added), + industries: this.businesses.filter(b => b.regionId === regionId).map((b) => b.industry) + }; + } } async setRegion(region: Region): Promise { @@ -107,6 +112,15 @@ export class DummyDatalayer implements DataLayer { return this.editRequests[index]; } + async addIndustries(industries: string[]): Promise { + this.industries.push(...industries); + } + + async deleteIndustries(industries: string[]): Promise { + this.industries = this.industries.filter(i => !industries.find(i2 => i2 === i)); + return Promise.resolve([]); + } + getPaginatedEditRequests(pageSize: number, afterId: string | undefined, filter: (r: EditRequest) => boolean) { this.editRequests.reverse(); let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; @@ -118,4 +132,5 @@ export class DummyDatalayer implements DataLayer { clearRegions() { this.regions = []; } + } From f65f6efe4e837eb93f5cf383912784dbede04e3d Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 30 Apr 2021 16:54:44 -0230 Subject: [PATCH 63/75] Added edit request count to paginated endpoints --- src/database/productionDataLayer.ts | 1 - src/endpoints/docs/editRequestSchemas.ts | 36 +++++++++--------------- src/endpoints/editRequest.ts | 14 +++++++-- tests/dataLayer.test.ts | 1 + tests/editRequests.test.ts | 11 +++++--- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index 7f6f407..e611965 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -239,7 +239,6 @@ export class ProductionDataLayer implements DataLayer { return Promise.all(promises); } - private async getPaginatedEditRequests(query: Query, pageSize: number, afterId: string | undefined) { let requests: EditRequest[] = []; query = query.orderBy("dateSubmitted", 'desc').limit(pageSize); diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index 54c40ce..8f26a44 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -28,6 +28,18 @@ import { businessSchema} from './businessesSchemas'; } } +const paginatedEditRequestsResponseSchema = { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'}, + editRequests: { + type: 'array', + items: editRequestSchema + } + } +}; + export const getEditRequestByIdSchema = { params: byIdSchema, response: { @@ -51,17 +63,7 @@ export const getEditRequestsByRegionSchema = { } }, response: { - 200: { - description: 'Successful response', - type: 'object', - properties: { - status: {type: 'string'}, - editRequests: { - type: 'array', - items: editRequestSchema - } - } - } + 200: paginatedEditRequestsResponseSchema } }; @@ -75,17 +77,7 @@ export const getAllEditRequestsByStatusSchema = { } }, response: { - 200: { - description: 'Successful response', - type: 'object', - properties: { - status: {type: 'string'}, - editRequests: { - type: 'array', - items: editRequestSchema - } - } - } + 200: paginatedEditRequestsResponseSchema } }; diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 3c275c1..19d138e 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -56,6 +56,7 @@ interface GetPaginatedEditsByRegionId extends AuthenticatedRequestByRegionId { export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { + const MAX_COUNT = 1000000000; app.get( `/edits`, @@ -69,7 +70,8 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; let response = { status: "ok", - editRequests: await dataLayer.getEditRequestsByUser(userAppId, pageSize, request.query.afterId) + editRequests: await dataLayer.getEditRequestsByUser(userAppId, pageSize, request.query.afterId), + totalCount: (await dataLayer.getEditRequestsByUser(userAppId, MAX_COUNT)).length } return JSON.stringify(response); } @@ -86,15 +88,19 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v return; } else { let editRequests : EditRequest[]; + let totalCount: number; let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; if(!request.query.status) { editRequests = await dataLayer.getAllEditRequests(pageSize, request.query.afterId); + totalCount = (await dataLayer.getAllEditRequests(MAX_COUNT)).length; } else { editRequests = await dataLayer.getEditRequestsByStatus(request.query.status, pageSize, request.query.afterId) + totalCount = (await dataLayer.getEditRequestsByStatus(request.query.status, MAX_COUNT)).length; } let response = { status: "ok", - editRequests: editRequests + editRequests, + totalCount } return JSON.stringify(response); } @@ -113,9 +119,11 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; let response = { status: "ok", - editRequests: [] + editRequests: [], + totalCount: 0 } response.editRequests = await dataLayer.getEditRequestsForRegion(request.params.regionId, pageSize, request.query.afterId) + response.totalCount = (await dataLayer.getEditRequestsForRegion(request.params.regionId, MAX_COUNT)).length return JSON.stringify(response); } } diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index a4c64e3..08442c5 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -253,6 +253,7 @@ describe("Production Data Layer Integration Tests", () => { testRequest.id = id; firstPageEdits.push({...testRequest, dateSubmitted: any(Date), dateUpdated: any(Date)}); } + let expectedFirstPageEdits = firstPageEdits.reverse(); let firstPageRecords = await productionDataLayer.getAllEditRequests(DEFAULT_PAGE_SIZE); diff --git a/tests/editRequests.test.ts b/tests/editRequests.test.ts index c8084a2..aff3646 100644 --- a/tests/editRequests.test.ts +++ b/tests/editRequests.test.ts @@ -167,14 +167,17 @@ describe("Edit Request unit tests", () => { const firstPageResponse = await getAllEditRequests(dummyAdminToken); expect(firstPageResponse.statusCode).toBe(200); - let firstResponseEdits = JSON.parse(firstPageResponse.payload).editRequests; - expect(firstResponseEdits).toStrictEqual(arrayContaining(firstPageObjects)); + let firstResponsePayload = JSON.parse(firstPageResponse.payload); + expect(firstResponsePayload.totalCount).toBe(DEFAULT_PAGE_SIZE + 1); + expect(firstResponsePayload.editRequests).toStrictEqual(arrayContaining(firstPageObjects)); - let afterId = firstResponseEdits[firstResponseEdits.length-1].id; + let afterId = firstResponsePayload.editRequests[firstResponsePayload.editRequests.length-1].id; afterId = !!afterId? afterId : ""; const secondPageResponse = await getAllEditRequests(dummyAdminToken, {afterId}); expect(secondPageResponse.statusCode).toBe(200); - expect(JSON.parse(secondPageResponse.payload).editRequests).toStrictEqual([secondPageRequest]); + let secondPagePayload = JSON.parse(secondPageResponse.payload); + expect(secondPagePayload.totalCount).toBe(DEFAULT_PAGE_SIZE + 1); + expect(secondPagePayload.editRequests).toStrictEqual([secondPageRequest]); await testApp.close(); done(); From 2166d8e88e005f81efe800e7b16a25f493b9952b Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 30 Apr 2021 17:10:12 -0230 Subject: [PATCH 64/75] Fixing docs and removing deprecated API method. --- src/endpoints/docs/editRequestSchemas.ts | 3 +- src/endpoints/editRequest.ts | 41 ++++-------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/src/endpoints/docs/editRequestSchemas.ts b/src/endpoints/docs/editRequestSchemas.ts index 8f26a44..a225400 100644 --- a/src/endpoints/docs/editRequestSchemas.ts +++ b/src/endpoints/docs/editRequestSchemas.ts @@ -36,7 +36,8 @@ const paginatedEditRequestsResponseSchema = { editRequests: { type: 'array', items: editRequestSchema - } + }, + totalCount: { type: 'number' } } }; diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 19d138e..5b553bb 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -33,6 +33,13 @@ interface UpdateEditRequest extends AuthenticatedRequestById { Body: EditRequest } +interface GetPaginatedEditsByRegionId extends AuthenticatedRequestByRegionId { + Querystring: { + afterId: string, + pageSize: number + } +} + interface GetPaginatedEditsByStatusRequest extends AuthenticatedRequest { Querystring: { status: string, @@ -41,43 +48,9 @@ interface GetPaginatedEditsByStatusRequest extends AuthenticatedRequest { } } -interface PaginatedQueryString { - afterId: string, - pageSize: number -} - -interface GetPaginatedEditsRequest extends AuthenticatedRequest { - Querystring: PaginatedQueryString -} - -interface GetPaginatedEditsByRegionId extends AuthenticatedRequestByRegionId { - Querystring: PaginatedQueryString -} - - export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { const MAX_COUNT = 1000000000; - app.get( - `/edits`, - {schema: getEditRequestByIdSchema}, - async (request, reply) => { - let {userAppId} = await verifyJwt(request); - if(!userAppId) { - reply.unauthorized("Must be logged in!"); - return; - } else { - let pageSize = !!request.query.pageSize && request.query.pageSize > 0 ? request.query.pageSize : DEFAULT_PAGE_SIZE; - let response = { - status: "ok", - editRequests: await dataLayer.getEditRequestsByUser(userAppId, pageSize, request.query.afterId), - totalCount: (await dataLayer.getEditRequestsByUser(userAppId, MAX_COUNT)).length - } - return JSON.stringify(response); - } - } - ); - app.get( "/edits/all", {schema: getAllEditRequestsByStatusSchema}, From 9460fa352e2656f1b45b670c8f05c80e9dfbfe38 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 1 May 2021 21:22:48 -0230 Subject: [PATCH 65/75] Fixing issues with filter and region delete endpoints as well as general test failures induced by last couple of fixes. --- src/database/productionDataLayer.ts | 23 ++++++++++++----- src/endpoints/filters.ts | 38 +++++++++++++++++------------ src/endpoints/regions.ts | 4 +-- src/utils.ts | 2 +- tests/auth0.test.ts | 1 + tests/dataLayer.test.ts | 4 +-- tests/filters.test.ts | 9 ++++--- tests/regions.test.ts | 18 +++++++++----- tests/testUtils/testDataLayer.ts | 5 ++-- 9 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index e611965..fedccd8 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -49,7 +49,7 @@ export interface DataLayer { getEditRequestsByUser(userAppId: string, pageSize: number, afterId?: string): Promise; createEditRequest(add: EditRequest): Promise; updateEditRequest(body: EditRequest): Promise; - addIndustries(industries: string[]): Promise; + addIndustries(industries: string[]): Promise; deleteIndustries(industries: string[]) : Promise; } @@ -141,8 +141,14 @@ export class ProductionDataLayer implements DataLayer { async getFilters(region?: string) : Promise{ if(!!region) { let regionData = (await this.firestore.collection("regions").doc(region).get()).data(); - regionData = !!regionData ? regionData : {}; - return regionData.filters; + let industries: string[] = []; + let years: number[] = []; + + if(!!regionData && !!regionData.filters) { + industries = regionData.filters.industries?.map((i: { industry: any; }) => i.industry); + years = regionData.filters.years?.map((y: { year: any; }) => Number.parseInt(y.year)) + } + return {industries, years}; } else { let industries: string[] = []; let industryData = (await this.firestore.collection("industries").get()).docs; @@ -225,9 +231,14 @@ export class ProductionDataLayer implements DataLayer { return this.getPaginatedEditRequests(query, pageSize, afterId); } - async addIndustries(industries: string[]): Promise { - industries.forEach( - i => this.firestore.collection("industries").doc(i).set({active: true}) + async addIndustries(industries: string[]): Promise { + return Promise.all( + industries.map( + async i => { + await this.firestore.collection("industries").doc(i).set({active: true}); + return; + } + ) ); } diff --git a/src/endpoints/filters.ts b/src/endpoints/filters.ts index 7193a7c..423e800 100644 --- a/src/endpoints/filters.ts +++ b/src/endpoints/filters.ts @@ -47,24 +47,30 @@ export function createFiltersEndpoint(app: FastifyInstance, dataLayer: DataLayer reply.unauthorized("Must be logged in to access this data"); return; } else { - let response = { - status: "ok", - date: Date.now(), - industries: [] - }; - let regions = await dataLayer.getAllRegions(); - let industries: string[] = []; - regions.forEach((r) => { - if (!!r.filters && !!r.filters.industries) { - industries.push(...r.filters.industries.map(i => i.industry)); + try { + let response = { + status: "ok", + date: Date.now(), + industries: [] + }; + let regions = await dataLayer.getAllRegions(); + let industries: string[] = []; + for (let i = 0; i < regions.length; i++) { + let filters = await dataLayer.getFilters(regions[i].id); + if (!!filters && !!filters.industries) { + industries.push(...filters.industries); + } } - }); - let globalIndustries = (await dataLayer.getFilters()).industries ?? []; - industries.push(...globalIndustries); - // Get just unique values - response.industries = Array.from(new Set(industries)); + let globalIndustries = (await dataLayer.getFilters()).industries ?? []; + industries.push(...globalIndustries); + // Get just unique values + response.industries = Array.from(new Set(industries)); - return JSON.stringify(response); + return JSON.stringify(response); + } catch (e) { + console.log(e); + throw e; + } } } ) diff --git a/src/endpoints/regions.ts b/src/endpoints/regions.ts index 0854f82..989dc95 100644 --- a/src/endpoints/regions.ts +++ b/src/endpoints/regions.ts @@ -141,8 +141,8 @@ export default function createRegionsEndpoint(app: FastifyInstance, dataLayer : '/regions/:regionId', {schema: deleteRegionReqSchema}, async (request, reply) => { - let {userId, admin} = <{userId:string, admin: boolean}>await request.jwtVerify(); - if(admin || isRegionManager(userId, request.params.regionId, dataLayer)) { + let {userAppId, admin} = await verifyJwt(request); + if(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer)) { await dataLayer.deleteRegion(request.params.regionId); reply.code(204); } else { diff --git a/src/utils.ts b/src/utils.ts index a584f6e..29d7e9f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,6 +16,6 @@ export function addRoutes(server: FastifyInstance, ...endpoints: EndpointFunctio export async function isRegionManager(userId: string, regionId: string, dataLayer: DataLayer) { const regions = (await dataLayer.getRegionsManagedBy(userId)); - return regions.find((r) => r.name === regionId); + return !!regions.find((r) => r.id === regionId); } diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index cfbbfd3..58830c8 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -103,6 +103,7 @@ describe("Auth0 integration tests", () => { headers:{authorization: `Bearer ${adminAccessToken}`} }); expect(regionsResponse.statusCode).toBe(201); + DummyRegion.id = JSON.parse(regionsResponse.payload).id; let response = await sut.inject({ method: "GET", diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 08442c5..324b593 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -63,7 +63,7 @@ describe("Production Data Layer Integration Tests", () => { expect(byRegionData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); let filters = await productionDataLayer.getFilters(biz.regionId); - expect(filters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})) + expect(filters).toEqual(expect.objectContaining({years: [biz.year_added], industries: [biz.industry]})) biz.id = id; biz.employees = 2; @@ -75,7 +75,7 @@ describe("Production Data Layer Integration Tests", () => { expect(updatedBizData).toEqual(expect.arrayContaining([expect.objectContaining(biz)])); let updatedFilters = await productionDataLayer.getFilters(biz.regionId); - expect(updatedFilters).toEqual(expect.objectContaining({years: [{year: biz.year_added, count: 1}], industries: [{industry: biz.industry, count: 1}]})); + expect(updatedFilters).toEqual(expect.objectContaining({years: [biz.year_added], industries: [biz.industry]})); await productionDataLayer.deleteBusiness(biz.id); let emptyBizData = await productionDataLayer.getBusinessesByRegion(biz.regionId); diff --git a/tests/filters.test.ts b/tests/filters.test.ts index 860b9b5..3e67507 100644 --- a/tests/filters.test.ts +++ b/tests/filters.test.ts @@ -17,7 +17,7 @@ describe("Filter Endpoint Tests", () => { let testDataLayer: DummyDatalayer; const CREATED_INDUSTRY = 'Created Industry'; let DEFAULT_INDUSTRIES = [DummyBiz.industry, `Not ${DummyBiz.industry}`, CREATED_INDUSTRY]; - + let dummyRegionId: string; beforeAll(async (done) => { setupAuth0TestEnv(); testDataLayer = new DummyDatalayer(); @@ -26,7 +26,8 @@ describe("Filter Endpoint Tests", () => { const bizApp = createBusinessesEndpoint(server, testDataLayer, dummyTokenVerifier); const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); - await createDummyRegion(regionsApp); + let dummyRegionResponse = await createDummyRegion(regionsApp) ?? {payload: "{}"}; + dummyRegionId = JSON.parse(dummyRegionResponse.payload).regionId; let notDummyRegion = {...DummyRegion}; notDummyRegion.name = `Not ${DummyRegion.name}`; await regionsApp.inject({ @@ -50,10 +51,10 @@ describe("Filter Endpoint Tests", () => { it('Region-specific filter data is correct', async (done) => { const server = testify(); - const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); + const filterApp = createFiltersEndpoint(server, testDataLayer, dummyTokenVerifier); const filterResponse = await filterApp.inject({ method: 'GET', - url: `/regions/${DummyRegion.name}/filters`, + url: `/regions/${dummyRegionId}/filters`, headers: { authorization: `Bearer ${dummyRegionManagerToken}`} }); diff --git a/tests/regions.test.ts b/tests/regions.test.ts index d36baa8..417cb16 100644 --- a/tests/regions.test.ts +++ b/tests/regions.test.ts @@ -40,7 +40,9 @@ describe("Region Endpoint Tests", () => { url: `/regions/manager/${region.manager}` }); expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions.filter(r => r.manager == region.manager)])); + expect(JSON.parse(response.payload).regions).toStrictEqual( + expect.arrayContaining([expect.objectContaining({...region})]) + ); } const response = await app.inject({ method: 'GET', @@ -48,7 +50,12 @@ describe("Region Endpoint Tests", () => { url: `/regions/manager/${dummyAdminId}` }); expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([...testRegions])); + expect(JSON.parse(response.payload).regions).toStrictEqual( + expect.arrayContaining([ + ...testRegions.map(r => expect.objectContaining(r)), + expect.objectContaining(DummyRegion) + ]) + ); await app.close(); done(); }); @@ -58,7 +65,7 @@ describe("Region Endpoint Tests", () => { const response = await getRegionsByDummyManager(app); expect(response.statusCode).toBe(200); - expect(JSON.parse(response.payload).regions).toEqual(expect.arrayContaining([DummyRegion])); + expect(JSON.parse(response.payload).regions).toStrictEqual(expect.arrayContaining([expect.objectContaining(DummyRegion)])); await app.close(); done(); }); @@ -119,7 +126,6 @@ describe("Region Endpoint Tests", () => { }); expect(deleteResponse.statusCode).toBe(204); - const getResponse = await getRegionsByDummyManager(app); expect(JSON.parse(getResponse.payload).regions).toEqual([]); @@ -136,7 +142,7 @@ describe("Region Endpoint Tests", () => { }); expect(getRegionAdminResponse.statusCode).toBe(200); - expect(JSON.parse(getRegionAdminResponse.payload).region).toStrictEqual(DummyRegion); + expect(JSON.parse(getRegionAdminResponse.payload).region).toStrictEqual(expect.objectContaining(DummyRegion)); const getSysAdminResponse = await app.inject({ method: 'GET', @@ -145,7 +151,7 @@ describe("Region Endpoint Tests", () => { }); expect(getSysAdminResponse.statusCode).toBe(200); - expect(JSON.parse(getSysAdminResponse.payload).region).toStrictEqual(DummyRegion); + expect(JSON.parse(getSysAdminResponse.payload).region).toStrictEqual(expect.objectContaining(DummyRegion)); await app.close(); done(); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index 942055b..adce61a 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -64,7 +64,7 @@ export class DummyDatalayer implements DataLayer { } async setRegion(region: Region): Promise { - this.regions.push(region); + this.regions.push({...region, id: region.name}); return {id: region.name}; } @@ -112,8 +112,9 @@ export class DummyDatalayer implements DataLayer { return this.editRequests[index]; } - async addIndustries(industries: string[]): Promise { + async addIndustries(industries: string[]): Promise { this.industries.push(...industries); + return []; } async deleteIndustries(industries: string[]): Promise { From f749f2dd749a055dce999fb1b2e601046aad2667 Mon Sep 17 00:00:00 2001 From: "Mike Burton (Celtx)" Date: Wed, 19 May 2021 21:51:24 -0230 Subject: [PATCH 66/75] Adding credential caching to try to avoid 500s --- src/auth0.ts | 51 ++++++++++++++++++++++-------- src/endpoints/cache.ts | 20 ++++++++++++ src/endpoints/docs/cacheSchemas.ts | 12 +++++++ src/index.ts | 1 + tests/auth0.test.ts | 45 +++++++++++++++++++++++++- 5 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 src/endpoints/cache.ts create mode 100644 src/endpoints/docs/cacheSchemas.ts diff --git a/src/auth0.ts b/src/auth0.ts index 2c1c9ab..216ff81 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -78,7 +78,8 @@ export interface UserInfoPatch { username?: string } -async function getUserRole(userId: string) : Promise<{role:string}> { +type UserRole = {role: string}; +async function getUserRole(userId: string) : Promise { let app_metadata = (await getUserById(userId)).app_metadata; return {role: app_metadata.role}; } @@ -107,6 +108,7 @@ export async function getAllUsers(per_page? : number, page?: number) { export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) { userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; + console.log("Patching from API"); return callManagementApi(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch)); } @@ -126,8 +128,7 @@ async function callManagementApi(path: string, method = "GET", body = "", ref } let metaResponse = await fetch(url, options); if(metaResponse.status === 200) { - let json = await metaResponse.json() - return json; + return await metaResponse.json(); } else if (!refresh && metaResponse.status === 401) { return await callManagementApi(path, method, body,true); } else { @@ -135,8 +136,8 @@ async function callManagementApi(path: string, method = "GET", body = "", ref } } - -export async function getUserInfo(authHeader: string) { +export type UserID = {userId: string}; +export async function getUserInfo(authHeader: string) : Promise { try { let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, { "method": "GET", @@ -149,17 +150,41 @@ export async function getUserInfo(authHeader: string) { throw e; } } +export type UserAuthEntry = {userAppId: string, admin: boolean, role: string}; +export type Auth0JwtVerifier = (request: FastifyRequest) => Promise; +export type ICacheEntry = { + timestamp: number, + data: UserAuthEntry +}; -export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<{userAppId: string, admin: boolean, role: string}>; -export async function verifyJwt(request: FastifyRequest) { - let authHeader = !request.headers.authorization ? "" : request.headers.authorization +let cache = new Map(); +const LIFETIME_MILLISECONDS = 1000 * 60 * 30; // half an hour +export async function verifyJwtCached(authHeader: string, cache: Map, getUserInfo: (token: string) => Promise, getUserRole: (id: string) => Promise) { if(!authHeader) { return {userAppId: "", admin: false, role: ""}; } else { - let {userId} = await getUserInfo(authHeader); - let {role} = await getUserRole(userId); - let admin: boolean = role === "admin" - let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId; - return {userAppId, admin, role}; + let cachedAuth = cache.get(authHeader); + if(!cachedAuth || Date.now() > cachedAuth.timestamp + LIFETIME_MILLISECONDS) { + let {userId} = await getUserInfo(authHeader); + let {role} = await getUserRole(userId); + let admin: boolean = role === "admin" + let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId; + cachedAuth = {timestamp: Date.now(), data: {userAppId, admin, role}}; + cache.set(authHeader, cachedAuth); + } + return cachedAuth.data; } } + +export async function verifyJwt(request: FastifyRequest) { + let authHeader = !request.headers.authorization ? "" : request.headers.authorization; + return await verifyJwtCached(authHeader, cache, getUserInfo, getUserRole); +} + +export function removeUserFromCache(request: FastifyRequest) { + let authHeader = !request.headers.authorization ? "" : request.headers.authorization; + deleteCachedUser(authHeader); +} +export function deleteCachedUser(authHeader: string) { + cache.delete(authHeader); +} diff --git a/src/endpoints/cache.ts b/src/endpoints/cache.ts new file mode 100644 index 0000000..9383196 --- /dev/null +++ b/src/endpoints/cache.ts @@ -0,0 +1,20 @@ +import {FastifyInstance} from "fastify"; +import {Auth0JwtVerifier, removeUserFromCache} from "../auth0"; +import {AuthenticatedRequest} from "./endpointUtils"; +import {emptyCacheSchema} from "./docs/cacheSchemas"; + +export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier) { + app.post( + "/cache/empty", + {schema: emptyCacheSchema}, + async (request, reply) => { + let {userAppId} = await verifyJwt(request); + if (!userAppId) { + reply.unauthorized("Must be logged in to use this endpoint"); + } else { + removeUserFromCache(request) + } + } + ); + return app; +} diff --git a/src/endpoints/docs/cacheSchemas.ts b/src/endpoints/docs/cacheSchemas.ts new file mode 100644 index 0000000..36c5d26 --- /dev/null +++ b/src/endpoints/docs/cacheSchemas.ts @@ -0,0 +1,12 @@ +export const emptyCacheSchema = { + description: 'Removes the currently authorized user from the credentials cache', + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + status: {type: 'string'} + } + } + } +}; diff --git a/src/index.ts b/src/index.ts index 803dbe2..1df2a5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ const server = fastify({logger: true}); server.register(fastifySensible); registerSwagger(server); registerCorsHandler(server); + addRoutes( server, createPingEndpoint, diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index 58830c8..e805192 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -7,7 +7,50 @@ import fastifySensible from "fastify-sensible"; import {authenticateToTestDomain, setupAuth0TestEnv} from "./testUtils/testify"; import {DummyRegion} from "./testUtils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; -import {getUserInfo, verifyJwt} from "../src/auth0"; +import { + deleteCachedUser, + getUserInfo, + ICacheEntry, + verifyJwt, + verifyJwtCached +} from "../src/auth0"; + +describe("Auth0 unit tests", () => { + it("Pulls cached credentials when possible", async () => { + const TEST_AUTH_HEADER = "Test"; + let userCalls = 0; + let roleCalls = 0; + let testData = { + userAppId: "testUser", + role: "testRole", + admin: false + }; + + async function testCachedVerify(cache: Map) { + let v = await verifyJwtCached( + TEST_AUTH_HEADER, + cache, + () => { + userCalls++; + return Promise.resolve({userId: testData.userAppId}) + }, + () => { + roleCalls++; + return Promise.resolve({role: testData.role}) + } + ); + + expect(userCalls).toBe(1); + expect(roleCalls).toBe(1); + expect(v).toStrictEqual(testData); + } + let cache = new Map(); + await testCachedVerify(cache); + await testCachedVerify(cache); + deleteCachedUser(TEST_AUTH_HEADER); + await testCachedVerify(cache); + }); +}); describe("Auth0 integration tests", () => { let sut : FastifyInstance; From c157f8311a41605a2ce1de439cf830473358ba2a Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 27 May 2021 12:36:52 -0230 Subject: [PATCH 67/75] Adding memcached requirements --- Dockerfile | 6 +++++- conf/memcached.conf | 11 +++++++++++ conf/startup.sh | 3 +++ package.json | 1 + yarn.lock | 5 +++++ 5 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 conf/memcached.conf create mode 100644 conf/startup.sh diff --git a/Dockerfile b/Dockerfile index 5853b65..13010e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM node:15 +RUN apt-get update +RUN apt-get install -y memcached + # Create and change to the app directory. WORKDIR /usr/src/app @@ -13,8 +16,9 @@ RUN yarn install --only=production # Copy local code to the container image. COPY . . +RUN chmod +x ./conf/startup.sh RUN yarn build # Run the web service on container startup. -CMD [ "yarn", "start" ] +CMD [ "./conf/startup.sh" ] diff --git a/conf/memcached.conf b/conf/memcached.conf new file mode 100644 index 0000000..c6f5216 --- /dev/null +++ b/conf/memcached.conf @@ -0,0 +1,11 @@ +# Memory: 256 MB +-m 256 + +# Port to use +-p 11211 + +# User for the service +-u memcache + +# Listening IP Address +-l 127.0.0.1 diff --git a/conf/startup.sh b/conf/startup.sh new file mode 100644 index 0000000..c9f3ae7 --- /dev/null +++ b/conf/startup.sh @@ -0,0 +1,3 @@ +#!/bin/bash +service memcached start +yarn start diff --git a/package.json b/package.json index 2dd2783..e0931c2 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0", "jwt-decode": "^3.1.2", + "memcached-mock": "^0.1.0", "node-fetch": "^2.6.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index ca919b9..0d0291a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3383,6 +3383,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +memcached-mock@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/memcached-mock/-/memcached-mock-0.1.0.tgz#3d53d7fbb4ac1e7417c4c363f436b909d3653132" + integrity sha1-PVPX+7SsHnQXxMNj9Da5CdNlMTI= + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz" From 6e7f3a425136a536fd58389530a97ee3dcc6dcad Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Thu, 27 May 2021 20:14:51 -0230 Subject: [PATCH 68/75] Memcached test implementation complete --- package.json | 6 +- src/auth0.ts | 210 ++++++----------------------------- src/dependencies/auth0Api.ts | 142 +++++++++++++++++++++++ src/endpoints/businesses.ts | 1 - src/endpoints/cache.ts | 12 +- src/endpoints/editRequest.ts | 2 - src/endpoints/users.ts | 3 +- src/index.ts | 18 ++- tests/auth0.test.ts | 115 +++++++++++-------- tests/users.test.ts | 2 +- yarn.lock | 68 ++++++++++++ 11 files changed, 345 insertions(+), 234 deletions(-) create mode 100644 src/dependencies/auth0Api.ts diff --git a/package.json b/package.json index e0931c2..db789a4 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,16 @@ "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0", "jwt-decode": "^3.1.2", - "memcached-mock": "^0.1.0", - "node-fetch": "^2.6.1" + "memcached-node": "^0.1.0", + "node-fetch": "^2.6.1", + "typemoq": "^2.1.0" }, "devDependencies": { "@types/jest": "^26.0.15", "@types/node": "^12.12.67", "baretest": "^2.0.0", "jest": "^26.6.3", + "memcached-mock": "^0.1.0", "ts-jest": "^26.4.4", "typescript": "^4.0.3" } diff --git a/src/auth0.ts b/src/auth0.ts index 216ff81..e816c13 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -1,190 +1,52 @@ -import { FastifyRequest} from "fastify"; -import fetch from "node-fetch"; +import {getUserById, getUserIdFromAuth0} from "./dependencies/auth0Api"; +import {Memcached, ResponseCode} from "memcached-node"; -let moduleAdminToken: string; - -async function getAdminToken(refresh : boolean) { - if(refresh || !moduleAdminToken) { - let mgmtDomain = process.env.AUTH0_DOMAIN; - - let refreshResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { - "method": "post", - "headers": { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "grant_type": "client_credentials", - "client_id": process.env.AUTH0_MGMT_CLIENT_ID, - "client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET, - "audience": `https://${mgmtDomain}/api/v2/`, - "scope": "read:users update:users read:users_app_metadata update:users_app_metadata" - }) - }); - let refreshJson = await refreshResponse.json(); - moduleAdminToken = refreshJson.access_token - } - return moduleAdminToken; -} - -export interface Auth0UserInfo { - user_id: string, // Should be URL encoded since it may contain characters that do not work well in a URL. - username?: string, - email?: string - email_verified?: boolean, - phone_number?: string - phone_verified?: boolean, - created_at?: string, - updated_at?: string, - identities?: [ - { - connection?: string, - user_id: string, - provider: string, - isSocial?: boolean - } - ], - app_metadata?: any, - user_metadata?: any, - picture?: string, - name?: string, - nickname?: string, - multifactor?: string[], - last_ip?: string, - last_login?: string, - logins_count?: number, - blocked?: boolean, - given_name?: string, - family_name?: string -} +export const LIFETIME_SECONDS = 60 * 30; // half an hour +export type MinimalRequest = { headers?: {authorization?: string }}; +export type Auth0JwtVerifier = (request: MinimalRequest) => Promise; +export type UserAuthEntry = {userAppId: string, admin: boolean, role: string}; -export interface UserInfoPatch { - blocked?: boolean, - email_verified?: boolean, - email?: string, - phone_number?: string, - phone_verified?: boolean, - user_metadata?: any, - app_metadata?: any, - given_name?: string, - family_name?: string, - name?: string, - nickname?: string, - picture?: string, - verify_email?: boolean, - verify_phone_number?: boolean, - password?: string, - connection?: string, - client_id?: string, - username?: string +export function getJwtVerifier(cache: Memcached, getUserInfo = getUserIdFromAuth0, getUserRole = getUserRoleFromAuth0 ) : Auth0JwtVerifier { + return (request : MinimalRequest) => verifyJwt(request, cache, getUserInfo, getUserRole); } -type UserRole = {role: string}; -async function getUserRole(userId: string) : Promise { +async function getUserRoleFromAuth0(userId: string) : Promise { let app_metadata = (await getUserById(userId)).app_metadata; - return {role: app_metadata.role}; -} - -/* - Get user by ID: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id - Get all users: https://auth0.com/docs/api/management/v2#!/Users/get_users - Update a user: https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id - */ - -export async function getUserById(userId: string) : Promise { - userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; - return callManagementApi(`/users/${userId}`); -} - -export async function getAllUsers(per_page? : number, page?: number) { - let querystring = ""; - if(!!per_page || !!page) { - querystring = "?"; - let perPageClause = !per_page ? "" : `per_page=${per_page}` - let pageClause = !page ? "" : `page=${page}`; - querystring = `?${perPageClause}&${pageClause}`; - } - return callManagementApi(`/users${querystring}`); + return app_metadata.role; } -export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) { - userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; - console.log("Patching from API"); - return callManagementApi(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch)); -} - -async function callManagementApi(path: string, method = "GET", body = "", refresh = false) : Promise { - let mgmtDomain = process.env.AUTH0_DOMAIN; - let adminToken = await getAdminToken(refresh); - let url = encodeURI(`https://${mgmtDomain}/api/v2${path}`); - let options : any = { - method, - "headers": { - "Authorization": `Bearer ${adminToken}` - } - }; - if(!!body) { - options.headers["Content-Type"] = "application/json"; - options.body = body; - } - let metaResponse = await fetch(url, options); - if(metaResponse.status === 200) { - return await metaResponse.json(); - } else if (!refresh && metaResponse.status === 401) { - return await callManagementApi(path, method, body,true); - } else { - throw new Error(JSON.stringify(metaResponse)); - } -} - -export type UserID = {userId: string}; -export async function getUserInfo(authHeader: string) : Promise { - try { - let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, { - "method": "GET", - "headers": {"Authorization": authHeader} - }); - let payload: any = await userResponse.json(); - let userId : string = !payload.sub ? "" : payload.sub; - return {userId}; - } catch (e) { - throw e; - } -} -export type UserAuthEntry = {userAppId: string, admin: boolean, role: string}; -export type Auth0JwtVerifier = (request: FastifyRequest) => Promise; -export type ICacheEntry = { - timestamp: number, - data: UserAuthEntry -}; - -let cache = new Map(); -const LIFETIME_MILLISECONDS = 1000 * 60 * 30; // half an hour -export async function verifyJwtCached(authHeader: string, cache: Map, getUserInfo: (token: string) => Promise, getUserRole: (id: string) => Promise) { +async function verifyJwtCached(authHeader: string, cache: Memcached) { if(!authHeader) { - return {userAppId: "", admin: false, role: ""}; + return null; } else { - let cachedAuth = cache.get(authHeader); - if(!cachedAuth || Date.now() > cachedAuth.timestamp + LIFETIME_MILLISECONDS) { - let {userId} = await getUserInfo(authHeader); - let {role} = await getUserRole(userId); - let admin: boolean = role === "admin" - let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId; - cachedAuth = {timestamp: Date.now(), data: {userAppId, admin, role}}; - cache.set(authHeader, cachedAuth); + let cachedData = await cache.get(authHeader); + if (cachedData.code === ResponseCode.EXISTS && !!cachedData.data) { + let headerMetadata = cachedData.data[authHeader]; + if (!!headerMetadata && !!headerMetadata.value) { + return JSON.parse(headerMetadata.value.toString()); + } else { + return null; + } + } else { + return null; } - return cachedAuth.data; } } -export async function verifyJwt(request: FastifyRequest) { - let authHeader = !request.headers.authorization ? "" : request.headers.authorization; - return await verifyJwtCached(authHeader, cache, getUserInfo, getUserRole); +async function verifyJwtFromAuth0(authHeader: string, getUserInfo: (token: string) => Promise, getUserRole: (id: string) => Promise) { + let userId = await getUserInfo(authHeader); + let role = await getUserRole(userId); + let admin: boolean = role === "admin" + let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId; + return {userAppId, admin, role}; } -export function removeUserFromCache(request: FastifyRequest) { - let authHeader = !request.headers.authorization ? "" : request.headers.authorization; - deleteCachedUser(authHeader); -} -export function deleteCachedUser(authHeader: string) { - cache.delete(authHeader); +async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserInfo: (token: string) => Promise, getUserRole: (userId: string) => Promise) { + let authHeader = !!request.headers && !!request.headers.authorization ? request.headers.authorization : ""; + let userData = await verifyJwtCached(authHeader, userCache); + if(!userData) { + userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole) + await userCache.add(authHeader, userData, {expires: LIFETIME_SECONDS}); + } + return userData; } diff --git a/src/dependencies/auth0Api.ts b/src/dependencies/auth0Api.ts new file mode 100644 index 0000000..46eaf47 --- /dev/null +++ b/src/dependencies/auth0Api.ts @@ -0,0 +1,142 @@ +import fetch from "node-fetch"; + +let moduleAdminToken: string; + +export interface Auth0UserInfo { + user_id: string, // Should be URL encoded since it may contain characters that do not work well in a URL. + username?: string, + email?: string + email_verified?: boolean, + phone_number?: string + phone_verified?: boolean, + created_at?: string, + updated_at?: string, + identities?: [ + { + connection?: string, + user_id: string, + provider: string, + isSocial?: boolean + } + ], + app_metadata?: any, + user_metadata?: any, + picture?: string, + name?: string, + nickname?: string, + multifactor?: string[], + last_ip?: string, + last_login?: string, + logins_count?: number, + blocked?: boolean, + given_name?: string, + family_name?: string +} + +export interface UserInfoPatch { + blocked?: boolean, + email_verified?: boolean, + email?: string, + phone_number?: string, + phone_verified?: boolean, + user_metadata?: any, + app_metadata?: any, + given_name?: string, + family_name?: string, + name?: string, + nickname?: string, + picture?: string, + verify_email?: boolean, + verify_phone_number?: boolean, + password?: string, + connection?: string, + client_id?: string, + username?: string +} + +/* + Get user by ID: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id + Get all users: https://auth0.com/docs/api/management/v2#!/Users/get_users + Update a user: https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id + */ +export async function getUserById(userId: string) : Promise { + userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; + return callManagementApi(`/users/${userId}`); +} + +export async function getAllUsers(per_page? : number, page?: number) { + let querystring = ""; + if(!!per_page || !!page) { + querystring = "?"; + let perPageClause = !per_page ? "" : `per_page=${per_page}` + let pageClause = !page ? "" : `page=${page}`; + querystring = `?${perPageClause}&${pageClause}`; + } + return callManagementApi(`/users${querystring}`); +} + +export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) { + userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`; + console.log("Patching from API"); + return callManagementApi(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch)); +} + +async function callManagementApi(path: string, method = "GET", body = "", refresh = false) : Promise { + let mgmtDomain = process.env.AUTH0_DOMAIN; + let adminToken = await getAdminToken(refresh); + let url = encodeURI(`https://${mgmtDomain}/api/v2${path}`); + let options : any = { + method, + "headers": { + "Authorization": `Bearer ${adminToken}` + } + }; + if(!!body) { + options.headers["Content-Type"] = "application/json"; + options.body = body; + } + let metaResponse = await fetch(url, options); + if(metaResponse.status === 200) { + return await metaResponse.json(); + } else if (!refresh && metaResponse.status === 401) { + return await callManagementApi(path, method, body,true); + } else { + throw new Error(JSON.stringify(metaResponse)); + } +} +export async function getUserIdFromAuth0(authHeader: string) { + try { + let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, { + "method": "GET", + "headers": {"Authorization": authHeader} + }); + let payload: any = await userResponse.json(); + let userId : string = !payload.sub ? "" : payload.sub; + return userId; + } catch (e) { + throw e; + } +} + +async function getAdminToken(refresh : boolean) { + if(refresh || !moduleAdminToken) { + let mgmtDomain = process.env.AUTH0_DOMAIN; + + let refreshResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { + "method": "post", + "headers": { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "grant_type": "client_credentials", + "client_id": process.env.AUTH0_MGMT_CLIENT_ID, + "client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET, + "audience": `https://${mgmtDomain}/api/v2/`, + "scope": "read:users update:users read:users_app_metadata update:users_app_metadata" + }) + }); + let refreshJson = await refreshResponse.json(); + moduleAdminToken = refreshJson.access_token + } + return moduleAdminToken; +} diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index 9d4e238..ab277ef 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -51,7 +51,6 @@ export interface BusinessUpdate { } export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLayer, verifyJwt: Auth0JwtVerifier) { - app.get( '/regions/:regionId/businesses', {schema: getBizSchema}, diff --git a/src/endpoints/cache.ts b/src/endpoints/cache.ts index 9383196..14058be 100644 --- a/src/endpoints/cache.ts +++ b/src/endpoints/cache.ts @@ -1,9 +1,10 @@ import {FastifyInstance} from "fastify"; -import {Auth0JwtVerifier, removeUserFromCache} from "../auth0"; +import {Auth0JwtVerifier} from "../auth0"; import {AuthenticatedRequest} from "./endpointUtils"; import {emptyCacheSchema} from "./docs/cacheSchemas"; +import {Client} from "memjs"; -export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier) { +export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier, cache: Client) { app.post( "/cache/empty", {schema: emptyCacheSchema}, @@ -11,8 +12,13 @@ export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVer let {userAppId} = await verifyJwt(request); if (!userAppId) { reply.unauthorized("Must be logged in to use this endpoint"); + return; } else { - removeUserFromCache(request) + await cache.flush(); + return { + status: "Cache emptied", + date: Date.now() + } } } ); diff --git a/src/endpoints/editRequest.ts b/src/endpoints/editRequest.ts index 5b553bb..e1504ec 100644 --- a/src/endpoints/editRequest.ts +++ b/src/endpoints/editRequest.ts @@ -269,7 +269,5 @@ export function createEditEndpoint(app: FastifyInstance, dataLayer: DataLayer, v } } - return app; - } diff --git a/src/endpoints/users.ts b/src/endpoints/users.ts index 1cd18c9..326a890 100644 --- a/src/endpoints/users.ts +++ b/src/endpoints/users.ts @@ -1,5 +1,6 @@ import {FastifyInstance} from "fastify"; -import {Auth0JwtVerifier, Auth0UserInfo, getAllUsers, getUserById, updateUser, UserInfoPatch} from "../auth0"; +import {Auth0JwtVerifier, Auth0UserInfo, UserInfoPatch} from "../auth0"; +import {getAllUsers, getUserById, updateUser} from "../dependencies/auth0Api" import {AuthenticatedRequest, AuthenticatedRequestById} from "./endpointUtils"; import {getAllUsersRequestSchema, getUserInfoRequestSchema, updateUserRequestSchema} from "./docs/userSchema"; diff --git a/src/index.ts b/src/index.ts index 1df2a5f..8bd5eec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import fastify, {FastifyInstance} from 'fastify'; import fastifySensible from "fastify-sensible"; +import MemcachePlus from "memcache-plus"; + import createPingEndpoint from './endpoints/ping'; import { addRoutes } from './utils'; import { createBusinessesEndpoint} from "./endpoints/businesses"; @@ -8,10 +10,11 @@ import {ProductionDataLayer} from "./database/productionDataLayer"; import {createFiltersEndpoint} from "./endpoints/filters"; import {registerCorsHandler} from "./cors"; import {registerSwagger} from "./swagger"; -import {verifyJwt} from "./auth0"; +import {getJwtVerifier} from "./auth0"; import {createEditEndpoint} from "./endpoints/editRequest"; import {productionFirestore} from "./database/firestore"; import createUsersEndpoint from "./endpoints/users"; +import {createCacheEndpoint} from "./endpoints/cache"; let productionDataLayer = new ProductionDataLayer(productionFirestore) const port = Number(process.env.PORT || 8080); @@ -20,14 +23,17 @@ server.register(fastifySensible); registerSwagger(server); registerCorsHandler(server); +let productionCache = new MemcachePlus(); +let prodJwtVerifier = getJwtVerifier(productionCache); addRoutes( server, createPingEndpoint, - (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer, verifyJwt), - (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer, verifyJwt), - (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, verifyJwt), - (app: FastifyInstance) => createEditEndpoint(app, productionDataLayer, verifyJwt), - (app: FastifyInstance) => createUsersEndpoint(app, verifyJwt) + (app: FastifyInstance) => createFiltersEndpoint(app, productionDataLayer, prodJwtVerifier), + (app: FastifyInstance) => createRegionsEndpoint(app, productionDataLayer, prodJwtVerifier), + (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, prodJwtVerifier), + (app: FastifyInstance) => createEditEndpoint(app, productionDataLayer, prodJwtVerifier), + (app: FastifyInstance) => createUsersEndpoint(app, prodJwtVerifier), + (app: FastifyInstance) => createCacheEndpoint(app, prodJwtVerifier, productionCache) ); server.listen(port, '::', (err, address) => { diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index e805192..9cad9b3 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -1,4 +1,3 @@ -import fastify, {FastifyInstance} from "fastify"; import {addRoutes} from "../src/utils"; import createRegionsEndpoint from "../src/endpoints/regions"; import {DummyDatalayer} from "./testUtils/testDataLayer"; @@ -7,48 +6,67 @@ import fastifySensible from "fastify-sensible"; import {authenticateToTestDomain, setupAuth0TestEnv} from "./testUtils/testify"; import {DummyRegion} from "./testUtils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; -import { - deleteCachedUser, - getUserInfo, - ICacheEntry, - verifyJwt, - verifyJwtCached -} from "../src/auth0"; +import {Auth0JwtVerifier, getJwtVerifier, LIFETIME_SECONDS} from "../src/auth0"; + +import fastify, {FastifyInstance} from "fastify"; +import {IMock, It, Mock, Times} from "typemoq" +import {Memcached, ResponseCode} from "memcached-node"; +import {getUserIdFromAuth0} from "../src/dependencies/auth0Api"; describe("Auth0 unit tests", () => { - it("Pulls cached credentials when possible", async () => { - const TEST_AUTH_HEADER = "Test"; - let userCalls = 0; - let roleCalls = 0; - let testData = { - userAppId: "testUser", - role: "testRole", - admin: false - }; - - async function testCachedVerify(cache: Map) { - let v = await verifyJwtCached( - TEST_AUTH_HEADER, - cache, - () => { - userCalls++; - return Promise.resolve({userId: testData.userAppId}) - }, - () => { - roleCalls++; - return Promise.resolve({role: testData.role}) + const TEST_AUTH_HEADER = "Test"; + const TEST_USER_ID = "testUser"; + const TEST_AUTH0_ID = "auth0|" + TEST_USER_ID + const TEST_ROLE = "testRole"; + let testData = { + userAppId: TEST_USER_ID, + role: TEST_ROLE, + admin: false + }; + let infoGetter = async (_: string) => ""; + let mockIdGetter: IMock<(authHeader: string) => Promise> = Mock.ofInstance(infoGetter); + let mockRoleGetter: IMock<(userId: string) => Promise> = Mock.ofInstance(infoGetter); + const badCache = new Memcached("127.0.0.1:11211"); + const mockCache: IMock = Mock.ofInstance(badCache); + + let sut : Auth0JwtVerifier; + + beforeEach(() => { + sut = getJwtVerifier(mockCache.object, mockIdGetter.object, mockRoleGetter.object); + }); + + it("Pulls cached credentials first", async () => { + mockCache.setup(c => c.get(It.isAnyString())) + .returns((_: string) => Promise.resolve( + { + code: ResponseCode.EXISTS, + data: { + [TEST_AUTH_HEADER]: { + key: TEST_AUTH_HEADER, + value: JSON.stringify(testData) + } } - ); - - expect(userCalls).toBe(1); - expect(roleCalls).toBe(1); - expect(v).toStrictEqual(testData); - } - let cache = new Map(); - await testCachedVerify(cache); - await testCachedVerify(cache); - deleteCachedUser(TEST_AUTH_HEADER); - await testCachedVerify(cache); + } + )); + mockIdGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_AUTH0_ID)); + mockRoleGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_ROLE)); + + let data = await sut({ headers: { authorization: TEST_AUTH_HEADER } }); + expect(data).toStrictEqual(testData); + mockIdGetter.verify(g => g(TEST_AUTH_HEADER), Times.never()); + mockRoleGetter.verify(g => g("dummyUser"), Times.never()) + }); + + it("Fetches from auth0 when not cached", async () => { + mockCache.setup(c => c.get(It.isAnyString())).returns((_: string) => Promise.resolve({code: ResponseCode.NOT_FOUND})); + mockIdGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_AUTH0_ID)); + mockRoleGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_ROLE)); + + let data = await sut({ headers: { authorization: TEST_AUTH_HEADER } }); + expect(data).toStrictEqual(testData); + mockCache.verify(c => c.add(TEST_AUTH_HEADER, testData, {expires: LIFETIME_SECONDS}), Times.once()); + mockIdGetter.verify(g => g(TEST_AUTH_HEADER), Times.once()); + mockRoleGetter.verify(g => g(TEST_AUTH0_ID), Times.once()) }); }); @@ -63,8 +81,8 @@ describe("Auth0 integration tests", () => { let authTokens = await authenticateToTestDomain(); userAccessToken = authTokens.userAccessToken; adminAccessToken = authTokens.adminAccessToken; - let userInfo = await getUserInfo(`Bearer ${userAccessToken}`); - userAppId = userInfo.userId.split("|")[1]; + let userId = await getUserIdFromAuth0(`Bearer ${userAccessToken}`); + userAppId = userId.split("|")[1]; done(); }); @@ -81,9 +99,14 @@ describe("Auth0 integration tests", () => { }); describe("Region Tests", () => { + let verifyJwt: Auth0JwtVerifier; beforeEach(() => { - addRoutes(sut, - () => createRegionsEndpoint(sut, new DummyDatalayer(), verifyJwt), + let dummyCache = Mock.ofType(Memcached); + dummyCache.setup(c => c.get(It.isAnyString())).returns(_ => Promise.resolve({code: ResponseCode.NOT_FOUND})); + verifyJwt = getJwtVerifier(dummyCache.object); + addRoutes( + sut, + () => createRegionsEndpoint(sut, new DummyDatalayer(), verifyJwt) ); }); @@ -119,7 +142,11 @@ describe("Auth0 integration tests", () => { describe("Business Tests", () => { let testDataLayer: DataLayer; + let verifyJwt: Auth0JwtVerifier; beforeEach(() => { + let dummyCache = Mock.ofType(Memcached); + dummyCache.setup(c => c.get(It.isAnyString())).returns(_ => Promise.resolve({code: ResponseCode.NOT_FOUND})) + verifyJwt = getJwtVerifier(dummyCache.object); testDataLayer = new DummyDatalayer(); addRoutes(sut, () => createBusinessesEndpoint(sut, testDataLayer, verifyJwt), diff --git a/tests/users.test.ts b/tests/users.test.ts index cdc503b..880dfae 100644 --- a/tests/users.test.ts +++ b/tests/users.test.ts @@ -15,7 +15,7 @@ describe("Auth0 user endpoint tests", () => { }); } let server = testify(); - let userApp = createUsersEndpoint(server, verifyJwt); + let userApp = createUsersEndpoint(server, verifyJwt, cache); setupAuth0TestEnv(); let authTokens = await authenticateToTestDomain(); userAccessToken = authTokens.userAccessToken; diff --git a/yarn.lock b/yarn.lock index 0d0291a..d201464 100644 --- a/yarn.lock +++ b/yarn.lock @@ -952,6 +952,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +async@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz" @@ -1257,6 +1262,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== + cjs-module-lexer@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz" @@ -1345,6 +1355,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +connection-parse@0.0.x: + version "0.0.7" + resolved "https://registry.yarnpkg.com/connection-parse/-/connection-parse-0.0.7.tgz#18e7318aab06a699267372b10c5226d25a1c9a69" + integrity sha1-GOcxiqsGppkmc3KxDFIm0locmmk= + content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz" @@ -2280,6 +2295,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hashring@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/hashring/-/hashring-3.2.0.tgz#fda4efde8aa22cdb97fb1d2a65e88401e1c144ce" + integrity sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4= + dependencies: + connection-parse "0.0.x" + simple-lru-cache "0.0.x" + hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz" @@ -3319,6 +3342,11 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.4: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz" @@ -3388,6 +3416,15 @@ memcached-mock@^0.1.0: resolved "https://registry.yarnpkg.com/memcached-mock/-/memcached-mock-0.1.0.tgz#3d53d7fbb4ac1e7417c4c363f436b909d3653132" integrity sha1-PVPX+7SsHnQXxMNj9Da5CdNlMTI= +memcached-node@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/memcached-node/-/memcached-node-0.1.0.tgz#65a46986ee9c97ec08650399db6325be9b4cf0a5" + integrity sha512-IsHgbJEgNIgtUWVan3Pspz4I1IPAGxKcgr9CyKLsaw7+abTi59qeWL0D356PFGJFKvm1qjnIy7/g+sHxST4kEA== + dependencies: + async "^3.2.0" + hashring "^3.2.0" + node "^13.13.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz" @@ -3519,6 +3556,11 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-bin-setup@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/node-bin-setup/-/node-bin-setup-1.0.6.tgz#4b5c9bb937ece702d7069b36ca78af4684677528" + integrity sha512-uPIxXNis1CRbv1DwqAxkgBk5NFV3s7cMN/Gf556jSw6jBvV7ca4F9lRL/8cALcZecRibeqU+5dFYqFFmzv5a0Q== + node-cache@^5.0.1: version "5.1.2" resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" @@ -3558,6 +3600,13 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" +node@^13.13.0: + version "13.14.0" + resolved "https://registry.yarnpkg.com/node/-/node-13.14.0.tgz#2e5afdc3aa66909f1d1d860f54064fbf2720d47c" + integrity sha512-y5pClvPYkG7cTPRDmyNeCuOseyRFuZbrvQVbyAEhkfRWtfhyyu6lHYUjBSJzrSZXlbkCIlx+TKqLDAEhxc9Vkw== + dependencies: + node-bin-setup "^1.0.0" + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz" @@ -3785,6 +3834,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postinstall-build@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz" @@ -4253,6 +4307,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-lru-cache@0.0.x: + version "0.0.2" + resolved "https://registry.yarnpkg.com/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz#d59cc3a193c1a5d0320f84ee732f6e4713e511dd" + integrity sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0= + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz" @@ -4681,6 +4740,15 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typemoq@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" + integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw== + dependencies: + circular-json "^0.3.1" + lodash "^4.17.4" + postinstall-build "^5.0.1" + typescript@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz" From 43680b16d9a85a79570a3784ab5d5321054fbcd2 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 28 May 2021 07:47:57 -0230 Subject: [PATCH 69/75] Fixed compile and test issues. --- package.json | 1 + src/auth0.ts | 14 +++++++++----- src/endpoints/businesses.ts | 4 ++-- src/endpoints/cache.ts | 6 +++--- src/endpoints/users.ts | 4 ++-- src/index.ts | 4 ++-- tests/auth0.test.ts | 29 +++++++++++++---------------- tests/testUtils/dummyData.ts | 7 ++++--- tests/testUtils/testify.ts | 17 +++++++++++++++-- tests/users.test.ts | 9 ++++----- yarn.lock | 7 +++++++ 11 files changed, 62 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index db789a4..54704e3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@auth0/auth0-spa-js": "^1.13.6", "@google-cloud/firestore": "^4.9.9", + "@types/hashring": "^3.2.1", "@types/node-fetch": "^2.5.8", "body-parser": "^1.19.0", "fastify": "^3.5.1", diff --git a/src/auth0.ts b/src/auth0.ts index e816c13..e9be5d2 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -43,10 +43,14 @@ async function verifyJwtFromAuth0(authHeader: string, getUserInfo: (token: strin async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserInfo: (token: string) => Promise, getUserRole: (userId: string) => Promise) { let authHeader = !!request.headers && !!request.headers.authorization ? request.headers.authorization : ""; - let userData = await verifyJwtCached(authHeader, userCache); - if(!userData) { - userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole) - await userCache.add(authHeader, userData, {expires: LIFETIME_SECONDS}); + if (!authHeader) { + return {userAppId: null, role: null, admin: false} + } else { + let userData = await verifyJwtCached(authHeader, userCache); + if(!userData) { + userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole) + await userCache.add(authHeader, userData, {expires: LIFETIME_SECONDS}); + } + return userData; } - return userData; } diff --git a/src/endpoints/businesses.ts b/src/endpoints/businesses.ts index ab277ef..8f4bb92 100644 --- a/src/endpoints/businesses.ts +++ b/src/endpoints/businesses.ts @@ -56,8 +56,8 @@ export function createBusinessesEndpoint(app: FastifyInstance, dataLayer: DataLa {schema: getBizSchema}, async (request , reply) => { - let {userAppId, admin} = await verifyJwt(request) - if(!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { + let {userAppId, admin} = await verifyJwt(request); + if (!(admin || await isRegionManager(userAppId, request.params.regionId, dataLayer))) { reply.unauthorized("User does not have access to region"); return; } else { diff --git a/src/endpoints/cache.ts b/src/endpoints/cache.ts index 14058be..29992ff 100644 --- a/src/endpoints/cache.ts +++ b/src/endpoints/cache.ts @@ -2,9 +2,9 @@ import {FastifyInstance} from "fastify"; import {Auth0JwtVerifier} from "../auth0"; import {AuthenticatedRequest} from "./endpointUtils"; import {emptyCacheSchema} from "./docs/cacheSchemas"; -import {Client} from "memjs"; +import {Memcached} from "memcached-node"; -export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier, cache: Client) { +export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier, cache: Memcached) { app.post( "/cache/empty", {schema: emptyCacheSchema}, @@ -14,7 +14,7 @@ export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVer reply.unauthorized("Must be logged in to use this endpoint"); return; } else { - await cache.flush(); + await cache.clean(); return { status: "Cache emptied", date: Date.now() diff --git a/src/endpoints/users.ts b/src/endpoints/users.ts index 326a890..85dd681 100644 --- a/src/endpoints/users.ts +++ b/src/endpoints/users.ts @@ -1,6 +1,6 @@ import {FastifyInstance} from "fastify"; -import {Auth0JwtVerifier, Auth0UserInfo, UserInfoPatch} from "../auth0"; -import {getAllUsers, getUserById, updateUser} from "../dependencies/auth0Api" +import {Auth0JwtVerifier} from "../auth0"; +import {Auth0UserInfo, getAllUsers, getUserById, updateUser, UserInfoPatch} from "../dependencies/auth0Api" import {AuthenticatedRequest, AuthenticatedRequestById} from "./endpointUtils"; import {getAllUsersRequestSchema, getUserInfoRequestSchema, updateUserRequestSchema} from "./docs/userSchema"; diff --git a/src/index.ts b/src/index.ts index 8bd5eec..d62d75a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import fastify, {FastifyInstance} from 'fastify'; import fastifySensible from "fastify-sensible"; -import MemcachePlus from "memcache-plus"; import createPingEndpoint from './endpoints/ping'; import { addRoutes } from './utils'; @@ -15,6 +14,7 @@ import {createEditEndpoint} from "./endpoints/editRequest"; import {productionFirestore} from "./database/firestore"; import createUsersEndpoint from "./endpoints/users"; import {createCacheEndpoint} from "./endpoints/cache"; +import {Memcached} from "memcached-node"; let productionDataLayer = new ProductionDataLayer(productionFirestore) const port = Number(process.env.PORT || 8080); @@ -23,7 +23,7 @@ server.register(fastifySensible); registerSwagger(server); registerCorsHandler(server); -let productionCache = new MemcachePlus(); +let productionCache = new Memcached("127.0.0.1:11211"); let prodJwtVerifier = getJwtVerifier(productionCache); addRoutes( server, diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index 9cad9b3..300b17f 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -1,18 +1,19 @@ +import fastify, {FastifyInstance} from "fastify"; +import fastifySensible from "fastify-sensible"; +import {Memcached, ResponseCode} from "memcached-node"; +import {IMock, It, Mock, Times} from "typemoq" + import {addRoutes} from "../src/utils"; import createRegionsEndpoint from "../src/endpoints/regions"; import {DummyDatalayer} from "./testUtils/testDataLayer"; import {createBusinessesEndpoint} from "../src/endpoints/businesses"; -import fastifySensible from "fastify-sensible"; -import {authenticateToTestDomain, setupAuth0TestEnv} from "./testUtils/testify"; +import {authenticateToTestDomain, getEmptyCacheJwtVerifier, setupAuth0TestEnv} from "./testUtils/testify"; import {DummyRegion} from "./testUtils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; import {Auth0JwtVerifier, getJwtVerifier, LIFETIME_SECONDS} from "../src/auth0"; - -import fastify, {FastifyInstance} from "fastify"; -import {IMock, It, Mock, Times} from "typemoq" -import {Memcached, ResponseCode} from "memcached-node"; import {getUserIdFromAuth0} from "../src/dependencies/auth0Api"; + describe("Auth0 unit tests", () => { const TEST_AUTH_HEADER = "Test"; const TEST_USER_ID = "testUser"; @@ -37,8 +38,8 @@ describe("Auth0 unit tests", () => { it("Pulls cached credentials first", async () => { mockCache.setup(c => c.get(It.isAnyString())) - .returns((_: string) => Promise.resolve( - { + .returns((_: string) => + Promise.resolve({ code: ResponseCode.EXISTS, data: { [TEST_AUTH_HEADER]: { @@ -46,8 +47,8 @@ describe("Auth0 unit tests", () => { value: JSON.stringify(testData) } } - } - )); + }) + ); mockIdGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_AUTH0_ID)); mockRoleGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_ROLE)); @@ -101,9 +102,7 @@ describe("Auth0 integration tests", () => { describe("Region Tests", () => { let verifyJwt: Auth0JwtVerifier; beforeEach(() => { - let dummyCache = Mock.ofType(Memcached); - dummyCache.setup(c => c.get(It.isAnyString())).returns(_ => Promise.resolve({code: ResponseCode.NOT_FOUND})); - verifyJwt = getJwtVerifier(dummyCache.object); + verifyJwt = getEmptyCacheJwtVerifier(); addRoutes( sut, () => createRegionsEndpoint(sut, new DummyDatalayer(), verifyJwt) @@ -144,9 +143,7 @@ describe("Auth0 integration tests", () => { let testDataLayer: DataLayer; let verifyJwt: Auth0JwtVerifier; beforeEach(() => { - let dummyCache = Mock.ofType(Memcached); - dummyCache.setup(c => c.get(It.isAnyString())).returns(_ => Promise.resolve({code: ResponseCode.NOT_FOUND})) - verifyJwt = getJwtVerifier(dummyCache.object); + verifyJwt = getEmptyCacheJwtVerifier(); testDataLayer = new DummyDatalayer(); addRoutes(sut, () => createBusinessesEndpoint(sut, testDataLayer, verifyJwt), diff --git a/tests/testUtils/dummyData.ts b/tests/testUtils/dummyData.ts index 2d09353..fe148ce 100644 --- a/tests/testUtils/dummyData.ts +++ b/tests/testUtils/dummyData.ts @@ -1,7 +1,8 @@ -import {FastifyInstance, FastifyRequest} from "fastify"; +import {FastifyInstance} from "fastify"; import {Business, BusinessUpdate} from "../../src/endpoints/businesses"; import {Region} from "../../src/database/productionDataLayer"; import {getMockToken} from "./testify"; +import {MinimalRequest} from "../../src/auth0"; const dummyManager = "DummyManagerId"; export const dummyAdminId = "admin"; @@ -30,8 +31,8 @@ export const DummyBizUpdate: BusinessUpdate = { }; -export async function dummyTokenVerifier (req: FastifyRequest) { - if(!req.headers.authorization || !req.headers.authorization.split("Bearer")[1].trim()) { +export async function dummyTokenVerifier (req: MinimalRequest) { + if(!req.headers || !req.headers.authorization || !req.headers.authorization.split("Bearer")[1].trim()) { return {userAppId: "", admin: false, role: ""}; } if(req.headers.authorization.indexOf(dummyAdminToken) > 0) { diff --git a/tests/testUtils/testify.ts b/tests/testUtils/testify.ts index dee7f34..4e84178 100644 --- a/tests/testUtils/testify.ts +++ b/tests/testUtils/testify.ts @@ -1,8 +1,12 @@ -import fastify, {FastifyRequest} from "fastify"; +import fastify from "fastify"; import fastifyJWT, {FastifyJWTOptions} from "fastify-jwt"; import jwt from "jsonwebtoken"; import fastifySensible from "fastify-sensible"; import fetch from "node-fetch"; +import {It, Mock} from "typemoq"; +import {Memcached, ResponseCode} from "memcached-node"; + +import {getJwtVerifier, MinimalRequest} from "../../src/auth0"; export const AUTH0_CLAIMS_NAMESPACE = "https://mun.ca"; export const mockSecret = 'dummy'; @@ -61,9 +65,18 @@ export function getMockToken(payload: {userAppId: string, admin: boolean }) { } export function getTestJwtVerifier(userAppId: string, admin: boolean) { - return async(_: FastifyRequest) => ({userAppId, admin, role: admin ? "admin" : "region" }); + return async(_: MinimalRequest) => ({userAppId, admin, role: admin ? "admin" : "region" }); } + +export function getEmptyCacheJwtVerifier() { + let cachePrototype = new Memcached("127.0.0.1:11211"); + let dummyCache = Mock.ofInstance(cachePrototype); + dummyCache.setup(c => c.get(It.isAnyString())).returns(_ => Promise.resolve({code: ResponseCode.NOT_FOUND})); + return getJwtVerifier(dummyCache.object); +} + + export const testify = () => { const f = fastify({logger: {level: "debug"}}); f.register(fastifyJWT, { diff --git a/tests/users.test.ts b/tests/users.test.ts index 880dfae..209fbca 100644 --- a/tests/users.test.ts +++ b/tests/users.test.ts @@ -1,6 +1,5 @@ -import {authenticateToTestDomain, setupAuth0TestEnv, testify} from "./testUtils/testify"; +import {authenticateToTestDomain, getEmptyCacheJwtVerifier, setupAuth0TestEnv, testify} from "./testUtils/testify"; import createUsersEndpoint from "../src/endpoints/users"; -import { verifyJwt } from "../src/auth0"; describe("Auth0 user endpoint tests", () => { let userAccessToken: string; @@ -15,7 +14,7 @@ describe("Auth0 user endpoint tests", () => { }); } let server = testify(); - let userApp = createUsersEndpoint(server, verifyJwt, cache); + let userApp = createUsersEndpoint(server, getEmptyCacheJwtVerifier()); setupAuth0TestEnv(); let authTokens = await authenticateToTestDomain(); userAccessToken = authTokens.userAccessToken; @@ -46,7 +45,7 @@ describe("Auth0 user endpoint tests", () => { }); } let server = testify(); - let userApp = createUsersEndpoint(server, verifyJwt); + let userApp = createUsersEndpoint(server, getEmptyCacheJwtVerifier()); setupAuth0TestEnv(); let authTokens = await authenticateToTestDomain(); userAccessToken = authTokens.userAccessToken; @@ -76,7 +75,7 @@ describe("Auth0 user endpoint tests", () => { }); } let server = testify(); - let userApp = createUsersEndpoint(server, verifyJwt); + let userApp = createUsersEndpoint(server, getEmptyCacheJwtVerifier()); setupAuth0TestEnv(); let authTokens = await authenticateToTestDomain(); userAccessToken = authTokens.userAccessToken; diff --git a/yarn.lock b/yarn.lock index d201464..ce4b4d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -667,6 +667,13 @@ dependencies: "@types/node" "*" +"@types/hashring@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/hashring/-/hashring-3.2.1.tgz#1f5176a1d5729d631bd4d944a0be9480cd6775f4" + integrity sha512-69vRQ10rZ0rj+KHMel6vSJpy9p7O5FsiczuS7RrzqaXpL4RYYPaW4MeseOISAJ0KQo+WUknXIYLwmFHhTTJrew== + dependencies: + "@types/node" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz" From c389ab6bea1faf9284dfb0eb19c345110081fe43 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 28 May 2021 07:52:19 -0230 Subject: [PATCH 70/75] Moving typemoq to dev dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54704e3..59c22d3 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "jwt-decode": "^3.1.2", "memcached-node": "^0.1.0", "node-fetch": "^2.6.1", - "typemoq": "^2.1.0" }, "devDependencies": { "@types/jest": "^26.0.15", @@ -49,6 +48,7 @@ "jest": "^26.6.3", "memcached-mock": "^0.1.0", "ts-jest": "^26.4.4", + "typemoq": "^2.1.0", "typescript": "^4.0.3" } } From 3fe8e7b5717b18ce9beaacb78453b217ab5069bb Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 28 May 2021 08:15:52 -0230 Subject: [PATCH 71/75] Docker build adjustments for new entrypoint --- .dockerignore | 1 + Dockerfile | 2 +- conf/startup.sh | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/Dockerfile b/Dockerfile index 13010e8..6f1ce2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ RUN chmod +x ./conf/startup.sh RUN yarn build # Run the web service on container startup. -CMD [ "./conf/startup.sh" ] +CMD [ "conf/startup.sh" ] diff --git a/conf/startup.sh b/conf/startup.sh index c9f3ae7..5620d95 100644 --- a/conf/startup.sh +++ b/conf/startup.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/bin/sh service memcached start yarn start diff --git a/package.json b/package.json index 59c22d3..34f0364 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "jwks-rsa": "^1.12.0", "jwt-decode": "^3.1.2", "memcached-node": "^0.1.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.1" }, "devDependencies": { "@types/jest": "^26.0.15", From 79d1f10074c5090bb58033a855ea4df64d9b3408 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Fri, 28 May 2021 09:49:55 -0230 Subject: [PATCH 72/75] Fixing the connection to memcached --- conf/memcached.conf | 2 +- src/index.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conf/memcached.conf b/conf/memcached.conf index c6f5216..88d5411 100644 --- a/conf/memcached.conf +++ b/conf/memcached.conf @@ -8,4 +8,4 @@ -u memcache # Listening IP Address --l 127.0.0.1 +-l 0.0.0.0 diff --git a/src/index.ts b/src/index.ts index d62d75a..4a8b545 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,9 @@ import createUsersEndpoint from "./endpoints/users"; import {createCacheEndpoint} from "./endpoints/cache"; import {Memcached} from "memcached-node"; +let productionCache = new Memcached("127.0.0.1:11211"); +productionCache.createPool(); + let productionDataLayer = new ProductionDataLayer(productionFirestore) const port = Number(process.env.PORT || 8080); const server = fastify({logger: true}); @@ -23,7 +26,6 @@ server.register(fastifySensible); registerSwagger(server); registerCorsHandler(server); -let productionCache = new Memcached("127.0.0.1:11211"); let prodJwtVerifier = getJwtVerifier(productionCache); addRoutes( server, From c22053547eeea23b6c2befc39ae53c297e6a1df8 Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 29 May 2021 04:50:42 -0230 Subject: [PATCH 73/75] Shortening the keys used for memcached to allow it to work in production. --- src/auth0.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/auth0.ts b/src/auth0.ts index e9be5d2..acb45c7 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -15,13 +15,17 @@ async function getUserRoleFromAuth0(userId: string) : Promise { return app_metadata.role; } -async function verifyJwtCached(authHeader: string, cache: Memcached) { - if(!authHeader) { +function getCacheKey(authHeader: string) { + return authHeader.split(" ")[1].substr(0, 100); +} + +async function verifyJwtCached(cacheKey: string, cache: Memcached) { + if(!cacheKey) { return null; } else { - let cachedData = await cache.get(authHeader); + let cachedData = await cache.get(cacheKey); if (cachedData.code === ResponseCode.EXISTS && !!cachedData.data) { - let headerMetadata = cachedData.data[authHeader]; + let headerMetadata = cachedData.data[cacheKey]; if (!!headerMetadata && !!headerMetadata.value) { return JSON.parse(headerMetadata.value.toString()); } else { @@ -46,10 +50,11 @@ async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserI if (!authHeader) { return {userAppId: null, role: null, admin: false} } else { - let userData = await verifyJwtCached(authHeader, userCache); + let cacheKey = getCacheKey(authHeader); + let userData = await verifyJwtCached(cacheKey, userCache); if(!userData) { userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole) - await userCache.add(authHeader, userData, {expires: LIFETIME_SECONDS}); + await userCache.add(cacheKey, userData, {expires: LIFETIME_SECONDS}); } return userData; } From ada8713958aae101e73550e26e91f00d0fa64b4c Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 29 May 2021 06:31:32 -0230 Subject: [PATCH 74/75] More fixes for the userinfo cache --- Dockerfile | 8 +++++--- package.json | 11 ++++++----- src/auth0.ts | 2 +- src/index.ts | 2 +- tsconfig.json | 4 +++- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6f1ce2f..4c9bb2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,13 +12,15 @@ WORKDIR /usr/src/app COPY package*.json ./ # Install production dependencies. -RUN yarn install --only=production +RUN yarn install --production # Copy local code to the container image. -COPY . . +COPY ./src ./src +COPY ./conf ./conf +COPY ./tsconfig.json . RUN chmod +x ./conf/startup.sh RUN yarn build # Run the web service on container startup. -CMD [ "conf/startup.sh" ] +CMD [ "./conf/startup.sh" ] diff --git a/package.json b/package.json index 34f0364..5d27ae3 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "license": "UNLICENSED", "private": true, "description": "API for the RAnLab app", - "main": "build/src/index.js", + "main": "build/index.js", "scripts": { "build": "tsc --project .", + "build-prod": "tsc ./src --project .", "test": "dev/test", "jest": "jest", "start": "node ." @@ -26,6 +27,7 @@ "@auth0/auth0-spa-js": "^1.13.6", "@google-cloud/firestore": "^4.9.9", "@types/hashring": "^3.2.1", + "@types/node": "^12.12.67", "@types/node-fetch": "^2.5.8", "body-parser": "^1.19.0", "fastify": "^3.5.1", @@ -39,16 +41,15 @@ "jwks-rsa": "^1.12.0", "jwt-decode": "^3.1.2", "memcached-node": "^0.1.0", - "node-fetch": "^2.6.1" + "node-fetch": "^2.6.1", + "typescript": "^4.0.3" }, "devDependencies": { "@types/jest": "^26.0.15", - "@types/node": "^12.12.67", "baretest": "^2.0.0", "jest": "^26.6.3", "memcached-mock": "^0.1.0", "ts-jest": "^26.4.4", - "typemoq": "^2.1.0", - "typescript": "^4.0.3" + "typemoq": "^2.1.0" } } diff --git a/src/auth0.ts b/src/auth0.ts index acb45c7..9cbe492 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -54,7 +54,7 @@ async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserI let userData = await verifyJwtCached(cacheKey, userCache); if(!userData) { userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole) - await userCache.add(cacheKey, userData, {expires: LIFETIME_SECONDS}); + await userCache.set(cacheKey, userData, {mode: "json", expires: LIFETIME_SECONDS}); } return userData; } diff --git a/src/index.ts b/src/index.ts index 4a8b545..390339f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import createUsersEndpoint from "./endpoints/users"; import {createCacheEndpoint} from "./endpoints/cache"; import {Memcached} from "memcached-node"; -let productionCache = new Memcached("127.0.0.1:11211"); +let productionCache = new Memcached("127.0.0.1:11211", {wait:true}); productionCache.createPool(); let productionDataLayer = new ProductionDataLayer(productionFirestore) diff --git a/tsconfig.json b/tsconfig.json index 7c0e8e9..7311df2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] } From 12bc915535574e6fb264c30ad6438e2d5db01c2d Mon Sep 17 00:00:00 2001 From: "Michael G. Burton" Date: Sat, 12 Jun 2021 17:51:21 -0230 Subject: [PATCH 75/75] Using firestore to cache user credentials --- package.json | 2 -- src/auth0.ts | 23 ++++++-------- src/database/productionDataLayer.ts | 32 +++++++++++++++++-- src/endpoints/cache.ts | 6 ++-- src/index.ts | 8 ++--- tests/auth0.test.ts | 36 +++++++++------------ tests/dataLayer.test.ts | 11 +++++++ tests/testUtils/testDataLayer.ts | 22 ++++++++++--- tests/testUtils/testify.ts | 7 ++--- yarn.lock | 49 ----------------------------- 10 files changed, 90 insertions(+), 106 deletions(-) diff --git a/package.json b/package.json index 5d27ae3..21c7761 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.0", "jwt-decode": "^3.1.2", - "memcached-node": "^0.1.0", "node-fetch": "^2.6.1", "typescript": "^4.0.3" }, @@ -48,7 +47,6 @@ "@types/jest": "^26.0.15", "baretest": "^2.0.0", "jest": "^26.6.3", - "memcached-mock": "^0.1.0", "ts-jest": "^26.4.4", "typemoq": "^2.1.0" } diff --git a/src/auth0.ts b/src/auth0.ts index 9cbe492..ff4c812 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -1,12 +1,12 @@ import {getUserById, getUserIdFromAuth0} from "./dependencies/auth0Api"; -import {Memcached, ResponseCode} from "memcached-node"; +import {DataLayer} from "./database/productionDataLayer"; -export const LIFETIME_SECONDS = 60 * 30; // half an hour +export const LIFETIME_MILLISECONDS = 1000 * 60 * 30; // half an hour export type MinimalRequest = { headers?: {authorization?: string }}; export type Auth0JwtVerifier = (request: MinimalRequest) => Promise; export type UserAuthEntry = {userAppId: string, admin: boolean, role: string}; -export function getJwtVerifier(cache: Memcached, getUserInfo = getUserIdFromAuth0, getUserRole = getUserRoleFromAuth0 ) : Auth0JwtVerifier { +export function getJwtVerifier(cache: DataLayer, getUserInfo = getUserIdFromAuth0, getUserRole = getUserRoleFromAuth0 ) : Auth0JwtVerifier { return (request : MinimalRequest) => verifyJwt(request, cache, getUserInfo, getUserRole); } @@ -19,18 +19,13 @@ function getCacheKey(authHeader: string) { return authHeader.split(" ")[1].substr(0, 100); } -async function verifyJwtCached(cacheKey: string, cache: Memcached) { +async function verifyJwtCached(cacheKey: string, cache: DataLayer) { if(!cacheKey) { return null; } else { - let cachedData = await cache.get(cacheKey); - if (cachedData.code === ResponseCode.EXISTS && !!cachedData.data) { - let headerMetadata = cachedData.data[cacheKey]; - if (!!headerMetadata && !!headerMetadata.value) { - return JSON.parse(headerMetadata.value.toString()); - } else { - return null; - } + let cachedData = await cache.getUserInfo(cacheKey); + if (!!cachedData) { + return cachedData; } else { return null; } @@ -45,7 +40,7 @@ async function verifyJwtFromAuth0(authHeader: string, getUserInfo: (token: strin return {userAppId, admin, role}; } -async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserInfo: (token: string) => Promise, getUserRole: (userId: string) => Promise) { +async function verifyJwt(request: MinimalRequest, userCache: DataLayer, getUserInfo: (token: string) => Promise, getUserRole: (userId: string) => Promise) { let authHeader = !!request.headers && !!request.headers.authorization ? request.headers.authorization : ""; if (!authHeader) { return {userAppId: null, role: null, admin: false} @@ -54,7 +49,7 @@ async function verifyJwt(request: MinimalRequest, userCache: Memcached, getUserI let userData = await verifyJwtCached(cacheKey, userCache); if(!userData) { userData = await verifyJwtFromAuth0(authHeader, getUserInfo, getUserRole) - await userCache.set(cacheKey, userData, {mode: "json", expires: LIFETIME_SECONDS}); + await userCache.setUserInfo(cacheKey, userData); } return userData; } diff --git a/src/database/productionDataLayer.ts b/src/database/productionDataLayer.ts index fedccd8..44b82bb 100644 --- a/src/database/productionDataLayer.ts +++ b/src/database/productionDataLayer.ts @@ -1,7 +1,8 @@ import {Business, CHUNK_SIZE} from "../endpoints/businesses"; import {EditRequest} from "../endpoints/editRequest"; import { - DocumentData, DocumentSnapshot, + DocumentData, + DocumentSnapshot, FieldValue, Firestore, Query, @@ -9,6 +10,7 @@ import { Timestamp, Transaction } from "@google-cloud/firestore"; +import {LIFETIME_MILLISECONDS} from "../auth0"; export interface IdObject { id: string @@ -51,6 +53,9 @@ export interface DataLayer { updateEditRequest(body: EditRequest): Promise; addIndustries(industries: string[]): Promise; deleteIndustries(industries: string[]) : Promise; + getUserInfo(cacheKey: string): Promise; + setUserInfo(cacheKey: string, userData: any): Promise; + cleanCachedUsers(): Promise; } export class ProductionDataLayer implements DataLayer { @@ -59,6 +64,30 @@ export class ProductionDataLayer implements DataLayer { this.firestore = firestore; } + async getUserInfo(cacheKey: string): Promise { + let userData = (await this.firestore.collection("userInfo").doc(cacheKey).get()).data(); + if(!!userData && userData.expires > Date.now()) { + return userData.metadata; + } else { + await this.firestore.collection("userInfo").doc(cacheKey).delete(); + } + return null; + } + + async setUserInfo(cacheKey: string, userData: any): Promise { + await this.firestore.collection("userInfo").doc(cacheKey).set({ + "expires": Date.now() + LIFETIME_MILLISECONDS, + "metadata": userData + }); + } + + async cleanCachedUsers(): Promise { + let cachedItems = await this.firestore.collection("userInfo").listDocuments(); + for(let item of cachedItems) { + item.delete().then(_ => {}); + } + } + async getBusinessById(id: string): Promise { let businessSnapshot = await this.firestore.collection("businesses").doc(id).get(); return {...businessSnapshot.data(), id: businessSnapshot.id}; @@ -360,6 +389,5 @@ export class ProductionDataLayer implements DataLayer { return {}; } } - } diff --git a/src/endpoints/cache.ts b/src/endpoints/cache.ts index 29992ff..368fe32 100644 --- a/src/endpoints/cache.ts +++ b/src/endpoints/cache.ts @@ -2,9 +2,9 @@ import {FastifyInstance} from "fastify"; import {Auth0JwtVerifier} from "../auth0"; import {AuthenticatedRequest} from "./endpointUtils"; import {emptyCacheSchema} from "./docs/cacheSchemas"; -import {Memcached} from "memcached-node"; +import {DataLayer} from "../database/productionDataLayer"; -export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier, cache: Memcached) { +export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVerifier, cache: DataLayer) { app.post( "/cache/empty", {schema: emptyCacheSchema}, @@ -14,7 +14,7 @@ export function createCacheEndpoint(app: FastifyInstance, verifyJwt: Auth0JwtVer reply.unauthorized("Must be logged in to use this endpoint"); return; } else { - await cache.clean(); + await cache.cleanCachedUsers(); return { status: "Cache emptied", date: Date.now() diff --git a/src/index.ts b/src/index.ts index 390339f..aaf70d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,10 +14,6 @@ import {createEditEndpoint} from "./endpoints/editRequest"; import {productionFirestore} from "./database/firestore"; import createUsersEndpoint from "./endpoints/users"; import {createCacheEndpoint} from "./endpoints/cache"; -import {Memcached} from "memcached-node"; - -let productionCache = new Memcached("127.0.0.1:11211", {wait:true}); -productionCache.createPool(); let productionDataLayer = new ProductionDataLayer(productionFirestore) const port = Number(process.env.PORT || 8080); @@ -26,7 +22,7 @@ server.register(fastifySensible); registerSwagger(server); registerCorsHandler(server); -let prodJwtVerifier = getJwtVerifier(productionCache); +let prodJwtVerifier = getJwtVerifier(productionDataLayer); addRoutes( server, createPingEndpoint, @@ -35,7 +31,7 @@ addRoutes( (app: FastifyInstance) => createBusinessesEndpoint(app, productionDataLayer, prodJwtVerifier), (app: FastifyInstance) => createEditEndpoint(app, productionDataLayer, prodJwtVerifier), (app: FastifyInstance) => createUsersEndpoint(app, prodJwtVerifier), - (app: FastifyInstance) => createCacheEndpoint(app, prodJwtVerifier, productionCache) + (app: FastifyInstance) => createCacheEndpoint(app, prodJwtVerifier, productionDataLayer) ); server.listen(port, '::', (err, address) => { diff --git a/tests/auth0.test.ts b/tests/auth0.test.ts index 300b17f..9df54de 100644 --- a/tests/auth0.test.ts +++ b/tests/auth0.test.ts @@ -1,6 +1,5 @@ import fastify, {FastifyInstance} from "fastify"; import fastifySensible from "fastify-sensible"; -import {Memcached, ResponseCode} from "memcached-node"; import {IMock, It, Mock, Times} from "typemoq" import {addRoutes} from "../src/utils"; @@ -10,12 +9,13 @@ import {createBusinessesEndpoint} from "../src/endpoints/businesses"; import {authenticateToTestDomain, getEmptyCacheJwtVerifier, setupAuth0TestEnv} from "./testUtils/testify"; import {DummyRegion} from "./testUtils/dummyData"; import {DataLayer} from "../src/database/productionDataLayer"; -import {Auth0JwtVerifier, getJwtVerifier, LIFETIME_SECONDS} from "../src/auth0"; +import {Auth0JwtVerifier, getJwtVerifier} from "../src/auth0"; import {getUserIdFromAuth0} from "../src/dependencies/auth0Api"; describe("Auth0 unit tests", () => { - const TEST_AUTH_HEADER = "Test"; + const TEST_AUTH_TOKEN = "Test" + const TEST_AUTH_HEADER = `Bearer ${TEST_AUTH_TOKEN}`; const TEST_USER_ID = "testUser"; const TEST_AUTH0_ID = "auth0|" + TEST_USER_ID const TEST_ROLE = "testRole"; @@ -25,30 +25,21 @@ describe("Auth0 unit tests", () => { admin: false }; let infoGetter = async (_: string) => ""; - let mockIdGetter: IMock<(authHeader: string) => Promise> = Mock.ofInstance(infoGetter); - let mockRoleGetter: IMock<(userId: string) => Promise> = Mock.ofInstance(infoGetter); - const badCache = new Memcached("127.0.0.1:11211"); - const mockCache: IMock = Mock.ofInstance(badCache); let sut : Auth0JwtVerifier; + let mockIdGetter: IMock<(authHeader: string) => Promise>; + let mockRoleGetter: IMock<(userId: string) => Promise>; + let mockCache: IMock; beforeEach(() => { + mockIdGetter = Mock.ofInstance(infoGetter); + mockRoleGetter = Mock.ofInstance(infoGetter); + mockCache = Mock.ofType(); sut = getJwtVerifier(mockCache.object, mockIdGetter.object, mockRoleGetter.object); }); it("Pulls cached credentials first", async () => { - mockCache.setup(c => c.get(It.isAnyString())) - .returns((_: string) => - Promise.resolve({ - code: ResponseCode.EXISTS, - data: { - [TEST_AUTH_HEADER]: { - key: TEST_AUTH_HEADER, - value: JSON.stringify(testData) - } - } - }) - ); + mockCache.setup(c => c.getUserInfo(It.isAnyString())).returns((_: string) => Promise.resolve(testData)); mockIdGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_AUTH0_ID)); mockRoleGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_ROLE)); @@ -58,14 +49,15 @@ describe("Auth0 unit tests", () => { mockRoleGetter.verify(g => g("dummyUser"), Times.never()) }); - it("Fetches from auth0 when not cached", async () => { - mockCache.setup(c => c.get(It.isAnyString())).returns((_: string) => Promise.resolve({code: ResponseCode.NOT_FOUND})); + it("Fetches from auth0 and adds to cache when not initially cached", async () => { + mockCache.setup(c => c.getUserInfo(TEST_AUTH_HEADER)).returns((_: string) => Promise.resolve(null)); + mockCache.setup(c => c.setUserInfo(TEST_AUTH_HEADER, It.isAny())); mockIdGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_AUTH0_ID)); mockRoleGetter.setup(g => g(It.isAnyString())).returns(_ => Promise.resolve(TEST_ROLE)); let data = await sut({ headers: { authorization: TEST_AUTH_HEADER } }); expect(data).toStrictEqual(testData); - mockCache.verify(c => c.add(TEST_AUTH_HEADER, testData, {expires: LIFETIME_SECONDS}), Times.once()); + mockCache.verify(c => c.setUserInfo(TEST_AUTH_TOKEN, It.isValue(testData)), Times.once()); mockIdGetter.verify(g => g(TEST_AUTH_HEADER), Times.once()); mockRoleGetter.verify(g => g(TEST_AUTH0_ID), Times.once()) }); diff --git a/tests/dataLayer.test.ts b/tests/dataLayer.test.ts index 324b593..8f868d2 100644 --- a/tests/dataLayer.test.ts +++ b/tests/dataLayer.test.ts @@ -355,4 +355,15 @@ describe("Production Data Layer Integration Tests", () => { done(); }); }); + + it("Can cache, retrieve, and delete a long token", async(done) => { + const longToken = "a".repeat(500); + const dummyData = {data: "Dummy"}; + expect(await productionDataLayer.getUserInfo(longToken)).toBe(null); + await productionDataLayer.setUserInfo(longToken, dummyData); + expect(await productionDataLayer.getUserInfo(longToken)).toStrictEqual(dummyData); + await productionDataLayer.cleanCachedUsers(); + expect(await productionDataLayer.getUserInfo(longToken)).toBe(null); + done(); + }); }); diff --git a/tests/testUtils/testDataLayer.ts b/tests/testUtils/testDataLayer.ts index adce61a..24bb0e5 100644 --- a/tests/testUtils/testDataLayer.ts +++ b/tests/testUtils/testDataLayer.ts @@ -7,6 +7,8 @@ export class DummyDatalayer implements DataLayer { regions: Region[] = []; editRequests: EditRequest[] = []; industries: string[] = []; + key: string = ""; + data: any = null; async getBusinessById(id: string): Promise { return this.businesses.find((b) => b.id === id) || null; @@ -122,6 +124,22 @@ export class DummyDatalayer implements DataLayer { return Promise.resolve([]); } + async getUserInfo(key: string): Promise { + if (this.key === key) { + return this.data; + } + return null; + } + async setUserInfo(key: string, data: any): Promise { + this.key = key; + this.data = data; + } + + async cleanCachedUsers(): Promise { + this.key = ""; + this.data = null; + } + getPaginatedEditRequests(pageSize: number, afterId: string | undefined, filter: (r: EditRequest) => boolean) { this.editRequests.reverse(); let startIndex = !!afterId? this.editRequests.findIndex((r) => r.id === afterId) + 1 : 0; @@ -130,8 +148,4 @@ export class DummyDatalayer implements DataLayer { return ret; } - clearRegions() { - this.regions = []; - } - } diff --git a/tests/testUtils/testify.ts b/tests/testUtils/testify.ts index 4e84178..1658720 100644 --- a/tests/testUtils/testify.ts +++ b/tests/testUtils/testify.ts @@ -4,9 +4,9 @@ import jwt from "jsonwebtoken"; import fastifySensible from "fastify-sensible"; import fetch from "node-fetch"; import {It, Mock} from "typemoq"; -import {Memcached, ResponseCode} from "memcached-node"; import {getJwtVerifier, MinimalRequest} from "../../src/auth0"; +import {DataLayer} from "../../src/database/productionDataLayer"; export const AUTH0_CLAIMS_NAMESPACE = "https://mun.ca"; export const mockSecret = 'dummy'; @@ -70,9 +70,8 @@ export function getTestJwtVerifier(userAppId: string, admin: boolean) { export function getEmptyCacheJwtVerifier() { - let cachePrototype = new Memcached("127.0.0.1:11211"); - let dummyCache = Mock.ofInstance(cachePrototype); - dummyCache.setup(c => c.get(It.isAnyString())).returns(_ => Promise.resolve({code: ResponseCode.NOT_FOUND})); + let dummyCache = Mock.ofType(); + dummyCache.setup(c => c.getUserInfo(It.isAnyString())).returns(_ => Promise.resolve(null)); return getJwtVerifier(dummyCache.object); } diff --git a/yarn.lock b/yarn.lock index ce4b4d8..a618ca1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -959,11 +959,6 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -async@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz" @@ -1362,11 +1357,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -connection-parse@0.0.x: - version "0.0.7" - resolved "https://registry.yarnpkg.com/connection-parse/-/connection-parse-0.0.7.tgz#18e7318aab06a699267372b10c5226d25a1c9a69" - integrity sha1-GOcxiqsGppkmc3KxDFIm0locmmk= - content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz" @@ -2302,14 +2292,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hashring@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/hashring/-/hashring-3.2.0.tgz#fda4efde8aa22cdb97fb1d2a65e88401e1c144ce" - integrity sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4= - dependencies: - connection-parse "0.0.x" - simple-lru-cache "0.0.x" - hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz" @@ -3418,20 +3400,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -memcached-mock@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/memcached-mock/-/memcached-mock-0.1.0.tgz#3d53d7fbb4ac1e7417c4c363f436b909d3653132" - integrity sha1-PVPX+7SsHnQXxMNj9Da5CdNlMTI= - -memcached-node@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/memcached-node/-/memcached-node-0.1.0.tgz#65a46986ee9c97ec08650399db6325be9b4cf0a5" - integrity sha512-IsHgbJEgNIgtUWVan3Pspz4I1IPAGxKcgr9CyKLsaw7+abTi59qeWL0D356PFGJFKvm1qjnIy7/g+sHxST4kEA== - dependencies: - async "^3.2.0" - hashring "^3.2.0" - node "^13.13.0" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz" @@ -3563,11 +3531,6 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-bin-setup@^1.0.0: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-bin-setup/-/node-bin-setup-1.0.6.tgz#4b5c9bb937ece702d7069b36ca78af4684677528" - integrity sha512-uPIxXNis1CRbv1DwqAxkgBk5NFV3s7cMN/Gf556jSw6jBvV7ca4F9lRL/8cALcZecRibeqU+5dFYqFFmzv5a0Q== - node-cache@^5.0.1: version "5.1.2" resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" @@ -3607,13 +3570,6 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node@^13.13.0: - version "13.14.0" - resolved "https://registry.yarnpkg.com/node/-/node-13.14.0.tgz#2e5afdc3aa66909f1d1d860f54064fbf2720d47c" - integrity sha512-y5pClvPYkG7cTPRDmyNeCuOseyRFuZbrvQVbyAEhkfRWtfhyyu6lHYUjBSJzrSZXlbkCIlx+TKqLDAEhxc9Vkw== - dependencies: - node-bin-setup "^1.0.0" - normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz" @@ -4314,11 +4270,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-lru-cache@0.0.x: - version "0.0.2" - resolved "https://registry.yarnpkg.com/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz#d59cc3a193c1a5d0320f84ee732f6e4713e511dd" - integrity sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0= - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz"