From 850f741077ace596095658ab9b31cf7f0f4f9aec Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:13:10 -0700 Subject: [PATCH] Tests Closes #5 --- .github/workflows/ci.yml | 57 ++ ai-providers/open-ai.ts | 6 +- index.d.ts | 1 + lib/generator.ts | 8 +- package-lock.json | 1341 ++++++++++++++++++++++++++++++- package.json | 14 +- plugins/api.ts | 2 +- plugins/auth.ts | 4 +- tests/e2e/api.test.ts | 150 ++++ tests/e2e/auth.test.ts | 51 ++ tests/e2e/index.ts | 7 + tests/e2e/rate-limiting.test.ts | 133 +++ tests/types/README.md | 2 + tests/types/fastify.test-d.ts | 37 + tests/types/schema.test-d.ts | 91 +++ tests/unit/.gitignore | 1 + tests/unit/ai-providers.test.ts | 41 + tests/unit/generator.test.ts | 165 ++++ tests/unit/index.ts | 6 + tests/utils/auth.ts | 19 + tests/utils/mocks.ts | 190 +++++ tests/utils/stackable.ts | 35 + 22 files changed, 2346 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/e2e/api.test.ts create mode 100644 tests/e2e/auth.test.ts create mode 100644 tests/e2e/index.ts create mode 100644 tests/e2e/rate-limiting.test.ts create mode 100644 tests/types/README.md create mode 100644 tests/types/fastify.test-d.ts create mode 100644 tests/types/schema.test-d.ts create mode 100644 tests/unit/.gitignore create mode 100644 tests/unit/ai-providers.test.ts create mode 100644 tests/unit/generator.test.ts create mode 100644 tests/unit/index.ts create mode 100644 tests/utils/auth.ts create mode 100644 tests/utils/mocks.ts create mode 100644 tests/utils/stackable.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..77e82eb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + setup-node-modules: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Git Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install Dependencies + run: npm install + + lint: + name: Linting + runs-on: ubuntu-latest + needs: setup-node-modules + steps: + - name: Git Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - name: Run Linting + run: npm run lint + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + needs: setup-node-modules + steps: + - name: Git Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - name: Run Tests + run: npm run test:unit + + e2e-tests: + name: E2E Tests + runs-on: ubuntu-latest + needs: setup-node-modules + steps: + - name: Git Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - name: Run Tests + run: npm run test:e2e diff --git a/ai-providers/open-ai.ts b/ai-providers/open-ai.ts index e27508b..3c16bfe 100644 --- a/ai-providers/open-ai.ts +++ b/ai-providers/open-ai.ts @@ -2,6 +2,7 @@ import { ReadableStream, UnderlyingByteSource, ReadableByteStreamController } fr import OpenAI from 'openai' import { AiProvider, NoContentError, StreamChunkCallback } from './provider' import { ReadableStream as ReadableStreamPolyfill } from 'web-streams-polyfill' +import { fetch } from 'undici' import { ChatCompletionChunk } from 'openai/resources/index.mjs' import { AiStreamEvent, encodeEvent } from './event' import createError from '@fastify/error' @@ -86,7 +87,8 @@ export class OpenAiProvider implements AiProvider { constructor (model: string, apiKey: string) { this.model = model - this.client = new OpenAI({ apiKey }) + // @ts-expect-error + this.client = new OpenAI({ apiKey, fetch }) } async ask (prompt: string): Promise { @@ -118,6 +120,6 @@ export class OpenAiProvider implements AiProvider { ], stream: true }) - return new ReadableStream(new OpenAiByteSource(response.toReadableStream())) + return new ReadableStream(new OpenAiByteSource(response.toReadableStream(), chunkCallback)) } } diff --git a/index.d.ts b/index.d.ts index 24c1cdd..eccbaf6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,6 @@ import { ReadableStream } from 'node:stream/web' import { PlatformaticApp } from '@platformatic/service' +import { errorResponseBuilderContext } from '@fastify/rate-limit' import { AiWarpConfig } from './config' declare module 'fastify' { diff --git a/lib/generator.ts b/lib/generator.ts index 6b72435..ff3c0fd 100644 --- a/lib/generator.ts +++ b/lib/generator.ts @@ -18,7 +18,11 @@ class AiWarpGenerator extends ServiceGenerator { const defaultBaseConfig = super.getDefaultConfig() const defaultConfig = { aiProvider: 'openai', - aiModel: 'gpt-3.5-turbo' + aiModel: 'gpt-3.5-turbo', + // TODO: temporary fix, when running the typescript files directly + // (in tests) this goes a directory above the actual project. Exposing + // temporarily until I come up with something better + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json') } return Object.assign({}, defaultBaseConfig, defaultConfig) } @@ -132,7 +136,7 @@ class AiWarpGenerator extends ServiceGenerator { async getStackablePackageJson (): Promise { if (this._packageJson == null) { - const packageJsonPath = join(__dirname, '..', '..', 'package.json') + const packageJsonPath = this.config.aiWarpPackageJsonPath const packageJsonFile = await readFile(packageJsonPath, 'utf8') const packageJson: Partial = JSON.parse(packageJsonFile) diff --git a/package-lock.json b/package-lock.json index 276feb2..ff0e581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,15 @@ "@platformatic/config": "^1.24.0", "@platformatic/generators": "^1.24.0", "@platformatic/service": "^1.24.0", + "@reporters/github": "^1.7.0", "fast-json-stringify": "^5.13.0", "fastify-user": "^0.3.3", "json-schema-to-typescript": "^13.0.0", "openai": "^4.28.4", "snazzy": "^9.0.0", - "ts-standard": "^12.0.2" + "ts-standard": "^12.0.2", + "tsd": "^0.30.7", + "tsx": "^4.7.1" }, "bin": { "create-ai-warp": "dist/cli/create.js", @@ -42,6 +45,35 @@ "node": ">=0.10.0" } }, + "node_modules/@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz", + "integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/http-client/node_modules/undici": { + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.5.4", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.5.4.tgz", @@ -58,6 +90,104 @@ "url": "https://github.com/sponsors/philsturgeon" } }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "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==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "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==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "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==" + }, + "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==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@bcherny/json-schema-ref-parser": { "version": "10.0.5-fork", "resolved": "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz", @@ -75,6 +205,351 @@ "url": "https://github.com/sponsors/philsturgeon" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -404,6 +879,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas/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==" + }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", @@ -882,6 +1373,15 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@reporters/github": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.7.0.tgz", + "integrity": "sha512-5velzo4PWkfTqvZD2Ins5Wg38KSjPsWofS4jKTw61IjL4z2vLFGx2KL2Aqpa7bTOo3NYtc7w0zbyaHsBmZtCxQ==", + "dependencies": { + "@actions/core": "^1.10.0", + "stack-utils": "^2.0.6" + } + }, "node_modules/@scalar/fastify-api-reference": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@scalar/fastify-api-reference/-/fastify-api-reference-1.19.2.tgz", @@ -917,6 +1417,28 @@ "integrity": "sha512-5Lrwo7VOiWEBJBhHmqNmf3TPB9ll8gcEshvYJyAIJyCZ2PF48MFOtiDHJNj8+FsNcqImaQYmxVkKBCBlyAa/wg==", "peer": true }, + "node_modules/@tsd/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-CQlfzol0ldaU+ftWuG52vH29uRoKboLinLy84wS8TQOu+m+tWoaUfk4svL4ij2V8M5284KymJBlHUusKj6k34w==", + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -946,6 +1468,11 @@ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" + }, "node_modules/@types/node": { "version": "20.11.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", @@ -963,6 +1490,11 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" + }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -1280,6 +1812,31 @@ } } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -1490,6 +2047,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -1723,6 +2288,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -1991,6 +2580,37 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2068,6 +2688,14 @@ "resolved": "https://registry.npmjs.org/desm/-/desm-1.3.1.tgz", "integrity": "sha512-vgTAOosB1aHrmzjGnzFCbjvXbk8QAOC/36JxJhcBkeAuUy8QwRFxAWBHemiDpUB3cbrBruFUdzpUS21aocvaWg==" }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/digest-fetch": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", @@ -2369,6 +2997,43 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/escape-goat": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", @@ -2515,6 +3180,82 @@ "typescript": "*" } }, + "node_modules/eslint-formatter-pretty": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", + "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "dependencies": { + "@types/eslint": "^7.2.13", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -2755,6 +3496,11 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==" + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -3395,6 +4141,19 @@ "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==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3505,6 +4264,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/ghtml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ghtml/-/ghtml-1.0.1.tgz", @@ -3638,6 +4408,14 @@ "graphql": ">=15" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "engines": { + "node": ">=6" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3736,6 +4514,28 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3860,6 +4660,14 @@ "node": ">= 0.10" } }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -4090,6 +4898,14 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -4279,16 +5095,67 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/joycon": { @@ -4325,6 +5192,11 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-ref-resolver": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", @@ -4474,6 +5346,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4504,6 +5384,11 @@ "set-cookie-parser": "^2.4.1" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, "node_modules/load-json-file": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", @@ -4596,6 +5481,17 @@ "es5-ext": "~0.10.2" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -4621,6 +5517,42 @@ "timers-ext": "^0.1.7" } }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mercurius": { "version": "13.4.0", "resolved": "https://registry.npmjs.org/mercurius/-/mercurius-13.4.0.tgz", @@ -4725,6 +5657,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -4754,6 +5694,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", @@ -4868,6 +5821,20 @@ } } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -5258,6 +6225,11 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5427,6 +6399,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -5457,6 +6443,35 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/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==" + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -5590,11 +6605,154 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "engines": { + "node": ">=8" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "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" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "engines": { + "node": ">=8" + } + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -5618,6 +6776,18 @@ "node": ">= 12.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", @@ -5698,6 +6868,14 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -6128,6 +7306,34 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -6141,6 +7347,25 @@ "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz", "integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/standard-engine": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", @@ -6492,6 +7717,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6514,6 +7750,18 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -6627,6 +7875,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "engines": { + "node": ">=8" + } + }, "node_modules/ts-standard": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/ts-standard/-/ts-standard-12.0.2.tgz", @@ -6677,6 +7933,26 @@ "json5": "lib/cli.js" } }, + "node_modules/tsd": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.30.7.tgz", + "integrity": "sha512-oTiJ28D6B/KXoU3ww/Eji+xqHJojiuPVMwA12g4KYX1O72N93Nb6P3P3h2OAhhf92Xl8NIhb/xFmBZd5zw/xUw==", + "dependencies": { + "@tsd/typescript": "~5.3.3", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "jest-diff": "^29.0.3", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + }, + "bin": { + "tsd": "dist/cli.js" + }, + "engines": { + "node": ">=14.16" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -6701,6 +7977,32 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/tsx": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", + "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==", + "dependencies": { + "esbuild": "~0.19.10", + "get-tsconfig": "^4.7.2" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", @@ -6876,6 +8178,23 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/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" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -7139,6 +8458,14 @@ "node": ">= 14" } }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a4c350c..a4c5c63 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,11 @@ "build:config": "node ./dist/lib/schema.js | json2ts > config.d.ts", "clean": "rm -fr ./dist", "lint": "ts-standard | snazzy", - "lint:fix": "ts-standard --fix | snazzy" + "lint:fix": "ts-standard --fix | snazzy", + "test": "npm run test:unit && npm run test:e2e && npm run test:types", + "test:unit": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/unit/index.ts", + "test:e2e": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/e2e/index.ts", + "test:types": "tsd" }, "engines": { "node": "^18.8.0 || >=20.6.0" @@ -28,14 +32,20 @@ "@platformatic/config": "^1.24.0", "@platformatic/generators": "^1.24.0", "@platformatic/service": "^1.24.0", + "@reporters/github": "^1.7.0", "fast-json-stringify": "^5.13.0", "fastify-user": "^0.3.3", "json-schema-to-typescript": "^13.0.0", "openai": "^4.28.4", "snazzy": "^9.0.0", - "ts-standard": "^12.0.2" + "ts-standard": "^12.0.2", + "tsd": "^0.30.7", + "tsx": "^4.7.1" }, "overrides": { "minimatch": "^5.0.0" + }, + "tsd": { + "directory": "tests/types" } } diff --git a/plugins/api.ts b/plugins/api.ts index c542314..2de4b91 100644 --- a/plugins/api.ts +++ b/plugins/api.ts @@ -50,7 +50,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { url: '/api/v1/stream', method: 'POST', schema: { - produces: ['text/event-stream; charset=utf-16'], + produces: ['text/event-stream'], body: Type.Object({ prompt: Type.String() }) diff --git a/plugins/auth.ts b/plugins/auth.ts index 0e6e1b7..b740d5f 100644 --- a/plugins/auth.ts +++ b/plugins/auth.ts @@ -12,7 +12,9 @@ export default fastifyPlugin(async (fastify: FastifyInstance) => { fastify.addHook('preHandler', async (request) => { await request.extractUser() - if (config.auth?.required !== undefined && config.auth?.required && request.user === undefined) { + const isAuthRequired = config.auth?.required !== undefined && config.auth?.required + const isMissingUser = request.user === undefined || request.user === null + if (isAuthRequired && isMissingUser) { throw new UnauthorizedError() } }) diff --git a/tests/e2e/api.test.ts b/tests/e2e/api.test.ts new file mode 100644 index 0000000..b43c74e --- /dev/null +++ b/tests/e2e/api.test.ts @@ -0,0 +1,150 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { before, after, describe, it } from 'node:test' +import assert from 'node:assert' +import { FastifyInstance } from 'fastify' +import fastifyPlugin from 'fastify-plugin' +import { MOCK_CONTENT_RESPONSE, buildExpectedStreamBodyString } from '../utils/mocks' +import { AiWarpConfig } from '../../config' +import { buildAiWarpApp } from '../utils/stackable' + +const expectedStreamBody = buildExpectedStreamBodyString() + +interface Provider { + name: string + config: AiWarpConfig['aiProvider'] +} + +const providers: Provider[] = [ + { + name: 'OpenAI', + config: { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } + } + }, + { + name: 'Mistral', + config: { + mistral: { + model: 'mistral-tiny', + apiKey: '' + } + } + } +] + +// Test the prompt and stream endpoint for each provider +for (const { name, config } of providers) { + describe(name, () => { + let app: FastifyInstance + let port: number + let chunkCallbackCalled = false + before(async () => { + [app, port] = await buildAiWarpApp({ aiProvider: config }) + + await app.register(fastifyPlugin(async () => { + app.ai.preResponseChunkCallback = (_, response) => { + chunkCallbackCalled = true + return response + } + })) + + await app.start() + }) + + after(async () => { + await app.close() + }) + + it('/api/v1/prompt returns expected response', async () => { + const res = await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) + assert.strictEqual(res.headers.get('content-type'), 'application/json; charset=utf-8') + + const body = await res.json() + assert.strictEqual(body.response, MOCK_CONTENT_RESPONSE) + }) + + it('/api/v1/stream returns expected response', async () => { + assert.strictEqual(chunkCallbackCalled, false) + + const res = await fetch(`http://localhost:${port}/api/v1/stream`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) + assert.strictEqual(res.headers.get('content-type'), 'text/event-stream') + + assert.strictEqual(chunkCallbackCalled, true) + + assert.notStrictEqual(res.body, undefined) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const reader = res.body!.getReader() + + let body = '' + const decoder = new TextDecoder() + while (true) { + const { value, done } = await reader.read() + if (done !== undefined && done) { + break + } + + body += decoder.decode(value) + } + + assert.strictEqual(body, expectedStreamBody) + }) + }) +} + +it('calls the preResponseCallback', async () => { + const [app, port] = await buildAiWarpApp({ + aiProvider: { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } + } + }) + + let callbackCalled = false + await app.register(fastifyPlugin(async () => { + app.ai.preResponseCallback = (_, response) => { + callbackCalled = true + return response + ' modified' + } + })) + + await app.start() + + const res = await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) + + assert.strictEqual(callbackCalled, true) + + const body = await res.json() + assert.strictEqual(body.response, `${MOCK_CONTENT_RESPONSE} modified`) + + await app.close() +}) diff --git a/tests/e2e/auth.test.ts b/tests/e2e/auth.test.ts new file mode 100644 index 0000000..2b3b765 --- /dev/null +++ b/tests/e2e/auth.test.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { it } from 'node:test' +import assert from 'node:assert' +import { buildAiWarpApp } from '../utils/stackable' +import { AiWarpConfig } from '../../config' +import { authConfig, createToken } from '../utils/auth' + +const aiProvider: AiWarpConfig['aiProvider'] = { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } +} + +it('returns 401 for unauthorized user', async () => { + const [app, port] = await buildAiWarpApp({ + aiProvider, + auth: { + required: true + } + }) + + try { + await app.start() + + const response = await fetch(`http://localhost:${port}`) + assert.strictEqual(response.status, 401) + } finally { + await app.close() + } +}) + +it('returns 200 for authorized user', async () => { + const [app, port] = await buildAiWarpApp({ + aiProvider, + auth: authConfig + }) + + try { + await app.start() + + const response = await fetch(`http://localhost:${port}`, { + headers: { + 'Authorization': `Bearer ${createToken({ asd: '123' })}` + } + }) + assert.strictEqual(response.status, 200) + } finally { + await app.close() + } +}) diff --git a/tests/e2e/index.ts b/tests/e2e/index.ts new file mode 100644 index 0000000..f344059 --- /dev/null +++ b/tests/e2e/index.ts @@ -0,0 +1,7 @@ +import './api.test' +import './rate-limiting.test' +import './auth.test' +import { mockMistralApi, mockOpenAiApi } from '../utils/mocks' + +mockOpenAiApi() +mockMistralApi() diff --git a/tests/e2e/rate-limiting.test.ts b/tests/e2e/rate-limiting.test.ts new file mode 100644 index 0000000..94523b2 --- /dev/null +++ b/tests/e2e/rate-limiting.test.ts @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { it } from 'node:test' +import assert from 'node:assert' +import fastifyPlugin from 'fastify-plugin' +import { AiWarpConfig } from '../../config' +import { buildAiWarpApp } from '../utils/stackable' + +const aiProvider: AiWarpConfig['aiProvider'] = { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } +} + +it('calls ai.rateLimiting.max callback', async () => { + const [app, port] = await buildAiWarpApp({ aiProvider }) + + try { + const expectedMax = 100 + let callbackCalled = false + await app.register(fastifyPlugin(async () => { + app.ai.rateLimiting.max = () => { + callbackCalled = true + return expectedMax + } + })) + + await app.start() + + const res = await fetch(`http://localhost:${port}`) + assert.strictEqual(callbackCalled, true) + assert.strictEqual(res.headers.get('x-ratelimit-limit'), `${expectedMax}`) + } finally { + await app.close() + } +}) + +it('calls ai.rateLimiting.allowList callback', async () => { + const [app, port] = await buildAiWarpApp({ aiProvider }) + + try { + let callbackCalled = false + app.register(fastifyPlugin(async () => { + app.ai.rateLimiting.allowList = () => { + callbackCalled = true + return true + } + })) + + await app.start() + + await fetch(`http://localhost:${port}`) + assert.strictEqual(callbackCalled, true) + } finally { + await app.close() + } +}) + +it('calls ai.rateLimiting.onBanReach callback', async () => { + const [app, port] = await buildAiWarpApp({ + aiProvider, + rateLimiting: { + max: 0, + ban: 0 + } + }) + + try { + let onBanReachCalled = false + let errorResponseBuilderCalled = false + app.register(fastifyPlugin(async () => { + app.ai.rateLimiting.onBanReach = () => { + onBanReachCalled = true + } + + app.ai.rateLimiting.errorResponseBuilder = () => { + errorResponseBuilderCalled = true + return { error: 'rate limited' } + } + })) + + await app.start() + + await fetch(`http://localhost:${port}`) + assert.strictEqual(onBanReachCalled, true) + assert.strictEqual(errorResponseBuilderCalled, true) + } finally { + await app.close() + } +}) + +it('calls ai.rateLimiting.keyGenerator callback', async () => { + const [app, port] = await buildAiWarpApp({ aiProvider }) + + try { + let callbackCalled = false + app.register(fastifyPlugin(async () => { + app.ai.rateLimiting.keyGenerator = (req) => { + callbackCalled = true + return req.ip + } + })) + + await app.start() + + await fetch(`http://localhost:${port}`) + assert.strictEqual(callbackCalled, true) + } finally { + await app.close() + } +}) + +it('calls ai.rateLimiting.errorResponseBuilder callback', async () => { + const [app, port] = await buildAiWarpApp({ aiProvider }) + + try { + let callbackCalled = false + app.register(fastifyPlugin(async () => { + app.ai.rateLimiting.max = () => 0 + app.ai.rateLimiting.errorResponseBuilder = () => { + callbackCalled = true + return { error: 'rate limited' } + } + })) + + await app.start() + + await fetch(`http://localhost:${port}`) + assert.strictEqual(callbackCalled, true) + } finally { + await app.close() + } +}) diff --git a/tests/types/README.md b/tests/types/README.md new file mode 100644 index 0000000..6b9c396 --- /dev/null +++ b/tests/types/README.md @@ -0,0 +1,2 @@ +# Type Tests +Tests for public-facing types diff --git a/tests/types/fastify.test-d.ts b/tests/types/fastify.test-d.ts new file mode 100644 index 0000000..ed36a22 --- /dev/null +++ b/tests/types/fastify.test-d.ts @@ -0,0 +1,37 @@ +import { ReadableStream } from 'node:stream/web' +import { FastifyInstance, FastifyRequest } from 'fastify' +import { errorResponseBuilderContext } from '@fastify/rate-limit' +import { expectAssignable } from 'tsd' +import '../../index.d' + +expectAssignable(async (_: FastifyRequest, _2: string) => '') + +expectAssignable(async (_: FastifyRequest, _2: string) => new ReadableStream()) + +expectAssignable((_: FastifyRequest, _2: string) => '') +expectAssignable(async (_: FastifyRequest, _2: string) => '') + +expectAssignable((_: FastifyRequest, _2: string) => '') +expectAssignable(async (_: FastifyRequest, _2: string) => '') + +expectAssignable((_: FastifyRequest, _2: string) => 0) +expectAssignable(async (_: FastifyRequest, _2: string) => 0) + +expectAssignable((_: FastifyRequest, _2: string) => true) +expectAssignable(async (_: FastifyRequest, _2: string) => true) + +expectAssignable((_: FastifyRequest, _2: string) => {}) +expectAssignable(async (_: FastifyRequest, _2: string) => {}) + +expectAssignable((_: FastifyRequest) => '') +expectAssignable((_: FastifyRequest) => 0) +expectAssignable(async (_: FastifyRequest) => '') +expectAssignable(async (_: FastifyRequest) => 0) + +expectAssignable((_: FastifyRequest, _2: errorResponseBuilderContext) => { return {} }) + +expectAssignable((_: FastifyRequest, _2: string) => {}) +expectAssignable(async (_: FastifyRequest, _2: string) => {}) + +expectAssignable((_: FastifyRequest, _2: string) => {}) +expectAssignable(async (_: FastifyRequest, _2: string) => {}) diff --git a/tests/types/schema.test-d.ts b/tests/types/schema.test-d.ts new file mode 100644 index 0000000..de60fd6 --- /dev/null +++ b/tests/types/schema.test-d.ts @@ -0,0 +1,91 @@ +import { expectAssignable } from 'tsd' +import { AiWarpConfig } from '../../config' + +// TODO: add more when more models are added +expectAssignable({ + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } +}) + +expectAssignable({ + openai: { + model: 'gpt-4', + apiKey: '' + } +}) + +expectAssignable({ + mistral: { + model: 'mistral-tiny', + apiKey: '' + } +}) + +expectAssignable({ + aiProvider: { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } + } +}) + +expectAssignable({ + aiProvider: { + mistral: { + model: 'mistral-tiny', + apiKey: '' + } + } +}) + +expectAssignable({ + $schema: './stackable.schema.json', + service: { + openapi: true + }, + watch: true, + server: { + hostname: '{PLT_SERVER_HOSTNAME}', + port: '{PORT}', + logger: { + level: '{PLT_SERVER_LOGGER_LEVEL}' + } + }, + module: '@platformatic/ai-warp', + aiProvider: { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '{PLT_OPENAI_API_KEY}' + } + }, + promptDecorators: { + prefix: '', + suffix: '' + }, + plugins: { + paths: [ + { + path: './plugins', + encapsulate: false + } + ] + } +}) + +expectAssignable({}) + +expectAssignable({ + prefix: '' +}) + +expectAssignable({ + suffix: '' +}) + +expectAssignable({ + prefix: '', + suffix: '' +}) diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/tests/unit/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts new file mode 100644 index 0000000..935a85f --- /dev/null +++ b/tests/unit/ai-providers.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { describe, it } from 'node:test' +import assert from 'node:assert' +import { MistralProvider } from '../../ai-providers/mistral' +import { OpenAiProvider } from '../../ai-providers/open-ai' +import { AiProvider } from '../../ai-providers/provider' +import { MOCK_CONTENT_RESPONSE, buildExpectedStreamBodyString } from '../utils/mocks' + +const expectedStreamBody = buildExpectedStreamBodyString() + +const providers: AiProvider[] = [ + new OpenAiProvider('gpt-3.5-turbo', ''), + new MistralProvider('mistral-tiny', '') +] + +for (const provider of providers) { + describe(provider.constructor.name, () => { + it('ask', async () => { + const response = await provider.ask('asd') + assert.strictEqual(response, MOCK_CONTENT_RESPONSE) + }) + + it('askStream', async () => { + const response = await provider.askStream('asd') + const reader = response.getReader() + + let body = '' + const decoder = new TextDecoder() + while (true) { + const { value, done } = await reader.read() + if (done !== undefined && done) { + break + } + + body += decoder.decode(value) + } + + assert.strictEqual(body, expectedStreamBody) + }) + }) +} diff --git a/tests/unit/generator.test.ts b/tests/unit/generator.test.ts new file mode 100644 index 0000000..f47f150 --- /dev/null +++ b/tests/unit/generator.test.ts @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { describe, it, afterEach } from 'node:test' +import assert from 'node:assert' +import { existsSync } from 'node:fs' +import { mkdir, rm, readFile } from 'node:fs/promises' +import { join } from 'node:path' +import AiWarpGenerator from '../../lib/generator' +import { generateGlobalTypesFile } from '../../lib/templates/types' +import { generatePluginWithTypesSupport } from '@platformatic/generators/lib/create-plugin' + +const tempDirBase = join(__dirname, 'tmp') + +let counter = 0 +export async function getTempDir (baseDir: string): Promise { + if (baseDir === undefined) { + baseDir = __dirname + } + const dir = join(baseDir, `platformatic-generators-${process.pid}-${Date.now()}-${counter++}`) + try { + await mkdir(dir, { recursive: true }) + } catch (err) { + // do nothing + } + return dir +} + +describe('AiWarpGenerator', () => { + afterEach(async () => { + try { + await rm(tempDirBase, { recursive: true }) + } catch (err) { + // do nothing + } + }) + + it('generates global.d.ts correctly', async () => { + const dir = await getTempDir(tempDirBase) + + const generator = new AiWarpGenerator() + generator.setConfig({ + targetDirectory: dir, + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json'), + aiProvider: 'openai', + aiModel: 'gpt-3.5-turbo' + }) + await generator.run() + + const globalTypes = await readFile(join(dir, 'global.d.ts'), 'utf8') + assert.strictEqual(globalTypes, generateGlobalTypesFile('@platformatic/ai-warp')) + }) + + it('adds env variables to .env', async () => { + const dir = await getTempDir(tempDirBase) + + const generator = new AiWarpGenerator() + generator.setConfig({ + targetDirectory: dir, + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json'), + aiProvider: 'openai', + aiModel: 'gpt-3.5-turbo' + }) + await generator.run() + + // Env file has the api key fields for all providers + const envFile = await readFile(join(dir, '.env'), 'utf8') + assert.ok(envFile.includes('PLT_OPENAI_API_KEY')) + assert.ok(envFile.includes('PLT_MISTRAL_API_KEY')) + + const sampleEnvFile = await readFile(join(dir, '.env.sample'), 'utf8') + assert.ok(sampleEnvFile.includes('PLT_OPENAI_API_KEY')) + assert.ok(sampleEnvFile.includes('PLT_MISTRAL_API_KEY')) + }) + + it('generates platformatic.json correctly', async () => { + const dir = await getTempDir(tempDirBase) + + const generator = new AiWarpGenerator() + generator.setConfig({ + targetDirectory: dir, + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json'), + aiProvider: 'openai', + aiModel: 'gpt-3.5-turbo' + }) + await generator.run() + + let configFile = JSON.parse(await readFile(join(dir, 'platformatic.json'), 'utf8')) + assert.deepStrictEqual(configFile.aiProvider, { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '{PLT_OPENAI_API_KEY}' + } + }) + + generator.setConfig({ + aiProvider: 'mistral', + aiModel: 'mistral-tiny' + }) + await generator.run() + + configFile = JSON.parse(await readFile(join(dir, 'platformatic.json'), 'utf8')) + assert.deepStrictEqual(configFile.aiProvider, { + mistral: { + model: 'mistral-tiny', + apiKey: '{PLT_MISTRAL_API_KEY}' + } + }) + }) + + it('doesn\'t generate a plugin when not wanted', async () => { + const dir = await getTempDir(tempDirBase) + + const generator = new AiWarpGenerator() + generator.setConfig({ + targetDirectory: dir, + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json'), + aiProvider: 'openai', + aiModel: 'gpt-3.5-turbo' + }) + await generator.run() + + const pluginsDirectory = join(dir, 'plugins') + assert.strictEqual(existsSync(pluginsDirectory), false) + }) + + it('generates expected js example plugin', async () => { + const dir = await getTempDir(tempDirBase) + + const generator = new AiWarpGenerator() + generator.setConfig({ + targetDirectory: dir, + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json'), + aiProvider: 'openai', + aiModel: 'gpt-3.5-turbo', + plugin: true + }) + await generator.run() + + const pluginsDirectory = join(dir, 'plugins') + assert.strictEqual(existsSync(pluginsDirectory), true) + + const exampleJsPlugin = await readFile(join(pluginsDirectory, 'example.js'), 'utf8') + assert.strictEqual(exampleJsPlugin, generatePluginWithTypesSupport(false).contents) + }) + + it('generates expected ts example plugin', async () => { + const dir = await getTempDir(tempDirBase) + + const generator = new AiWarpGenerator() + generator.setConfig({ + targetDirectory: dir, + aiWarpPackageJsonPath: join(__dirname, '..', '..', 'package.json'), + aiProvider: 'openai', + aiModel: 'gpt-3.5-turbo', + plugin: true, + typescript: true + }) + await generator.run() + + const pluginsDirectory = join(dir, 'plugins') + assert.strictEqual(existsSync(pluginsDirectory), true) + + const exampleTsPlugin = await readFile(join(pluginsDirectory, 'example.ts'), 'utf8') + assert.strictEqual(exampleTsPlugin, generatePluginWithTypesSupport(true).contents) + }) +}) diff --git a/tests/unit/index.ts b/tests/unit/index.ts new file mode 100644 index 0000000..56ca4ab --- /dev/null +++ b/tests/unit/index.ts @@ -0,0 +1,6 @@ +import './generator.test' +import './ai-providers.test' +import { mockMistralApi, mockOpenAiApi } from '../utils/mocks' + +mockOpenAiApi() +mockMistralApi() diff --git a/tests/utils/auth.ts b/tests/utils/auth.ts new file mode 100644 index 0000000..d7ad501 --- /dev/null +++ b/tests/utils/auth.ts @@ -0,0 +1,19 @@ +import { createSigner } from 'fast-jwt' +import { AiWarpConfig } from '../../config' + +export const authConfig: AiWarpConfig['auth'] = { + required: true, + jwt: { + secret: 'secret' + } +} + +export function createToken (payload: string | Buffer | { [key: string]: any }, opts = {}): string { + const signSync = createSigner({ + key: 'secret', + expiresIn: '1h', + ...opts + }) + + return signSync(payload) +} diff --git a/tests/utils/mocks.ts b/tests/utils/mocks.ts new file mode 100644 index 0000000..02b711d --- /dev/null +++ b/tests/utils/mocks.ts @@ -0,0 +1,190 @@ +import { MockAgent, setGlobalDispatcher } from 'undici' + +export const MOCK_CONTENT_RESPONSE = 'asd123' + +export const MOCK_STREAMING_CONTENT_CHUNKS = [ + 'chunk1', + 'chunk2', + 'chunk3' +] + +/** + * @returns The full body that should be returned from the stream endpoint + */ +export function buildExpectedStreamBodyString (): string { + let body = '' + for (const chunk of MOCK_STREAMING_CONTENT_CHUNKS) { + body += `event: content\ndata: {"response":"${chunk}"}\n\n` + } + return body +} + +const mockAgent = new MockAgent() +let isMockAgentEstablished = false +function establishMockAgent (): void { + if (isMockAgentEstablished) { + return + } + setGlobalDispatcher(mockAgent) + isMockAgentEstablished = true +} + +let isOpenAiMocked = false + +/** + * Mock OpenAI's rest api + * @see https://platform.openai.com/docs/api-reference/chat + */ +export function mockOpenAiApi (): void { + if (isOpenAiMocked) { + return + } + + isOpenAiMocked = true + + establishMockAgent() + + const pool = mockAgent.get('https://api.openai.com') + pool.intercept({ + path: '/v1/chat/completions', + method: 'POST' + }).reply(200, (opts) => { + if (typeof opts.body !== 'string') { + throw new Error(`body is not a string (${typeof opts.body})`) + } + + const body = JSON.parse(opts.body) + + let response = '' + if (body.stream === true) { + for (let i = 0; i < MOCK_STREAMING_CONTENT_CHUNKS.length; i++) { + response += 'data: ' + response += JSON.stringify({ + id: 'chatcmpl-123', + object: 'chat.completion.chunk', + created: 1694268190, + model: 'gpt-3.5-turbo-0125', + system_fingerprint: 'fp_44709d6fcb', + choices: [{ + index: 0, + delta: { + role: 'assistant', + content: MOCK_STREAMING_CONTENT_CHUNKS[i] + }, + logprobs: null, + finish_reason: i === MOCK_STREAMING_CONTENT_CHUNKS.length ? 'stop' : null + }] + }) + response += '\n\n' + } + response += 'data: [DONE]\n\n' + } else { + response += JSON.stringify({ + id: 'chatcmpl-123', + object: 'chat.completion', + created: new Date().getTime() / 1000, + model: 'gpt-3.5-turbo-0125', + system_fingerprint: 'fp_fp_44709d6fcb', + choices: [{ + index: 0, + message: { + role: 'assistant', + content: MOCK_CONTENT_RESPONSE + }, + logprobs: null, + finish_reason: 'stop' + }], + usage: { + prompt_tokens: 1, + completion_tokens: 1, + total_tokens: 2 + } + }) + } + + return response + }, { + headers: { + 'content-type': 'application/json' + } + }).persist() +} + +let isMistralMocked = false + +/** + * Mock Mistral's rest api + * @see https://docs.mistral.ai/api/#operation/createChatCompletion + */ +export function mockMistralApi (): void { + if (isMistralMocked) { + return + } + + isMistralMocked = true + + establishMockAgent() + + const pool = mockAgent.get('https://api.mistral.ai') + pool.intercept({ + path: '/v1/chat/completions', + method: 'POST' + }).reply(200, (opts) => { + if (typeof opts.body !== 'string') { + throw new Error(`body is not a string (${typeof opts.body})`) + } + + const body = JSON.parse(opts.body) + + let response = '' + if (body.stream === true) { + for (let i = 0; i < MOCK_STREAMING_CONTENT_CHUNKS.length; i++) { + response += 'data: ' + response += JSON.stringify({ + id: 'cmpl-e5cc70bb28c444948073e77776eb30ef', + object: 'chat.completion.chunk', + created: 1694268190, + model: 'mistral-small-latest', + choices: [{ + index: 0, + delta: { + role: 'assistant', + content: MOCK_STREAMING_CONTENT_CHUNKS[i] + }, + logprobs: null, + finish_reason: i === MOCK_STREAMING_CONTENT_CHUNKS.length ? 'stop' : null + }] + }) + response += '\n\n' + } + response += 'data: [DONE]\n\n' + } else { + response += JSON.stringify({ + id: 'cmpl-e5cc70bb28c444948073e77776eb30ef', + object: 'chat.completion', + created: new Date().getTime() / 1000, + model: 'mistral-small-latest', + choices: [{ + index: 0, + message: { + role: 'assistant', + content: MOCK_CONTENT_RESPONSE + }, + logprobs: null, + finish_reason: 'stop' + }], + usage: { + prompt_tokens: 1, + completion_tokens: 1, + total_tokens: 2 + } + }) + } + + return response + }, { + headers: { + 'content-type': 'application/json' + } + }).persist() +} diff --git a/tests/utils/stackable.ts b/tests/utils/stackable.ts new file mode 100644 index 0000000..662dec8 --- /dev/null +++ b/tests/utils/stackable.ts @@ -0,0 +1,35 @@ +import { buildServer } from '@platformatic/service' +import { FastifyInstance } from 'fastify' +import stackable from '../../index' +import { AiWarpConfig } from '../../config' + +declare module 'fastify' { + interface FastifyInstance { + start: () => Promise + } +} + +let apps = 0 +function getPort (): number { + apps++ + return 3042 + apps +} + +export async function buildAiWarpApp (config: AiWarpConfig): Promise<[FastifyInstance, number]> { + const port = getPort() + const app = await buildServer({ + server: { + port, + forceCloseConnections: true, + healthCheck: { + enabled: false + }, + logger: { + level: 'silent' + } + }, + ...config + }, stackable) + + return [app, port] +}