From 67c45936ff83e76b71491c17d20226bbd50c0199 Mon Sep 17 00:00:00 2001 From: Frederik Pytlick Date: Tue, 26 Sep 2023 13:22:58 +0200 Subject: [PATCH] #2 - Added item upload endpoint --- .env.example | 7 +- .github/workflows/test.yaml | 1 + README.md | 29 + package-lock.json | 743 +++++++++++++----- package.json | 4 +- .../20230927083830_item/migration.sql | 35 + prisma/schema.prisma | 55 +- src/locales/da.json | 52 ++ src/locales/en.json | 52 ++ src/modules/auth/__test__/user.test.ts | 2 + src/modules/index.ts | 6 +- .../item/__test__/blob.service.test.ts | 55 ++ .../item/__test__/item.service.test.ts | 35 + src/modules/item/__test__/upload.test.ts | 187 +++++ src/modules/item/blob.service.ts | 70 ++ src/modules/item/index.ts | 13 + src/modules/item/item.controller.ts | 90 +++ src/modules/item/item.route.ts | 25 + src/modules/item/item.schema.ts | 125 +++ src/modules/item/item.service.ts | 16 + src/plugins/config.ts | 5 + src/plugins/index.ts | 2 + src/plugins/jwt.ts | 5 +- src/plugins/plainText.ts | 22 + 24 files changed, 1405 insertions(+), 231 deletions(-) create mode 100644 prisma/migrations/20230927083830_item/migration.sql create mode 100644 src/modules/item/__test__/blob.service.test.ts create mode 100644 src/modules/item/__test__/item.service.test.ts create mode 100644 src/modules/item/__test__/upload.test.ts create mode 100644 src/modules/item/blob.service.ts create mode 100644 src/modules/item/index.ts create mode 100644 src/modules/item/item.controller.ts create mode 100644 src/modules/item/item.route.ts create mode 100644 src/modules/item/item.schema.ts create mode 100644 src/modules/item/item.service.ts create mode 100644 src/plugins/plainText.ts diff --git a/.env.example b/.env.example index dc27aa6..5e4f6e2 100644 --- a/.env.example +++ b/.env.example @@ -24,8 +24,11 @@ DATABASE_URL="${DATABASE_URL_WITHOUT_SCHEMA}/${DATABASE_NAME}" DATABASE_URL_NON_POOLING="${DATABASE_URL_WITHOUT_SCHEMA}/${DATABASE_NAME}" # Redis -REDIS_HOST="redis" +REDIS_HOST="localhost" REDIS_PORT="6379" REDIS_USER="" REDIS_PASSWORD="" -REDIS_URL="rediss://${REDIS_USER}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}" \ No newline at end of file +REDIS_URL="redis://${REDIS_USER}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}" + +# Vercel Blob Storage +BLOB_READ_WRITE_TOKEN="vercel_blob_rw_0511953119b0c1167283c7453a088727" # Fake vercel token \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f5203ff..f0faf39 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,6 +40,7 @@ jobs: --health-timeout 5s --health-retries 5 env: + BLOB_READ_WRITE_TOKEN: 'vercel_blob_rw_0511953119b0c1167283c7453a088727' # Fake vercel token DATABASE_URL: 'postgresql://postgres_user:postgres_password@localhost:5432/postgres_db' DATABASE_URL_NON_POOLING: 'postgresql://postgres_user:postgres_password@localhost:5432/postgres_db' DATABASE_URL_WITHOUT_SCHEMA: 'postgresql://postgres_user:postgres_password@localhost:5432' diff --git a/README.md b/README.md index 220c7d2..82141d3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,35 @@ Deploys the pending migrations to the databases. To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/). +## Setup blob storage connection locally + +### Install & run ngrok + +Head to https://ngrok.com/download and download ngrok for your operating system. + +**Beware:** If you are using WSL on Windows, you can just follow the linux guide and set it up in your WSL. + +Now create a ngrok tunnel to your API: + +```sh +ngrok http 3000 + +# Example using a static domain. This is highly recommended +ngrok http --domain=healthy-monkey-dancing.ngrok-free.app 3000 +``` + +I recommend heading to https://dashboard.ngrok.com/cloud-edge/domains and creating a static domain, so you won't have to update your `.env` file in the web application every time you create a new tunnel. + +### Setup .env files + +#### API + +Set the `BLOB_READ_WRITE_TOKEN` variable to the one from vercel. + +#### Web + +Set the `PUBLIC_API_URL` variable to the ngrok tunnel url. Example: `PUBLIC_API_URL="https://healthy-monkey-dancing.ngrok-free.app/api/"` + ## Generate Ed25519 key pair ```bash diff --git a/package-lock.json b/package-lock.json index 9d7267c..aa5e642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@fastify/swagger": "^8.10.0", "@fastify/swagger-ui": "^1.9.3", "@prisma/client": "^5.3.1", + "@vercel/blob": "^0.12.5", "ajv-errors": "^3.0.0", "bcrypt": "^5.1.1", "dotenv": "^16.3.1", @@ -74,7 +75,6 @@ "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, "dependencies": { "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" @@ -87,7 +87,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -99,7 +98,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -113,7 +111,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -121,14 +118,12 @@ "node_modules/@babel/code-frame/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -137,7 +132,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -146,7 +140,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -359,7 +352,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -391,7 +383,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -405,7 +396,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -417,7 +407,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -431,7 +420,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -439,14 +427,12 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -455,7 +441,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -464,7 +449,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -1235,7 +1219,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -1275,7 +1258,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -1370,7 +1352,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -1452,7 +1433,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -1610,14 +1590,12 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -1626,7 +1604,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -1668,86 +1645,6 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.85.tgz", - "integrity": "sha512-jTikp+i4nO4Ofe6qGm4I3sFeebD1OvueBCHITux5tQKD6umN1c2z4CRGv6K49NIz/qEpUcdr6Qny6K+3yibVFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.85.tgz", - "integrity": "sha512-3uHYkjVU+2F+YbVYtq5rH0uCJIztFTALaS3mQEfQUZKXZ5/8jD5titTCRqFKtSlQg0CzaFZgsYsuqwYBmgN0mA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.85.tgz", - "integrity": "sha512-ouHzAHsFaEOkRuoTAOI/8n2m8BQAAnb4vr/xbMhhDOmix0lp5eNsW5Iac/EcJ2uG6B3n7P2K8oycj9SWkj+pfw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.85.tgz", - "integrity": "sha512-/Z1CZOWiO+NqJEh1J20PIxQFHMH43upQJ1l7FJ5Z7+MyuYF8WkeJ7OSovau729pBR+38vvvccEJrMZIztfv7hQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.85.tgz", - "integrity": "sha512-gfh7CfKavi076dbMBTzfdawSGcYfZ4+1Q+8aRkSesqepKHcIWIJti8Cf3zB4a6CHNhJe+VN0Gb7DEfumydAm1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.3.85", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.85.tgz", @@ -1780,54 +1677,6 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.85.tgz", - "integrity": "sha512-ibckJDZw8kNosciMexwk0z75ZyUhwtiFMV9rSBpup0opa7NNCUCoERCJ1e9LRyMdhsVUoLpZg/KZiHCdTw96hQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.85.tgz", - "integrity": "sha512-hY4MpHGUVQHL1T2kgRXOigDho4DTIpVPYzJ4uyy8VQRbS7GzN5XtvdGP/fA4zp8+2BQjcig+6J7Y92SY15ouNQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.85", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.85.tgz", - "integrity": "sha512-ktxWOMFJ0iqKn6WUHtXqi4CS7xkyHmrRtjllGRuGqxmLmDX/HSOfuQ55Tm1KXKk5oHLacJkUbOSF2kBrpZ8dpg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/jest": { "version": "0.2.29", "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.29.tgz", @@ -1850,6 +1699,14 @@ "integrity": "sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg==", "dev": true }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1936,14 +1793,12 @@ "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -1952,7 +1807,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } @@ -1967,6 +1821,16 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", @@ -1976,8 +1840,7 @@ "node_modules/@types/node": { "version": "20.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", - "dev": true + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" }, "node_modules/@types/semver": { "version": "7.5.2", @@ -1988,8 +1851,7 @@ "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, "node_modules/@types/strip-bom": { "version": "3.0.0", @@ -2003,6 +1865,11 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==" + }, "node_modules/@types/uuid": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", @@ -2013,7 +1880,6 @@ "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", - "dev": true, "dependencies": { "@types/yargs-parser": "*" } @@ -2021,8 +1887,7 @@ "node_modules/@types/yargs-parser": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.0", @@ -2213,6 +2078,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vercel/blob": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-0.12.5.tgz", + "integrity": "sha512-XCsg9W2WrDlQGnw9X38RbM2mtfhTvp4x9lThx4Cr496c0f4IIoMbuwiFd9uvu228D+RcyeopTN7KJAUERCSogw==", + "dependencies": { + "jest-environment-jsdom": "29.7.0", + "undici": "5.24.0" + }, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2238,7 +2120,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2246,6 +2127,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2259,7 +2149,6 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -2353,7 +2242,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2482,6 +2370,11 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -2691,7 +2584,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -2781,6 +2673,17 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2835,7 +2738,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2907,7 +2809,6 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, "funding": [ { "type": "github", @@ -2966,7 +2867,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2977,8 +2877,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-support": { "version": "1.1.3", @@ -2994,6 +2893,17 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3070,6 +2980,71 @@ "node": ">= 8" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -3095,6 +3070,11 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -3153,6 +3133,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -3233,6 +3221,25 @@ "node": ">=6.0.0" } }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -3301,6 +3308,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-schema": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/env-schema/-/env-schema-5.2.0.tgz", @@ -3432,6 +3450,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "8.49.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", @@ -3569,7 +3607,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -3606,7 +3643,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -3615,7 +3651,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3898,7 +3933,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3963,6 +3997,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4003,20 +4050,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -4246,8 +4279,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -4278,7 +4310,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4345,6 +4376,17 @@ "readable-stream": "^3.6.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4366,6 +4408,19 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4387,6 +4442,17 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4664,7 +4730,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -4692,6 +4757,11 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -5087,6 +5157,32 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -5170,7 +5266,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -5190,7 +5285,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -5379,7 +5473,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -5496,6 +5589,81 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5756,7 +5924,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -5989,6 +6156,11 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6166,6 +6338,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6217,7 +6400,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -6417,7 +6599,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -6431,7 +6612,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -6493,6 +6673,11 @@ "node": ">= 0.10" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -6527,6 +6712,11 @@ } ] }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6555,8 +6745,7 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/readable-stream": { "version": "3.6.2", @@ -6649,6 +6838,11 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -6853,6 +7047,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -6965,7 +7170,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -6982,7 +7186,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -7015,7 +7219,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -7027,7 +7230,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, "engines": { "node": ">=8" } @@ -7057,6 +7259,14 @@ "reusify": "^1.0.0" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7178,7 +7388,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7198,6 +7407,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -7286,7 +7500,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -7310,6 +7523,20 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7540,7 +7767,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } @@ -7657,6 +7883,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.24.0.tgz", + "integrity": "sha512-OKlckxBjFl0oXxcj9FU6oB8fDAaiRUq+D8jrFWGmOfI/gIyjk/IeS75LMzgYKUaeHzLUcYvf9bbJGSrUwTfwwQ==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -7695,6 +7940,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7747,6 +8001,17 @@ "node": ">= 0.8" } }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7769,6 +8034,25 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -7869,6 +8153,39 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index bbaf13a..60a9394 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "This project is a fastify template", "main": "src/server.ts", "scripts": { - "test": "jest", + "test": "jest --runInBand", + "test-fast": "jest", "build": "rm -rf build && tsc -p tsconfig.build.json", "vercel-build": "prisma generate && npm run build", "start": "node build/src/server.js", @@ -27,6 +28,7 @@ "@fastify/swagger-ui": "^1.9.3", "@prisma/client": "^5.3.1", "ajv-errors": "^3.0.0", + "@vercel/blob": "^0.12.5", "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "fastify": "^4.23.2", diff --git a/prisma/migrations/20230927083830_item/migration.sql b/prisma/migrations/20230927083830_item/migration.sql new file mode 100644 index 0000000..b1a2995 --- /dev/null +++ b/prisma/migrations/20230927083830_item/migration.sql @@ -0,0 +1,35 @@ +-- CreateTable +CREATE TABLE "Item" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(255) NOT NULL, + "mimeType" VARCHAR(255) NOT NULL, + "blobUrl" VARCHAR(1024) NOT NULL, + "ownerId" INTEGER NOT NULL, + "parentId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "Item_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Item_blobUrl_idx" ON "Item"("blobUrl"); + +-- CreateIndex +CREATE INDEX "Item_ownerId_idx" ON "Item"("ownerId"); + +-- CreateIndex +CREATE INDEX "Item_parentId_idx" ON "Item"("parentId"); + +-- CreateIndex +CREATE INDEX "Item_ownerId_parentId_idx" ON "Item"("ownerId", "parentId"); + +-- CreateIndex +CREATE INDEX "Item_deletedAt_idx" ON "Item"("deletedAt"); + +-- AddForeignKey +ALTER TABLE "Item" ADD CONSTRAINT "Item_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Item" ADD CONSTRAINT "Item_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a8865b5..1bba5ec 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,25 +12,54 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - name String @db.VarChar(50) - email String @unique @db.VarChar(255) - password String @db.Text - sessions UserSession[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + name String @db.VarChar(50) + email String @unique @db.VarChar(255) + password String @db.Text + + sessions UserSession[] + Item Item[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model UserSession { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - refreshToken String @db.VarChar(1024) - tokenFamily String @db.VarChar(36) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + refreshToken String @db.VarChar(1024) + tokenFamily String @db.VarChar(36) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([tokenFamily]) @@index([userId]) @@index([userId, tokenFamily]) } + +model Item { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + mimeType String @db.VarChar(255) + blobUrl String @db.VarChar(1024) + ownerId Int + parentId Int? + + owner User @relation(fields: [ownerId], references: [id]) + + parentItem Item? @relation("ItemToItem", fields: [parentId], references: [id]) + Items Item[] @relation("ItemToItem") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([blobUrl]) + @@index([ownerId]) + @@index([parentId]) + @@index([ownerId, parentId]) + @@index([deletedAt]) +} diff --git a/src/locales/da.json b/src/locales/da.json index 7a56589..259d922 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -23,5 +23,57 @@ "refreshToken": { "expired": "Refresh token er udløbet", "used": "Refresh token er allerede blevet brugt" + }, + "blob": { + "type": { + "enum": "Type skal være en af følgende: 'blob.generate-client-token' eller 'blob.upload-completed'", + "type": "Type skal være en tekst", + "required": "Type er påkrævet" + }, + "payload": { + "type": "Payload skal være et objekt", + "required": "Payload er påkrævet", + "pathname": { + "type": "Pathname skal være en tekst" + }, + "callbackUrl": { + "type": "CallbackURL skal være en tekst" + }, + "clientPayload": { + "type": "ClientPayload skal være en tekst" + }, + "tokenPayload": { + "type": "TokenPayload skal være en tekst" + }, + "blob": { + "type": "Blob skal være et objekt", + "url": { + "type": "URL skal være en tekst", + "required": "URL er påkrævet" + }, + "pathname": { + "type": "Pathname skal være en tekst", + "required": "Pathname er påkrævet" + }, + "contentType": { + "type": "ContentType skal være en tekst", + "required": "ContentType er påkrævet" + }, + "contentDisposition": { + "type": "ContentDisposition skal være en tekst", + "required": "ContentDisposition er påkrævet" + } + } + } + }, + "item": { + "upload": { + "clientPayload": { + "required": "clientPayload er påkrævet", + "parentId": { + "required": "clientPayload.parentId er påkrævet" + } + } + } } } diff --git a/src/locales/en.json b/src/locales/en.json index b2289d6..a5ccc9f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -23,5 +23,57 @@ "refreshToken": { "expired": "Refresh token has reached absolute expiry", "used": "Refresh token has already been used" + }, + "blob": { + "type": { + "enum": "Type must be one of the following: 'blob.generate-client-token' or 'blob.upload-completed'", + "type": "Type must be a string", + "required": "Type is required" + }, + "payload": { + "type": "Payload must be an object", + "required": "Payload is required", + "pathname": { + "type": "Pathname must be a string" + }, + "callbackUrl": { + "type": "CallbackURL must be a string" + }, + "clientPayload": { + "type": "ClientPayload must be a string" + }, + "tokenPayload": { + "type": "TokenPayload must be a string" + }, + "blob": { + "type": "Blob must be an object", + "url": { + "type": "URL must be a string", + "required": "URL is required" + }, + "pathname": { + "type": "Pathname must be a string", + "required": "Pathname is required" + }, + "contentType": { + "type": "ContentType must be a string", + "required": "ContentType is required" + }, + "contentDisposition": { + "type": "ContentDisposition must be a string", + "required": "ContentDisposition is required" + } + } + } + }, + "item": { + "upload": { + "clientPayload": { + "required": "clientPayload is required", + "parentId": { + "required": "clientPayload.parentId is required" + } + } + } } } diff --git a/src/modules/auth/__test__/user.test.ts b/src/modules/auth/__test__/user.test.ts index c80e904..9b8bd80 100644 --- a/src/modules/auth/__test__/user.test.ts +++ b/src/modules/auth/__test__/user.test.ts @@ -3,6 +3,7 @@ import { jwt } from '../../../plugins/jwt'; import TimeUtil from '../../../utils/time'; import UserService from '../user.service'; import AuthService from '../auth.service'; +import { v4 } from 'uuid'; describe('GET /api/auth/user', () => { let userService: UserService; @@ -49,6 +50,7 @@ describe('GET /api/auth/user', () => { jwt.signAccessToken({ sub: 542, iat: TimeUtil.getNowUnixTimeStamp(), + tokenFamily: v4(), }), }, }); diff --git a/src/modules/index.ts b/src/modules/index.ts index e376036..43a1ab1 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -2,6 +2,7 @@ import { FastifyInstance, FastifyPluginOptions } from 'fastify'; import fastifyPlugin from 'fastify-plugin'; import auth from './auth'; +import item from './item'; const getOptionsWithPrefix = (options: FastifyPluginOptions, prefix: string) => { return { @@ -15,5 +16,8 @@ export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPl return { status: 'OK' }; }); - await Promise.all([fastify.register(auth, getOptionsWithPrefix(options, '/auth'))]); + await Promise.all([ + fastify.register(auth, getOptionsWithPrefix(options, '/auth')), + fastify.register(item, getOptionsWithPrefix(options, '/item')), + ]); }); diff --git a/src/modules/item/__test__/blob.service.test.ts b/src/modules/item/__test__/blob.service.test.ts new file mode 100644 index 0000000..26ee582 --- /dev/null +++ b/src/modules/item/__test__/blob.service.test.ts @@ -0,0 +1,55 @@ +import { User } from '@prisma/client'; +import UserService from '../../auth/user.service'; +import BlobService from '../blob.service'; +import { FastifyRequest } from 'fastify'; +import AuthService from '../../auth/auth.service'; +import { HandleUploadBody } from '@vercel/blob/client'; + +describe('BlobService', () => { + let blobService: BlobService; + let userService: UserService; + let authService: AuthService; + + let user: User; + + beforeAll(async () => { + blobService = new BlobService(); + userService = new UserService(); + authService = new AuthService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + }); + + describe('handleUpload()', () => { + it('should return undefined tokenPayload if no formatTokenPayload callback is passed', async () => { + const { accessToken } = await authService.createTokens(user.id); + + await blobService.handleUpload( + { + body: { + type: 'blob.generate-client-token', + payload: { + callbackUrl: 'https://example.com/api/item', + clientPayload: JSON.stringify({ parentId: null }), + pathname: 'test.txt', + }, + } as HandleUploadBody, + headers: { + authorization: 'Bearer ' + accessToken, + }, + raw: { + url: 'https://example.com/test-ihufsdihudsfuds.txt', + }, + } as unknown as FastifyRequest, + ['text/plain'], + async ({ tokenPayload }) => { + expect(tokenPayload).toBeUndefined(); + }, + ); + }); + }); +}); diff --git a/src/modules/item/__test__/item.service.test.ts b/src/modules/item/__test__/item.service.test.ts new file mode 100644 index 0000000..9c130f4 --- /dev/null +++ b/src/modules/item/__test__/item.service.test.ts @@ -0,0 +1,35 @@ +import { User } from '@prisma/client'; +import UserService from '../../auth/user.service'; +import ItemService from '../item.service'; + +describe('ItemService', () => { + let itemService: ItemService; + let userService: UserService; + + let user: User; + + beforeAll(async () => { + itemService = new ItemService(); + userService = new UserService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + }); + + describe('createItem()', () => { + it('should not throw exceptions', async () => { + expect( + itemService.createItem({ + name: 'test', + mimeType: 'text/plain', + blobUrl: 'https://example.com/test-ihufsdihudsfuds.txt', + ownerId: user.id, + parentId: null, + }), + ).resolves.not.toThrow(); + }); + }); +}); diff --git a/src/modules/item/__test__/upload.test.ts b/src/modules/item/__test__/upload.test.ts new file mode 100644 index 0000000..7f23743 --- /dev/null +++ b/src/modules/item/__test__/upload.test.ts @@ -0,0 +1,187 @@ +import { User } from '@prisma/client'; +import UserService from '../../auth/user.service'; +import AuthService from '../../auth/auth.service'; + +describe('POST /api/item', () => { + let userService: UserService; + let authService: AuthService; + + let user: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + }); + + /* + * Generate client token tests + */ + + it('should return status 200 and return a new clientToken', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/item', + headers: { + 'content-type': 'text/plain', + authorization: 'Bearer ' + accessToken, + }, + payload: { + type: 'blob.generate-client-token', + payload: { + callbackUrl: 'https://example.com/api/item', + clientPayload: JSON.stringify({ parentId: null }), + pathname: 'test.txt', + }, + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + type: 'blob.generate-client-token', + clientToken: expect.any(String), + }); + }); + + it('should return status 401, when unauthorized', async () => { + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/item', + headers: { + 'content-type': 'text/plain', + authorization: 'invalid_access_token!!!', + }, + payload: { + type: 'blob.generate-client-token', + payload: { + callbackUrl: 'https://example.com/api/item', + clientPayload: JSON.stringify({ parentId: null }), + pathname: 'test.txt', + }, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'Unauthorized', + message: 'Unauthorized', + statusCode: 401, + }); + }); + + it('should return status 400, when clientPayload is missing', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/item', + headers: { + 'content-type': 'text/plain', + authorization: 'Bearer ' + accessToken, + }, + payload: { + type: 'blob.generate-client-token', + payload: { + callbackUrl: 'https://example.com/api/item', + pathname: 'test.txt', + }, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'Bad Request', + message: 'clientPayload is required', + statusCode: 400, + }); + }); + + it('should return status 400, when clientPayload.parentId is missing', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/item', + headers: { + 'content-type': 'text/plain', + authorization: 'Bearer ' + accessToken, + }, + payload: { + type: 'blob.generate-client-token', + payload: { + callbackUrl: 'https://example.com/api/item', + clientPayload: JSON.stringify({}), + pathname: 'test.txt', + }, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'Bad Request', + message: 'clientPayload.parentId is required', + statusCode: 400, + }); + }); + + it('should return status 400, when providing invalid json string as payload', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/item', + headers: { + 'content-type': 'text/plain', + authorization: 'Bearer ' + accessToken, + }, + payload: 'Invalid json', + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'Bad Request', + message: expect.stringContaining('Unexpected token'), + statusCode: 400, + }); + }); + + /* + * Upload completed callback tests + */ + + it('should return status 400, when called without valid "x-vercel-signature" header', async () => { + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/item', + headers: { + 'content-type': 'text/plain', + }, + payload: { + type: 'blob.upload-completed', + payload: { + blob: { + url: 'https://example.com/test-ihufsdihudsfuds.txt', + pathname: 'test.txt', + contentType: 'text/plain', + contentDisposition: 'attachment; filename="test.txt"', + }, + tokenPayload: JSON.stringify({ parentId: null, ownerId: user.id }), + }, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'Bad Request', + message: 'Vercel Blob: Missing callback signature', + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/blob.service.ts b/src/modules/item/blob.service.ts new file mode 100644 index 0000000..b13e3c7 --- /dev/null +++ b/src/modules/item/blob.service.ts @@ -0,0 +1,70 @@ +import { HeadBlobResult, PutBlobResult, del, head } from '@vercel/blob'; +import { HandleUploadBody, handleUpload } from '@vercel/blob/client'; +import { accessTokenPayload, jwt } from '../../plugins/jwt'; +import { FastifyRequest } from 'fastify'; + +type OnUploadCompletedCallback = (body: { + blob: PutBlobResult; + tokenPayload?: string | undefined; +}) => Promise; + +type FormatTokenPayloadCallback = ( + clientPayload: string | undefined, + accessTokenPayload: accessTokenPayload, +) => FormatTokenPayloadCallbackReturn; +type FormatTokenPayloadCallbackReturn = string | undefined | Promise; + +type BlobGenerateTokenResponse = { + type: 'blob.generate-client-token'; + clientToken: string; +}; +type BlobUploadedCompletedResponse = { + type: 'blob.upload-completed'; + response: 'ok'; +}; + +export default class BlobService { + /* istanbul ignore next */ + public async deleteBlob(url: string | string[]): Promise { + await del(url); + } + + /* istanbul ignore next */ + public async getBlobMetaData(url: string): Promise { + return await head(url); + } + + public async handleUpload( + request: FastifyRequest, + allowedContentTypes: string[], + onUploadCompleted: OnUploadCompletedCallback, + formatTokenPayload?: FormatTokenPayloadCallback, + ): Promise { + return await handleUpload({ + body: request.body as HandleUploadBody, + request: request.raw, + onBeforeGenerateToken: async (pathname, clientPayload) => { + const accessToken = + request.headers.authorization?.replace('Bearer ', '') ?? /* istanbul ignore next */ ''; + + // You should not be able to upload files without being signed in + try { + jwt.verify(accessToken); + } catch (error) { + throw new Error('Unauthorized'); + } + + const accessTokenPayload = jwt.decodeAccessToken(accessToken); + + return { + allowedContentTypes, + // Use the provided formatTokenPayload callback to format the token payload. If no callback is provided, the token payload will be undefined. + tokenPayload: formatTokenPayload + ? await formatTokenPayload(clientPayload, accessTokenPayload) + : undefined, + }; + }, + onUploadCompleted, + }); + } +} diff --git a/src/modules/item/index.ts b/src/modules/item/index.ts new file mode 100644 index 0000000..dfad8b9 --- /dev/null +++ b/src/modules/item/index.ts @@ -0,0 +1,13 @@ +import { FastifyInstance, FastifyPluginOptions } from 'fastify'; + +import fastifyPlugin from 'fastify-plugin'; +import itemRoute from './item.route'; +import { itemSchemas } from './item.schema'; + +export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => { + for (const schema of itemSchemas) { + fastify.addSchema(schema); + } + + await fastify.register(itemRoute, options); +}); diff --git a/src/modules/item/item.controller.ts b/src/modules/item/item.controller.ts new file mode 100644 index 0000000..34ad35c --- /dev/null +++ b/src/modules/item/item.controller.ts @@ -0,0 +1,90 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import { UploadInput } from './item.schema'; +import BlobService from './blob.service'; +import ItemService from './item.service'; + +export default class ItemController { + private itemService: ItemService; + private blobService: BlobService; + + constructor(itemService: ItemService, blobService: BlobService) { + this.blobService = blobService; + this.itemService = itemService; + } + + public async uploadHandler( + request: FastifyRequest<{ + Body: UploadInput; + }>, + reply: FastifyReply, + ) { + try { + const jsonResponse = await this.blobService.handleUpload( + request, + ['text/plain'], + /* istanbul ignore next */ // Sadly this is not testable, beacuse the function is normally called by the blob service, with generated data + async ({ blob, tokenPayload }) => { + try { + if (!tokenPayload) { + request.log.error( + "Vercel blob storage didn't pass a token payload!", + blob, + tokenPayload, + ); + throw new Error('Unauthorized'); + } + + const tokenPayloadObject = JSON.parse(tokenPayload); + if (!tokenPayloadObject.ownerId) { + request.log.error( + "Vercel blob storage didn't pass a valid token payload! ownerId is missing!", + blob, + tokenPayload, + ); + throw new Error('Unauthorized'); + } + + await this.itemService.createItem({ + name: blob.pathname, + mimeType: blob.contentType, + blobUrl: blob.url, + ownerId: tokenPayloadObject.ownerId, + parentId: tokenPayloadObject.parentId ?? null, + }); + } catch (e) { + request.log.error(e); + await this.blobService.deleteBlob(blob.url); + } + }, + async (clientPayload, accessTokenPayload) => { + if (!clientPayload) { + throw new Error(request.i18n.t('item.upload.clientPayload.required')); + } + + const clientPayloadObject = JSON.parse(clientPayload); + if (clientPayloadObject.parentId === undefined) { + throw new Error(request.i18n.t('item.upload.clientPayload.parentId.required')); + } + + return JSON.stringify({ + parentId: clientPayloadObject.parentId, + ownerId: accessTokenPayload.sub, + }); + }, + ); + + return reply.code(200).send(jsonResponse); + } catch (e) { + if (e instanceof Error) { + if (e.message === 'Unauthorized') { + return reply.unauthorized(); + } + + return reply.badRequest(e.message); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } +} diff --git a/src/modules/item/item.route.ts b/src/modules/item/item.route.ts new file mode 100644 index 0000000..d344223 --- /dev/null +++ b/src/modules/item/item.route.ts @@ -0,0 +1,25 @@ +import { FastifyInstance } from 'fastify'; +import ItemController from './item.controller'; +import ItemService from './item.service'; +import BlobService from './blob.service'; + +export default async (fastify: FastifyInstance) => { + const itemController = new ItemController(new ItemService(), new BlobService()); + + fastify.post( + '/', + { + schema: { + headers: { + Authorization: true, + }, + tags: ['Item'], + body: { $ref: 'uploadItemSchema' }, + response: { + 200: { $ref: 'uploadItemResponseSchema' }, + }, + }, + }, + itemController.uploadHandler.bind(itemController), + ); +}; diff --git a/src/modules/item/item.schema.ts b/src/modules/item/item.schema.ts new file mode 100644 index 0000000..e6318c8 --- /dev/null +++ b/src/modules/item/item.schema.ts @@ -0,0 +1,125 @@ +import { FromSchema } from 'json-schema-to-ts'; + +const uploadItemSchema = { + $id: 'uploadItemSchema', + type: 'object', + properties: { + type: { + type: 'string', + enum: ['blob.generate-client-token', 'blob.upload-completed'], + errorMessage: { + enum: 'blob.type.enum', + type: 'blob.type.type', + }, + }, + payload: { + type: 'object', + properties: { + pathname: { + type: 'string', + errorMessage: { + type: 'blob.payload.pathname.type', + }, + }, + callbackUrl: { + type: 'string', + errorMessage: { + type: 'blob.payload.callbackUrl.type', + }, + }, + clientPayload: { + type: 'string', + errorMessage: { + type: 'blob.payload.clientPayload.type', + }, + }, + tokenPayload: { + type: 'string', + errorMessage: { + type: 'blob.payload.tokenPayload.type', + }, + }, + blob: { + type: 'object', + properties: { + url: { + type: 'string', + errorMessage: { + type: 'blob.payload.blob.url.type', + }, + }, + pathname: { + type: 'string', + errorMessage: { + type: 'blob.payload.blob.pathname.type', + }, + }, + contentType: { + type: 'string', + errorMessage: { + type: 'blob.payload.blob.contentType.type', + }, + }, + contentDisposition: { + type: 'string', + errorMessage: { + type: 'blob.payload.blob.contentDisposition.type', + }, + }, + }, + required: ['url', 'pathname', 'contentType', 'contentDisposition'], + errorMessage: { + type: 'blob.payload.blob.type', + required: { + url: 'blob.payload.blob.url.required', + pathname: 'blob.payload.blob.pathname.required', + contentType: 'blob.payload.blob.contentType.required', + contentDisposition: 'blob.payload.blob.contentDisposition.required', + }, + }, + }, + }, + errorMessage: { + type: 'blob.payload.type', + }, + }, + }, + required: ['type', 'payload'], + errorMessage: { + required: { + type: 'blob.type.required', + payload: 'blob.payload.required', + }, + }, +} as const; + +const uploadItemResponseSchema = { + $id: 'uploadItemResponseSchema', + type: 'object', + properties: { + type: { + type: 'string', + enum: ['blob.generate-client-token', 'blob.upload-completed'], + }, + response: { + type: 'string', + enum: ['ok'], + }, + clientToken: { + type: 'string', + }, + }, + required: ['type'], +} as const; + +export type UploadInput = FromSchema; + +export type CreateItem = { + name: string; + mimeType: string; + blobUrl: string; + ownerId: number; + parentId: number | null; +}; + +export const itemSchemas = [uploadItemSchema, uploadItemResponseSchema]; diff --git a/src/modules/item/item.service.ts b/src/modules/item/item.service.ts new file mode 100644 index 0000000..d7c89db --- /dev/null +++ b/src/modules/item/item.service.ts @@ -0,0 +1,16 @@ +import { prisma } from '../../plugins/prisma'; +import { CreateItem } from './item.schema'; + +export default class ItemService { + public async createItem(input: CreateItem) { + await prisma.item.create({ + data: { + name: input.name, + mimeType: input.mimeType, + blobUrl: input.blobUrl, + ownerId: input.ownerId, + parentId: input.parentId, + }, + }); + } +} diff --git a/src/plugins/config.ts b/src/plugins/config.ts index 182fdab..ec6ed9c 100644 --- a/src/plugins/config.ts +++ b/src/plugins/config.ts @@ -15,6 +15,7 @@ declare module 'fastify' { SECRET: string; PRIVATE: string; PUBLIC: string; + BLOB_READ_WRITE_TOKEN: string; HOST: string; PORT: number; DATABASE_URL: string; @@ -41,6 +42,7 @@ export default fastifyPlugin( 'DATABASE_URL', 'DATABASE_URL_NON_POOLING', 'REDIS_URL', + 'BLOB_READ_WRITE_TOKEN', ], properties: { SECRET: { @@ -52,6 +54,9 @@ export default fastifyPlugin( PUBLIC: { type: 'string', }, + BLOB_READ_WRITE_TOKEN: { + type: 'string', + }, HOST: { type: 'string', default: '0.0.0.0', diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 4849836..2366e02 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -11,6 +11,7 @@ import cors from './cors'; import jwt from './jwt'; import i18n from './i18n'; import errorHandler from './error.handler'; +import plainText from './plainText'; export default fastifyPlugin(async (fastify: FastifyInstance) => { await Promise.all([ @@ -18,6 +19,7 @@ export default fastifyPlugin(async (fastify: FastifyInstance) => { fastify.register(sensible), fastify.register(i18n), fastify.register(errorHandler), + fastify.register(plainText), ]); await Promise.all([ diff --git a/src/plugins/jwt.ts b/src/plugins/jwt.ts index 39aec49..481ec9d 100644 --- a/src/plugins/jwt.ts +++ b/src/plugins/jwt.ts @@ -28,7 +28,10 @@ declare module 'fastify' { jwt: JWT; } interface FastifyInstance { - authenticate(request: FastifyRequest, reply: FastifyReply): Promise; + authenticate( + request: T, + reply: U, + ): Promise; } } diff --git a/src/plugins/plainText.ts b/src/plugins/plainText.ts new file mode 100644 index 0000000..d55a487 --- /dev/null +++ b/src/plugins/plainText.ts @@ -0,0 +1,22 @@ +import fastifyPlugin from 'fastify-plugin'; +import { FastifyInstance } from 'fastify'; + +export default fastifyPlugin( + async (fastify: FastifyInstance) => { + fastify.addContentTypeParser('text/plain', { parseAs: 'string' }, (req, body: string, done) => { + try { + const json = JSON.parse(body); + + return done(null, json); + } catch (e) { + if (e instanceof Error) { + return done(fastify.httpErrors.badRequest(e.message), null); + } + + /* istanbul ignore next */ + return done(null, null); + } + }); + }, + { name: 'plainText' }, +);