diff --git a/.fern/metadata.json b/.fern/metadata.json deleted file mode 100644 index c557186..0000000 --- a/.fern/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cliVersion": "0.113.1", - "generatorName": "fernapi/fern-typescript-sdk", - "generatorVersion": "3.28.6" -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5834b38..a27fb10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,40 +3,40 @@ name: ci on: [push] jobs: - compile: - runs-on: ubuntu-latest + compile: + runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 + steps: + - name: Checkout repo + uses: actions/checkout@v4 - - name: Set up node - uses: actions/setup-node@v4 + - name: Set up node + uses: actions/setup-node@v3 - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 - - name: Install dependencies - run: pnpm install + - name: Install dependencies + run: pnpm install - - name: Compile - run: pnpm build + - name: Compile + run: pnpm build - test: - runs-on: ubuntu-latest + test: + runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 + steps: + - name: Checkout repo + uses: actions/checkout@v4 - - name: Set up node - uses: actions/setup-node@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Set up node + uses: actions/setup-node@v3 - - name: Install dependencies - run: pnpm install + - name: Install pnpm + uses: pnpm/action-setup@v4 - - name: Test - run: pnpm test + - name: Install dependencies + run: pnpm install + + - name: Test + run: pnpm test diff --git a/.npmignore b/.npmignore index c0c40ac..383dd36 100644 --- a/.npmignore +++ b/.npmignore @@ -5,7 +5,6 @@ tests .github .fernignore .prettierrc.yml -biome.json tsconfig.json yarn.lock pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e4691be --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +dist +*.tsbuildinfo +_tmp_* +*.tmp +.tmp/ +*.log +.DS_Store +Thumbs.db + \ No newline at end of file diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..0c06786 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,2 @@ +tabWidth: 4 +printWidth: 120 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index fe5bc2f..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,133 +0,0 @@ -# Contributing - -Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. - -## Getting Started - -### Prerequisites - -- Node.js 20 or higher -- pnpm package manager - -### Installation - -Install the project dependencies: - -```bash -pnpm install -``` - -### Building - -Build the project: - -```bash -pnpm build -``` - -### Testing - -Run the test suite: - -```bash -pnpm test -``` - -Run specific test types: -- `pnpm test:unit` - Run unit tests -- `pnpm test:wire` - Run wire/integration tests - -### Linting and Formatting - -Check code style: - -```bash -pnpm run lint -pnpm run format:check -``` - -Fix code style issues: - -```bash -pnpm run lint:fix -pnpm run format:fix -``` - -Or use the combined check command: - -```bash -pnpm run check:fix -``` - -## About Generated Code - -**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. - -### Generated Files - -The following directories contain generated code: -- `src/api/` - API client classes and types -- `src/serialization/` - Serialization/deserialization logic -- Most TypeScript files in `src/` - -### How to Customize - -If you need to customize the SDK, you have two options: - -#### Option 1: Use `.fernignore` - -For custom code that should persist across SDK regenerations: - -1. Create a `.fernignore` file in the project root -2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) -3. Add your custom code to those files - -Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. - -For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). - -#### Option 2: Contribute to the Generator - -If you want to change how code is generated for all users of this SDK: - -1. The TypeScript SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) -2. Generator code is located at `generators/typescript/sdk/` -3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) -4. Submit a pull request with your changes to the generator - -This approach is best for: -- Bug fixes in generated code -- New features that would benefit all users -- Improvements to code generation patterns - -## Making Changes - -### Workflow - -1. Create a new branch for your changes -2. Make your modifications -3. Run tests to ensure nothing breaks: `pnpm test` -4. Run linting and formatting: `pnpm run check:fix` -5. Build the project: `pnpm build` -6. Commit your changes with a clear commit message -7. Push your branch and create a pull request - -### Commit Messages - -Write clear, descriptive commit messages that explain what changed and why. - -### Code Style - -This project uses automated code formatting and linting. Run `pnpm run check:fix` before committing to ensure your code meets the project's style guidelines. - -## Questions or Issues? - -If you have questions or run into issues: - -1. Check the [Fern documentation](https://buildwithfern.com) -2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) -3. Open a new issue if your question hasn't been addressed - -## License - -By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/README.md b/README.md index 35b0a51..b1870be 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,26 @@ The FernAutopilotTest TypeScript library provides convenient access to the FernAutopilotTest APIs from TypeScript. +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Additional Headers](#additional-headers) + - [Additional Query String Parameters](#additional-query-string-parameters) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Aborting Requests](#aborting-requests) + - [Access Raw Response Data](#access-raw-response-data) + - [Runtime Compatibility](#runtime-compatibility) +- [Contributing](#contributing) + ## Installation ```sh -npm i -s +npm i -s ``` ## Reference @@ -25,7 +41,9 @@ import { FernAutopilotTestApiClient } from ""; const client = new FernAutopilotTestApiClient({ environment: "YOUR_BASE_URL" }); await client.imdb.createMovie({ title: "title", - rating: 1.1 + rating: 1.1, + metadata: "metadata", + more_metadata: "more_metadata", }); ``` @@ -129,76 +147,10 @@ console.log(data); console.log(rawResponse.headers['X-My-Header']); ``` -### Logging - -The SDK supports logging. You can configure the logger by passing in a `logging` object to the client options. - -```typescript -import { FernAutopilotTestApiClient, logging } from "FernAutopilotTestApi"; - -const client = new FernAutopilotTestApiClient({ - ... - logging: { - level: logging.LogLevel.Debug, // defaults to logging.LogLevel.Info - logger: new logging.ConsoleLogger(), // defaults to ConsoleLogger - silent: false, // defaults to true, set to false to enable logging - } -}); -``` -The `logging` object can have the following properties: -- `level`: The log level to use. Defaults to `logging.LogLevel.Info`. -- `logger`: The logger to use. Defaults to a `logging.ConsoleLogger`. -- `silent`: Whether to silence the logger. Defaults to `true`. - -The `level` property can be one of the following values: -- `logging.LogLevel.Debug` -- `logging.LogLevel.Info` -- `logging.LogLevel.Warn` -- `logging.LogLevel.Error` - -To provide a custom logger, you can pass in an object that implements the `logging.ILogger` interface. - -
-Custom logger examples - -Here's an example using the popular `winston` logging library. -```ts -import winston from 'winston'; - -const winstonLogger = winston.createLogger({...}); - -const logger: logging.ILogger = { - debug: (msg, ...args) => winstonLogger.debug(msg, ...args), - info: (msg, ...args) => winstonLogger.info(msg, ...args), - warn: (msg, ...args) => winstonLogger.warn(msg, ...args), - error: (msg, ...args) => winstonLogger.error(msg, ...args), -}; -``` - -Here's an example using the popular `pino` logging library. - -```ts -import pino from 'pino'; - -const pinoLogger = pino({...}); - -const logger: logging.ILogger = { - debug: (msg, ...args) => pinoLogger.debug(args, msg), - info: (msg, ...args) => pinoLogger.info(args, msg), - warn: (msg, ...args) => pinoLogger.warn(args, msg), - error: (msg, ...args) => pinoLogger.error(args, msg), -}; -``` -
- - ### Runtime Compatibility - The SDK works in the following runtimes: - - - Node.js 18+ - Vercel - Cloudflare Workers @@ -228,4 +180,4 @@ otherwise they would be overwritten upon the next generated release. Feel free t a proof of concept, but know that we will not be able to merge it as-is. We suggest opening an issue first to discuss with us! -On the other hand, contributions to the README are always very welcome! \ No newline at end of file +On the other hand, contributions to the README are always very welcome! diff --git a/biome.json b/biome.json deleted file mode 100644 index a777468..0000000 --- a/biome.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", - "root": true, - "vcs": { - "enabled": false - }, - "files": { - "ignoreUnknown": true, - "includes": [ - "**", - "!!dist", - "!!**/dist", - "!!lib", - "!!**/lib", - "!!_tmp_*", - "!!**/_tmp_*", - "!!*.tmp", - "!!**/*.tmp", - "!!.tmp/", - "!!**/.tmp/", - "!!*.log", - "!!**/*.log", - "!!**/.DS_Store", - "!!**/Thumbs.db" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 4, - "lineWidth": 120 - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "linter": { - "rules": { - "style": { - "useNodejsImportProtocol": "off" - }, - "suspicious": { - "noAssignInExpressions": "warn", - "noUselessEscapeInString": { - "level": "warn", - "fix": "none", - "options": {} - }, - "noThenProperty": "warn", - "useIterableCallbackReturn": "warn", - "noShadowRestrictedNames": "warn", - "noTsIgnore": { - "level": "warn", - "fix": "none", - "options": {} - }, - "noConfusingVoidType": { - "level": "warn", - "fix": "none", - "options": {} - } - } - } - } -} diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..3d497b3 --- /dev/null +++ b/changelog.md @@ -0,0 +1,4 @@ +## 2.0.1 - 2025-11-17 +* SDK regeneration +* Unable to analyze changes with AI, incrementing PATCH version. + diff --git a/package.json b/package.json index 5426420..d959e2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "", - "version": "2.0.0", + "version": "2.0.1", "private": false, "repository": "github:fern-demo/autopilot-typescript-sdk", "type": "commonjs", @@ -29,12 +29,7 @@ "LICENSE" ], "scripts": { - "format": "biome format --write --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", - "format:check": "biome format --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", - "lint": "biome lint --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", - "lint:fix": "biome lint --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", - "check": "biome check --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", - "check:fix": "biome check --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "format": "prettier . --write --ignore-unknown", "build": "pnpm build:cjs && pnpm build:esm", "build:cjs": "tsc --project ./tsconfig.cjs.json", "build:esm": "tsc --project ./tsconfig.esm.json && node scripts/rename-to-esm-files.js dist/esm", @@ -42,15 +37,14 @@ "test:unit": "vitest --project unit", "test:wire": "vitest --project wire" }, - "dependencies": {}, "devDependencies": { "webpack": "^5.97.1", "ts-loader": "^9.5.1", "vitest": "^3.2.4", "msw": "2.11.2", "@types/node": "^18.19.70", - "typescript": "~5.7.2", - "@biomejs/biome": "2.3.1" + "prettier": "^3.4.2", + "typescript": "~5.7.2" }, "browser": { "fs": false, @@ -58,7 +52,7 @@ "path": false, "stream": false }, - "packageManager": "pnpm@10.20.0", + "packageManager": "pnpm@10.14.0", "engines": { "node": ">=18.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdbfec3..3ce8d6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,15 @@ importers: .: devDependencies: - '@biomejs/biome': - specifier: 2.3.1 - version: 2.3.1 '@types/node': specifier: ^18.19.70 - version: 18.19.130 + version: 18.19.129 msw: specifier: 2.11.2 - version: 2.11.2(@types/node@18.19.130)(typescript@5.7.3) + version: 2.11.2(@types/node@18.19.129)(typescript@5.7.3) + prettier: + specifier: ^3.4.2 + version: 3.6.2 ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.7.3)(webpack@5.102.1) @@ -25,234 +25,181 @@ importers: version: 5.7.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@18.19.130)(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(terser@5.44.1) + version: 3.2.4(@types/node@18.19.129)(msw@2.11.2(@types/node@18.19.129)(typescript@5.7.3))(terser@5.44.0) webpack: specifier: ^5.97.1 version: 5.102.1 packages: - '@biomejs/biome@2.3.1': - resolution: {integrity: sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.3.1': - resolution: {integrity: sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.3.1': - resolution: {integrity: sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.3.1': - resolution: {integrity: sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-arm64@2.3.1': - resolution: {integrity: sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-x64-musl@2.3.1': - resolution: {integrity: sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-linux-x64@2.3.1': - resolution: {integrity: sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-win32-arm64@2.3.1': - resolution: {integrity: sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.3.1': - resolution: {integrity: sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} '@bundled-es-modules/statuses@1.0.1': resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@inquirer/ansi@1.0.1': - resolution: {integrity: sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==} + '@inquirer/ansi@1.0.0': + resolution: {integrity: sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==} engines: {node: '>=18'} - '@inquirer/confirm@5.1.19': - resolution: {integrity: sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==} + '@inquirer/confirm@5.1.18': + resolution: {integrity: sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -260,8 +207,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.3.0': - resolution: {integrity: sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==} + '@inquirer/core@10.2.2': + resolution: {integrity: sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -269,12 +216,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.14': - resolution: {integrity: sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==} + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} engines: {node: '>=18'} - '@inquirer/type@3.0.9': - resolution: {integrity: sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==} + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -298,8 +245,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mswjs/interceptors@0.39.8': - resolution: {integrity: sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==} + '@mswjs/interceptors@0.39.7': + resolution: {integrity: sha512-sURvQbbKsq5f8INV54YJgJEdk8oxBanqkTiXXd33rKmofFCwZLhLRszPduMZ9TA9b8/1CHc/IJmOlBHJk2Q5AQ==} engines: {node: '>=18'} '@open-draft/deferred-promise@2.2.0': @@ -311,118 +258,118 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@rollup/rollup-android-arm-eabi@4.53.1': - resolution: {integrity: sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==} + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.53.1': - resolution: {integrity: sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==} + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.53.1': - resolution: {integrity: sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==} + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.53.1': - resolution: {integrity: sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==} + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.53.1': - resolution: {integrity: sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==} + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.53.1': - resolution: {integrity: sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==} + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.53.1': - resolution: {integrity: sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.53.1': - resolution: {integrity: sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==} + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.53.1': - resolution: {integrity: sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==} + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.53.1': - resolution: {integrity: sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==} + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.53.1': - resolution: {integrity: sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==} + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.53.1': - resolution: {integrity: sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==} + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.53.1': - resolution: {integrity: sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==} + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.53.1': - resolution: {integrity: sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==} + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.53.1': - resolution: {integrity: sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==} + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.53.1': - resolution: {integrity: sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==} + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.53.1': - resolution: {integrity: sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==} + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.53.1': - resolution: {integrity: sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==} + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.53.1': - resolution: {integrity: sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==} + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.53.1': - resolution: {integrity: sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==} + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.53.1': - resolution: {integrity: sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==} + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.53.1': - resolution: {integrity: sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==} + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} cpu: [x64] os: [win32] - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -442,8 +389,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@18.19.129': + resolution: {integrity: sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -567,16 +514,16 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - baseline-browser-mapping@2.8.25: - resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} + baseline-browser-mapping@2.8.13: + resolution: {integrity: sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==} hasBin: true braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.27.0: - resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -587,8 +534,8 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - caniuse-lite@1.0.30001754: - resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} + caniuse-lite@1.0.30001748: + resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -641,8 +588,8 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - electron-to-chromium@1.5.248: - resolution: {integrity: sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==} + electron-to-chromium@1.5.232: + resolution: {integrity: sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -654,8 +601,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} hasBin: true @@ -724,8 +671,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} has-flag@4.0.0: @@ -759,15 +706,15 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -809,8 +756,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -840,6 +787,11 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -854,8 +806,8 @@ packages: rettime@0.7.0: resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} - rollup@4.53.1: - resolution: {integrity: sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==} + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -903,8 +855,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -948,8 +900,8 @@ packages: uglify-js: optional: true - terser@5.44.1: - resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} hasBin: true @@ -975,11 +927,11 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@7.0.17: - resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + tldts-core@7.0.16: + resolution: {integrity: sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==} - tldts@7.0.17: - resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + tldts@7.0.16: + resolution: {integrity: sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==} hasBin: true to-regex-range@5.0.1: @@ -1009,8 +961,8 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1020,8 +972,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@7.2.2: - resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1137,41 +1089,6 @@ packages: snapshots: - '@biomejs/biome@2.3.1': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.1 - '@biomejs/cli-darwin-x64': 2.3.1 - '@biomejs/cli-linux-arm64': 2.3.1 - '@biomejs/cli-linux-arm64-musl': 2.3.1 - '@biomejs/cli-linux-x64': 2.3.1 - '@biomejs/cli-linux-x64-musl': 2.3.1 - '@biomejs/cli-win32-arm64': 2.3.1 - '@biomejs/cli-win32-x64': 2.3.1 - - '@biomejs/cli-darwin-arm64@2.3.1': - optional: true - - '@biomejs/cli-darwin-x64@2.3.1': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.3.1': - optional: true - - '@biomejs/cli-linux-arm64@2.3.1': - optional: true - - '@biomejs/cli-linux-x64-musl@2.3.1': - optional: true - - '@biomejs/cli-linux-x64@2.3.1': - optional: true - - '@biomejs/cli-win32-arm64@2.3.1': - optional: true - - '@biomejs/cli-win32-x64@2.3.1': - optional: true - '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -1180,111 +1097,111 @@ snapshots: dependencies: statuses: 2.0.2 - '@esbuild/aix-ppc64@0.25.12': + '@esbuild/aix-ppc64@0.25.10': optional: true - '@esbuild/android-arm64@0.25.12': + '@esbuild/android-arm64@0.25.10': optional: true - '@esbuild/android-arm@0.25.12': + '@esbuild/android-arm@0.25.10': optional: true - '@esbuild/android-x64@0.25.12': + '@esbuild/android-x64@0.25.10': optional: true - '@esbuild/darwin-arm64@0.25.12': + '@esbuild/darwin-arm64@0.25.10': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/darwin-x64@0.25.10': optional: true - '@esbuild/freebsd-arm64@0.25.12': + '@esbuild/freebsd-arm64@0.25.10': optional: true - '@esbuild/freebsd-x64@0.25.12': + '@esbuild/freebsd-x64@0.25.10': optional: true - '@esbuild/linux-arm64@0.25.12': + '@esbuild/linux-arm64@0.25.10': optional: true - '@esbuild/linux-arm@0.25.12': + '@esbuild/linux-arm@0.25.10': optional: true - '@esbuild/linux-ia32@0.25.12': + '@esbuild/linux-ia32@0.25.10': optional: true - '@esbuild/linux-loong64@0.25.12': + '@esbuild/linux-loong64@0.25.10': optional: true - '@esbuild/linux-mips64el@0.25.12': + '@esbuild/linux-mips64el@0.25.10': optional: true - '@esbuild/linux-ppc64@0.25.12': + '@esbuild/linux-ppc64@0.25.10': optional: true - '@esbuild/linux-riscv64@0.25.12': + '@esbuild/linux-riscv64@0.25.10': optional: true - '@esbuild/linux-s390x@0.25.12': + '@esbuild/linux-s390x@0.25.10': optional: true - '@esbuild/linux-x64@0.25.12': + '@esbuild/linux-x64@0.25.10': optional: true - '@esbuild/netbsd-arm64@0.25.12': + '@esbuild/netbsd-arm64@0.25.10': optional: true - '@esbuild/netbsd-x64@0.25.12': + '@esbuild/netbsd-x64@0.25.10': optional: true - '@esbuild/openbsd-arm64@0.25.12': + '@esbuild/openbsd-arm64@0.25.10': optional: true - '@esbuild/openbsd-x64@0.25.12': + '@esbuild/openbsd-x64@0.25.10': optional: true - '@esbuild/openharmony-arm64@0.25.12': + '@esbuild/openharmony-arm64@0.25.10': optional: true - '@esbuild/sunos-x64@0.25.12': + '@esbuild/sunos-x64@0.25.10': optional: true - '@esbuild/win32-arm64@0.25.12': + '@esbuild/win32-arm64@0.25.10': optional: true - '@esbuild/win32-ia32@0.25.12': + '@esbuild/win32-ia32@0.25.10': optional: true - '@esbuild/win32-x64@0.25.12': + '@esbuild/win32-x64@0.25.10': optional: true - '@inquirer/ansi@1.0.1': {} + '@inquirer/ansi@1.0.0': {} - '@inquirer/confirm@5.1.19(@types/node@18.19.130)': + '@inquirer/confirm@5.1.18(@types/node@18.19.129)': dependencies: - '@inquirer/core': 10.3.0(@types/node@18.19.130) - '@inquirer/type': 3.0.9(@types/node@18.19.130) + '@inquirer/core': 10.2.2(@types/node@18.19.129) + '@inquirer/type': 3.0.8(@types/node@18.19.129) optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 18.19.129 - '@inquirer/core@10.3.0(@types/node@18.19.130)': + '@inquirer/core@10.2.2(@types/node@18.19.129)': dependencies: - '@inquirer/ansi': 1.0.1 - '@inquirer/figures': 1.0.14 - '@inquirer/type': 3.0.9(@types/node@18.19.130) + '@inquirer/ansi': 1.0.0 + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@18.19.129) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 18.19.129 - '@inquirer/figures@1.0.14': {} + '@inquirer/figures@1.0.13': {} - '@inquirer/type@3.0.9(@types/node@18.19.130)': + '@inquirer/type@3.0.8(@types/node@18.19.129)': optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 18.19.129 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -1305,7 +1222,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mswjs/interceptors@0.39.8': + '@mswjs/interceptors@0.39.7': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -1323,76 +1240,75 @@ snapshots: '@open-draft/until@2.1.0': {} - '@rollup/rollup-android-arm-eabi@4.53.1': + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true - '@rollup/rollup-android-arm64@4.53.1': + '@rollup/rollup-android-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-arm64@4.53.1': + '@rollup/rollup-darwin-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-x64@4.53.1': + '@rollup/rollup-darwin-x64@4.52.4': optional: true - '@rollup/rollup-freebsd-arm64@4.53.1': + '@rollup/rollup-freebsd-arm64@4.52.4': optional: true - '@rollup/rollup-freebsd-x64@4.53.1': + '@rollup/rollup-freebsd-x64@4.52.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.53.1': + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.53.1': + '@rollup/rollup-linux-arm-musleabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.53.1': + '@rollup/rollup-linux-arm64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.53.1': + '@rollup/rollup-linux-arm64-musl@4.52.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.53.1': + '@rollup/rollup-linux-loong64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.53.1': + '@rollup/rollup-linux-ppc64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.53.1': + '@rollup/rollup-linux-riscv64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.53.1': + '@rollup/rollup-linux-riscv64-musl@4.52.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.53.1': + '@rollup/rollup-linux-s390x-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.53.1': + '@rollup/rollup-linux-x64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-musl@4.53.1': + '@rollup/rollup-linux-x64-musl@4.52.4': optional: true - '@rollup/rollup-openharmony-arm64@4.53.1': + '@rollup/rollup-openharmony-arm64@4.52.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.53.1': + '@rollup/rollup-win32-arm64-msvc@4.52.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.53.1': + '@rollup/rollup-win32-ia32-msvc@4.52.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.53.1': + '@rollup/rollup-win32-x64-gnu@4.52.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.53.1': + '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true - '@types/chai@5.2.3': + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 '@types/cookie@0.6.0': {} @@ -1412,7 +1328,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@18.19.130': + '@types/node@18.19.129': dependencies: undici-types: 5.26.5 @@ -1420,20 +1336,20 @@ snapshots: '@vitest/expect@3.2.4': dependencies: - '@types/chai': 5.2.3 + '@types/chai': 5.2.2 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.2.2(@types/node@18.19.130)(terser@5.44.1))': + '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.129)(typescript@5.7.3))(vite@7.1.9(@types/node@18.19.129)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 0.30.19 optionalDependencies: - msw: 2.11.2(@types/node@18.19.130)(typescript@5.7.3) - vite: 7.2.2(@types/node@18.19.130)(terser@5.44.1) + msw: 2.11.2(@types/node@18.19.129)(typescript@5.7.3) + vite: 7.1.9(@types/node@18.19.129)(terser@5.44.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -1448,7 +1364,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.21 + magic-string: 0.30.19 pathe: 2.0.3 '@vitest/spy@3.2.4': @@ -1571,25 +1487,25 @@ snapshots: assertion-error@2.0.1: {} - baseline-browser-mapping@2.8.25: {} + baseline-browser-mapping@2.8.13: {} braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.27.0: + browserslist@4.26.3: dependencies: - baseline-browser-mapping: 2.8.25 - caniuse-lite: 1.0.30001754 - electron-to-chromium: 1.5.248 - node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.27.0) + baseline-browser-mapping: 2.8.13 + caniuse-lite: 1.0.30001748 + electron-to-chromium: 1.5.232 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) buffer-from@1.1.2: {} cac@6.7.14: {} - caniuse-lite@1.0.30001754: {} + caniuse-lite@1.0.30001748: {} chai@5.3.3: dependencies: @@ -1632,7 +1548,7 @@ snapshots: deep-eql@5.0.2: {} - electron-to-chromium@1.5.248: {} + electron-to-chromium@1.5.232: {} emoji-regex@8.0.0: {} @@ -1643,34 +1559,34 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild@0.25.12: + esbuild@0.25.10: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 escalade@3.2.0: {} @@ -1716,7 +1632,7 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.12.0: {} + graphql@16.11.0: {} has-flag@4.0.0: {} @@ -1730,7 +1646,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 18.19.130 + '@types/node': 18.19.129 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -1740,11 +1656,11 @@ snapshots: json-schema-traverse@1.0.0: {} - loader-runner@4.3.1: {} + loader-runner@4.3.0: {} loupe@3.2.1: {} - magic-string@0.30.21: + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1763,17 +1679,17 @@ snapshots: ms@2.1.3: {} - msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3): + msw@2.11.2(@types/node@18.19.129)(typescript@5.7.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 - '@inquirer/confirm': 5.1.19(@types/node@18.19.130) - '@mswjs/interceptors': 0.39.8 + '@inquirer/confirm': 5.1.18(@types/node@18.19.129) + '@mswjs/interceptors': 0.39.7 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.6 - graphql: 16.12.0 + graphql: 16.11.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -1795,7 +1711,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.27: {} + node-releases@2.0.23: {} outvariant@1.4.3: {} @@ -1817,6 +1733,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@3.6.2: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -1827,32 +1745,32 @@ snapshots: rettime@0.7.0: {} - rollup@4.53.1: + rollup@4.52.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.1 - '@rollup/rollup-android-arm64': 4.53.1 - '@rollup/rollup-darwin-arm64': 4.53.1 - '@rollup/rollup-darwin-x64': 4.53.1 - '@rollup/rollup-freebsd-arm64': 4.53.1 - '@rollup/rollup-freebsd-x64': 4.53.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.1 - '@rollup/rollup-linux-arm-musleabihf': 4.53.1 - '@rollup/rollup-linux-arm64-gnu': 4.53.1 - '@rollup/rollup-linux-arm64-musl': 4.53.1 - '@rollup/rollup-linux-loong64-gnu': 4.53.1 - '@rollup/rollup-linux-ppc64-gnu': 4.53.1 - '@rollup/rollup-linux-riscv64-gnu': 4.53.1 - '@rollup/rollup-linux-riscv64-musl': 4.53.1 - '@rollup/rollup-linux-s390x-gnu': 4.53.1 - '@rollup/rollup-linux-x64-gnu': 4.53.1 - '@rollup/rollup-linux-x64-musl': 4.53.1 - '@rollup/rollup-openharmony-arm64': 4.53.1 - '@rollup/rollup-win32-arm64-msvc': 4.53.1 - '@rollup/rollup-win32-ia32-msvc': 4.53.1 - '@rollup/rollup-win32-x64-gnu': 4.53.1 - '@rollup/rollup-win32-x64-msvc': 4.53.1 + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 safe-buffer@5.2.1: {} @@ -1889,7 +1807,7 @@ snapshots: statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@3.9.0: {} strict-event-emitter@0.5.1: {} @@ -1923,10 +1841,10 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.44.1 + terser: 5.44.0 webpack: 5.102.1 - terser@5.44.1: + terser@5.44.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 @@ -1948,11 +1866,11 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@7.0.17: {} + tldts-core@7.0.16: {} - tldts@7.0.17: + tldts@7.0.16: dependencies: - tldts-core: 7.0.17 + tldts-core: 7.0.16 to-regex-range@5.0.1: dependencies: @@ -1960,7 +1878,7 @@ snapshots: tough-cookie@6.0.0: dependencies: - tldts: 7.0.17 + tldts: 7.0.16 ts-loader@9.5.4(typescript@5.7.3)(webpack@5.102.1): dependencies: @@ -1978,19 +1896,19 @@ snapshots: undici-types@5.26.5: {} - update-browserslist-db@1.1.4(browserslist@4.27.0): + update-browserslist-db@1.1.3(browserslist@4.26.3): dependencies: - browserslist: 4.27.0 + browserslist: 4.26.3 escalade: 3.2.0 picocolors: 1.1.1 - vite-node@3.2.4(@types/node@18.19.130)(terser@5.44.1): + vite-node@3.2.4(@types/node@18.19.129)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.2(@types/node@18.19.130)(terser@5.44.1) + vite: 7.1.9(@types/node@18.19.129)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - jiti @@ -2005,24 +1923,24 @@ snapshots: - tsx - yaml - vite@7.2.2(@types/node@18.19.130)(terser@5.44.1): + vite@7.1.9(@types/node@18.19.129)(terser@5.44.0): dependencies: - esbuild: 0.25.12 + esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.53.1 + rollup: 4.52.4 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 18.19.129 fsevents: 2.3.3 - terser: 5.44.1 + terser: 5.44.0 - vitest@3.2.4(@types/node@18.19.130)(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(terser@5.44.1): + vitest@3.2.4(@types/node@18.19.129)(msw@2.11.2(@types/node@18.19.129)(typescript@5.7.3))(terser@5.44.0): dependencies: - '@types/chai': 5.2.3 + '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.2.2(@types/node@18.19.130)(terser@5.44.1)) + '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.129)(typescript@5.7.3))(vite@7.1.9(@types/node@18.19.129)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2031,20 +1949,20 @@ snapshots: chai: 5.3.3 debug: 4.4.3 expect-type: 1.2.2 - magic-string: 0.30.21 + magic-string: 0.30.19 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.10.0 + std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.2(@types/node@18.19.130)(terser@5.44.1) - vite-node: 3.2.4(@types/node@18.19.130)(terser@5.44.1) + vite: 7.1.9(@types/node@18.19.129)(terser@5.44.0) + vite-node: 3.2.4(@types/node@18.19.129)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 18.19.129 transitivePeerDependencies: - jiti - less @@ -2076,7 +1994,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.27.0 + browserslist: 4.26.3 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 @@ -2085,7 +2003,7 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 + loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6e4c395..339da38 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1 +1 @@ -packages: ['.'] \ No newline at end of file +packages: ["."] diff --git a/reference.md b/reference.md index 1e28385..81851b3 100644 --- a/reference.md +++ b/reference.md @@ -1,5 +1,7 @@ # Reference + ## Imdb +
client.imdb.createMovie({ ...params }) -> FernAutopilotTestApi.MovieId
@@ -13,6 +15,7 @@
Add a movie to the database +
@@ -29,10 +32,12 @@ Add a movie to the database ```typescript await client.imdb.createMovie({ title: "title", - rating: 1.1 + rating: 1.1, + metadata: "metadata", + more_metadata: "more_metadata", }); - ``` + @@ -46,22 +51,21 @@ await client.imdb.createMovie({
-**request:** `FernAutopilotTestApi.CreateMovieRequest` - +**request:** `FernAutopilotTestApi.CreateMovieRequest` +
-**requestOptions:** `Imdb.RequestOptions` - +**requestOptions:** `Imdb.RequestOptions` +
-
@@ -79,6 +83,7 @@ await client.imdb.createMovie({
Retrieve a movie from the database based on the ID +
@@ -94,8 +99,8 @@ Retrieve a movie from the database based on the ID ```typescript await client.imdb.getMovie("tt0111161"); - ``` + @@ -109,22 +114,21 @@ await client.imdb.getMovie("tt0111161");
-**id:** `FernAutopilotTestApi.MovieId` - +**id:** `FernAutopilotTestApi.MovieId` +
-**requestOptions:** `Imdb.RequestOptions` - +**requestOptions:** `Imdb.RequestOptions` +
- diff --git a/src/BaseClient.ts b/src/BaseClient.ts deleted file mode 100644 index 678bdf0..0000000 --- a/src/BaseClient.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as core from "./core/index.js"; - -export interface BaseClientOptions { - environment: core.Supplier; - /** Specify a custom URL to connect the client to. */ - baseUrl?: core.Supplier; - /** Additional headers to include in requests. */ - headers?: Record | null | undefined>; - /** The default maximum time to wait for a response in seconds. */ - timeoutInSeconds?: number; - /** The default number of times to retry the request. Defaults to 2. */ - maxRetries?: number; - /** Provide a custom fetch implementation. Useful for platforms that don't have a built-in fetch or need a custom implementation. */ - fetch?: typeof fetch; - /** Configure logging for the client. */ - logging?: core.logging.LogConfig | core.logging.Logger; -} - -export interface BaseRequestOptions { - /** The maximum time to wait for a response in seconds. */ - timeoutInSeconds?: number; - /** The number of times to retry the request. Defaults to 2. */ - maxRetries?: number; - /** A hook to abort the request. */ - abortSignal?: AbortSignal; - /** Additional query string parameters to include in the request. */ - queryParams?: Record; - /** Additional headers to include in the request. */ - headers?: Record | null | undefined>; -} diff --git a/src/Client.ts b/src/Client.ts index 44bfcd0..ff674ac 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,14 +1,30 @@ // This file was auto-generated by Fern from our API Definition. -import { Imdb } from "./api/resources/imdb/client/Client.js"; -import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; -import { mergeHeaders } from "./core/headers.js"; import * as core from "./core/index.js"; +import { mergeHeaders } from "./core/headers.js"; +import { Imdb } from "./api/resources/imdb/client/Client.js"; export declare namespace FernAutopilotTestApiClient { - export interface Options extends BaseClientOptions {} + export interface Options { + environment: core.Supplier; + /** Specify a custom URL to connect the client to. */ + baseUrl?: core.Supplier; + /** Additional headers to include in requests. */ + headers?: Record | null | undefined>; + } - export interface RequestOptions extends BaseRequestOptions {} + export interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Additional query string parameters to include in the request. */ + queryParams?: Record; + /** Additional headers to include in the request. */ + headers?: Record | null | undefined>; + } } export class FernAutopilotTestApiClient { @@ -18,13 +34,11 @@ export class FernAutopilotTestApiClient { constructor(_options: FernAutopilotTestApiClient.Options) { this._options = { ..._options, - logging: core.logging.createLogger(_options?.logging), headers: mergeHeaders( { "X-Fern-Language": "JavaScript", "X-Fern-SDK-Name": "", - "X-Fern-SDK-Version": "2.0.0", - "User-Agent": "/2.0.0", + "X-Fern-SDK-Version": "2.0.1", "X-Fern-Runtime": core.RUNTIME.type, "X-Fern-Runtime-Version": core.RUNTIME.version, }, diff --git a/src/api/resources/imdb/client/Client.ts b/src/api/resources/imdb/client/Client.ts index 89960a8..fe7535e 100644 --- a/src/api/resources/imdb/client/Client.ts +++ b/src/api/resources/imdb/client/Client.ts @@ -1,15 +1,31 @@ // This file was auto-generated by Fern from our API Definition. -import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; -import { mergeHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; -import * as errors from "../../../../errors/index.js"; import * as FernAutopilotTestApi from "../../../index.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as errors from "../../../../errors/index.js"; export declare namespace Imdb { - export interface Options extends BaseClientOptions {} + export interface Options { + environment: core.Supplier; + /** Specify a custom URL to connect the client to. */ + baseUrl?: core.Supplier; + /** Additional headers to include in requests. */ + headers?: Record | null | undefined>; + } - export interface RequestOptions extends BaseRequestOptions {} + export interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Additional query string parameters to include in the request. */ + queryParams?: Record; + /** Additional headers to include in the request. */ + headers?: Record | null | undefined>; + } } export class Imdb { @@ -28,7 +44,9 @@ export class Imdb { * @example * await client.imdb.createMovie({ * title: "title", - * rating: 1.1 + * rating: 1.1, + * metadata: "metadata", + * more_metadata: "more_metadata" * }) */ public createMovie( @@ -42,7 +60,7 @@ export class Imdb { request: FernAutopilotTestApi.CreateMovieRequest, requestOptions?: Imdb.RequestOptions, ): Promise> { - const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + let _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); const _response = await core.fetcher({ url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? @@ -55,11 +73,9 @@ export class Imdb { queryParameters: requestOptions?.queryParams, requestType: "json", body: request, - timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, - maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, abortSignal: requestOptions?.abortSignal, - fetchFn: this._options?.fetch, - logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as FernAutopilotTestApi.MovieId, rawResponse: _response.rawResponse }; @@ -117,21 +133,19 @@ export class Imdb { id: FernAutopilotTestApi.MovieId, requestOptions?: Imdb.RequestOptions, ): Promise> { - const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); + let _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); const _response = await core.fetcher({ url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - `/movies/${core.url.encodePathParam(id)}`, + `/movies/${encodeURIComponent(id)}`, ), method: "GET", headers: _headers, queryParameters: requestOptions?.queryParams, - timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, - maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, abortSignal: requestOptions?.abortSignal, - fetchFn: this._options?.fetch, - logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as FernAutopilotTestApi.Movie, rawResponse: _response.rawResponse }; diff --git a/src/api/resources/imdb/errors/MovieDoesNotExistError.ts b/src/api/resources/imdb/errors/MovieDoesNotExistError.ts index e1bd808..e53e1dd 100644 --- a/src/api/resources/imdb/errors/MovieDoesNotExistError.ts +++ b/src/api/resources/imdb/errors/MovieDoesNotExistError.ts @@ -1,8 +1,8 @@ // This file was auto-generated by Fern from our API Definition. -import type * as core from "../../../../core/index.js"; import * as errors from "../../../../errors/index.js"; -import type * as FernAutopilotTestApi from "../../../index.js"; +import * as FernAutopilotTestApi from "../../../index.js"; +import * as core from "../../../../core/index.js"; export class MovieDoesNotExistError extends errors.FernAutopilotTestApiError { constructor(body: FernAutopilotTestApi.MovieId, rawResponse?: core.RawResponse) { diff --git a/src/api/resources/imdb/index.ts b/src/api/resources/imdb/index.ts index b90a45b..f63e7d1 100644 --- a/src/api/resources/imdb/index.ts +++ b/src/api/resources/imdb/index.ts @@ -1,3 +1,3 @@ -export * from "./client/index.js"; -export * from "./errors/index.js"; export * from "./types/index.js"; +export * from "./errors/index.js"; +export * from "./client/index.js"; diff --git a/src/api/resources/imdb/types/CreateMovieRequest.ts b/src/api/resources/imdb/types/CreateMovieRequest.ts index 86bfe80..05b6ccd 100644 --- a/src/api/resources/imdb/types/CreateMovieRequest.ts +++ b/src/api/resources/imdb/types/CreateMovieRequest.ts @@ -3,4 +3,6 @@ export interface CreateMovieRequest { title: string; rating: number; + metadata: string; + more_metadata: string; } diff --git a/src/api/resources/imdb/types/Movie.ts b/src/api/resources/imdb/types/Movie.ts index 8b52190..ae90804 100644 --- a/src/api/resources/imdb/types/Movie.ts +++ b/src/api/resources/imdb/types/Movie.ts @@ -1,12 +1,11 @@ // This file was auto-generated by Fern from our API Definition. -import type * as FernAutopilotTestApi from "../../../index.js"; +import * as FernAutopilotTestApi from "../../../index.js"; export interface Movie { id: FernAutopilotTestApi.MovieId; title: string; /** The rating scale out of ten stars */ rating: number; - description: string; metadata: string; } diff --git a/src/api/resources/imdb/types/index.ts b/src/api/resources/imdb/types/index.ts index e349b81..b3ed5ed 100644 --- a/src/api/resources/imdb/types/index.ts +++ b/src/api/resources/imdb/types/index.ts @@ -1,3 +1,3 @@ -export * from "./CreateMovieRequest.js"; -export * from "./Movie.js"; export * from "./MovieId.js"; +export * from "./Movie.js"; +export * from "./CreateMovieRequest.js"; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index bee73c4..a93afdd 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -1,3 +1,3 @@ -export * from "./imdb/errors/index.js"; export * as imdb from "./imdb/index.js"; export * from "./imdb/types/index.js"; +export * from "./imdb/errors/index.js"; diff --git a/src/core/exports.ts b/src/core/exports.ts deleted file mode 100644 index 69296d7..0000000 --- a/src/core/exports.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./logging/exports.js"; diff --git a/src/core/fetcher/APIResponse.ts b/src/core/fetcher/APIResponse.ts index 97ab83c..dd4b946 100644 --- a/src/core/fetcher/APIResponse.ts +++ b/src/core/fetcher/APIResponse.ts @@ -1,4 +1,4 @@ -import type { RawResponse } from "./RawResponse.js"; +import { RawResponse } from "./RawResponse.js"; /** * The response of an API call. diff --git a/src/core/fetcher/BinaryResponse.ts b/src/core/fetcher/BinaryResponse.ts index 4b4d0e8..614cb59 100644 --- a/src/core/fetcher/BinaryResponse.ts +++ b/src/core/fetcher/BinaryResponse.ts @@ -1,4 +1,4 @@ -import type { ResponseWithBody } from "./ResponseWithBody.js"; +import { ResponseWithBody } from "./ResponseWithBody.js"; export type BinaryResponse = { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ diff --git a/src/core/fetcher/EndpointSupplier.ts b/src/core/fetcher/EndpointSupplier.ts index 8079841..5f1ddde 100644 --- a/src/core/fetcher/EndpointSupplier.ts +++ b/src/core/fetcher/EndpointSupplier.ts @@ -1,5 +1,5 @@ -import type { EndpointMetadata } from "./EndpointMetadata.js"; -import type { Supplier } from "./Supplier.js"; +import { EndpointMetadata } from "./EndpointMetadata.js"; +import { Supplier } from "./Supplier.js"; type EndpointSupplierFn = (arg: { endpointMetadata: EndpointMetadata }) => T | Promise; export type EndpointSupplier = Supplier | EndpointSupplierFn; diff --git a/src/core/fetcher/Fetcher.ts b/src/core/fetcher/Fetcher.ts index e68597d..09a1da1 100644 --- a/src/core/fetcher/Fetcher.ts +++ b/src/core/fetcher/Fetcher.ts @@ -1,8 +1,7 @@ import { toJson } from "../json.js"; -import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; -import type { APIResponse } from "./APIResponse.js"; +import { APIResponse } from "./APIResponse.js"; import { createRequestUrl } from "./createRequestUrl.js"; -import type { EndpointMetadata } from "./EndpointMetadata.js"; +import { EndpointMetadata } from "./EndpointMetadata.js"; import { EndpointSupplier } from "./EndpointSupplier.js"; import { getErrorResponseBody } from "./getErrorResponseBody.js"; import { getFetchFn } from "./getFetchFn.js"; @@ -26,12 +25,10 @@ export declare namespace Fetcher { maxRetries?: number; withCredentials?: boolean; abortSignal?: AbortSignal; - requestType?: "json" | "file" | "bytes" | "form" | "other"; + requestType?: "json" | "file" | "bytes"; responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response"; duplex?: "half"; endpointMetadata?: EndpointMetadata; - fetchFn?: typeof fetch; - logging?: LogConfig | Logger; } export type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError; @@ -58,155 +55,6 @@ export declare namespace Fetcher { } } -const SENSITIVE_HEADERS = new Set([ - "authorization", - "www-authenticate", - "x-api-key", - "api-key", - "apikey", - "x-api-token", - "x-auth-token", - "auth-token", - "cookie", - "set-cookie", - "proxy-authorization", - "proxy-authenticate", - "x-csrf-token", - "x-xsrf-token", - "x-session-token", - "x-access-token", -]); - -function redactHeaders(headers: Record): Record { - const filtered: Record = {}; - for (const [key, value] of Object.entries(headers)) { - if (SENSITIVE_HEADERS.has(key.toLowerCase())) { - filtered[key] = "[REDACTED]"; - } else { - filtered[key] = value; - } - } - return filtered; -} - -const SENSITIVE_QUERY_PARAMS = new Set([ - "api_key", - "api-key", - "apikey", - "token", - "access_token", - "access-token", - "auth_token", - "auth-token", - "password", - "passwd", - "secret", - "api_secret", - "api-secret", - "apisecret", - "key", - "session", - "session_id", - "session-id", -]); - -function redactQueryParameters(queryParameters?: Record): Record | undefined { - if (queryParameters == null) { - return queryParameters; - } - const redacted: Record = {}; - for (const [key, value] of Object.entries(queryParameters)) { - if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) { - redacted[key] = "[REDACTED]"; - } else { - redacted[key] = value; - } - } - return redacted; -} - -function redactUrl(url: string): string { - const protocolIndex = url.indexOf("://"); - if (protocolIndex === -1) return url; - - const afterProtocol = protocolIndex + 3; - - // Find the first delimiter that marks the end of the authority section - const pathStart = url.indexOf("/", afterProtocol); - let queryStart = url.indexOf("?", afterProtocol); - let fragmentStart = url.indexOf("#", afterProtocol); - - const firstDelimiter = Math.min( - pathStart === -1 ? url.length : pathStart, - queryStart === -1 ? url.length : queryStart, - fragmentStart === -1 ? url.length : fragmentStart, - ); - - // Find the LAST @ before the delimiter (handles multiple @ in credentials) - let atIndex = -1; - for (let i = afterProtocol; i < firstDelimiter; i++) { - if (url[i] === "@") { - atIndex = i; - } - } - - if (atIndex !== -1) { - url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`; - } - - // Recalculate queryStart since url might have changed - queryStart = url.indexOf("?"); - if (queryStart === -1) return url; - - fragmentStart = url.indexOf("#", queryStart); - const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length; - const queryString = url.slice(queryStart + 1, queryEnd); - - if (queryString.length === 0) return url; - - // FAST PATH: Quick check if any sensitive keywords present - // Using indexOf is faster than regex for simple substring matching - const lower = queryString.toLowerCase(); - const hasSensitive = - lower.includes("token") || - lower.includes("key") || - lower.includes("password") || - lower.includes("passwd") || - lower.includes("secret") || - lower.includes("session") || - lower.includes("auth"); - - if (!hasSensitive) { - return url; - } - - // SLOW PATH: Parse and redact - const redactedParams: string[] = []; - const params = queryString.split("&"); - - for (const param of params) { - const equalIndex = param.indexOf("="); - if (equalIndex === -1) { - redactedParams.push(param); - continue; - } - - const key = param.slice(0, equalIndex); - let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()); - - if (!shouldRedact && key.includes("%")) { - try { - const decodedKey = decodeURIComponent(key); - shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase()); - } catch {} - } - - redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param); - } - - return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd); -} - async function getHeaders(args: Fetcher.Args): Promise> { const newHeaders: Record = {}; if (args.body !== undefined && args.contentType != null) { @@ -235,22 +83,9 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise= 200 && response.status < 400) { - if (logger.isDebug()) { - const metadata = { - method: args.method, - url: redactUrl(url), - statusCode: response.status, - responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())), - }; - logger.debug("HTTP request succeeded", metadata); - } return { ok: true, body: (await getResponseBody(response, args.responseType)) as R, @@ -286,15 +112,6 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise case "application/ld+json": case "application/problem+json": case "application/vnd.api+json": - case "text/json": { + case "text/json": const text = await response.text(); return text.length > 0 ? fromJson(text) : undefined; - } default: if (contentType.startsWith("application/vnd.") && contentType.endsWith("+json")) { const text = await response.text(); diff --git a/src/core/fetcher/getRequestBody.ts b/src/core/fetcher/getRequestBody.ts index 91d9d81..e38457c 100644 --- a/src/core/fetcher/getRequestBody.ts +++ b/src/core/fetcher/getRequestBody.ts @@ -1,17 +1,13 @@ import { toJson } from "../json.js"; -import { toQueryString } from "../url/qs.js"; export declare namespace GetRequestBody { interface Args { body: unknown; - type: "json" | "file" | "bytes" | "form" | "other"; + type: "json" | "file" | "bytes" | "other"; } } export async function getRequestBody({ body, type }: GetRequestBody.Args): Promise { - if (type === "form") { - return toQueryString(body, { arrayFormat: "repeat", encode: true }); - } if (type.includes("json")) { return toJson(body); } else { diff --git a/src/core/fetcher/getResponseBody.ts b/src/core/fetcher/getResponseBody.ts index 0f24de1..7ca8b3d 100644 --- a/src/core/fetcher/getResponseBody.ts +++ b/src/core/fetcher/getResponseBody.ts @@ -1,6 +1,6 @@ -import { fromJson } from "../json.js"; import { getBinaryResponse } from "./BinaryResponse.js"; import { isResponseWithBody } from "./ResponseWithBody.js"; +import { fromJson } from "../json.js"; export async function getResponseBody(response: Response, responseType?: string): Promise { if (!isResponseWithBody(response)) { @@ -26,9 +26,9 @@ export async function getResponseBody(response: Response, responseType?: string) const text = await response.text(); if (text.length > 0) { try { - const responseBody = fromJson(text); + let responseBody = fromJson(text); return responseBody; - } catch (_err) { + } catch (err) { return { ok: false, error: { diff --git a/src/core/fetcher/makeRequest.ts b/src/core/fetcher/makeRequest.ts index c78e700..1a5ffd3 100644 --- a/src/core/fetcher/makeRequest.ts +++ b/src/core/fetcher/makeRequest.ts @@ -13,17 +13,19 @@ export const makeRequest = async ( ): Promise => { const signals: AbortSignal[] = []; - let timeoutAbortId: NodeJS.Timeout | undefined; + // Add timeout signal + let timeoutAbortId: NodeJS.Timeout | undefined = undefined; if (timeoutMs != null) { const { signal, abortId } = getTimeoutSignal(timeoutMs); timeoutAbortId = abortId; signals.push(signal); } + // Add arbitrary signal if (abortSignal != null) { signals.push(abortSignal); } - const newSignals = anySignal(signals); + let newSignals = anySignal(signals); const response = await fetchFn(url, { method: method, headers, diff --git a/src/core/fetcher/requestWithRetries.ts b/src/core/fetcher/requestWithRetries.ts index 1f68968..560432e 100644 --- a/src/core/fetcher/requestWithRetries.ts +++ b/src/core/fetcher/requestWithRetries.ts @@ -4,25 +4,30 @@ const DEFAULT_MAX_RETRIES = 2; const JITTER_FACTOR = 0.2; // 20% random jitter function addPositiveJitter(delay: number): number { + // Generate a random value between 0 and +JITTER_FACTOR const jitterMultiplier = 1 + Math.random() * JITTER_FACTOR; return delay * jitterMultiplier; } function addSymmetricJitter(delay: number): number { + // Generate a random value in a JITTER_FACTOR-sized percentage range around delay const jitterMultiplier = 1 + (Math.random() - 0.5) * JITTER_FACTOR; return delay * jitterMultiplier; } function getRetryDelayFromHeaders(response: Response, retryAttempt: number): number { + // Check for Retry-After header first (RFC 7231), with no jitter const retryAfter = response.headers.get("Retry-After"); if (retryAfter) { + // Parse as number of seconds... const retryAfterSeconds = parseInt(retryAfter, 10); - if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) { + if (!isNaN(retryAfterSeconds) && retryAfterSeconds > 0) { return Math.min(retryAfterSeconds * 1000, MAX_RETRY_DELAY); } + // ...or as an HTTP date; both are valid const retryAfterDate = new Date(retryAfter); - if (!Number.isNaN(retryAfterDate.getTime())) { + if (!isNaN(retryAfterDate.getTime())) { const delay = retryAfterDate.getTime() - Date.now(); if (delay > 0) { return Math.min(Math.max(delay, 0), MAX_RETRY_DELAY); @@ -30,10 +35,12 @@ function getRetryDelayFromHeaders(response: Response, retryAttempt: number): num } } + // Then check for industry-standard X-RateLimit-Reset header, with positive jitter const rateLimitReset = response.headers.get("X-RateLimit-Reset"); if (rateLimitReset) { const resetTime = parseInt(rateLimitReset, 10); - if (!Number.isNaN(resetTime)) { + if (!isNaN(resetTime)) { + // Assume Unix timestamp in epoch seconds const delay = resetTime * 1000 - Date.now(); if (delay > 0) { return addPositiveJitter(Math.min(delay, MAX_RETRY_DELAY)); @@ -41,7 +48,8 @@ function getRetryDelayFromHeaders(response: Response, retryAttempt: number): num } } - return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** retryAttempt, MAX_RETRY_DELAY)); + // Fall back to exponential backoff, with symmetric jitter + return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * Math.pow(2, retryAttempt), MAX_RETRY_DELAY)); } export async function requestWithRetries( @@ -52,6 +60,7 @@ export async function requestWithRetries( for (let i = 0; i < maxRetries; ++i) { if ([408, 429].includes(response.status) || response.status >= 500) { + // Get delay with appropriate jitter applied const delay = getRetryDelayFromHeaders(response, i); await new Promise((resolve) => setTimeout(resolve, delay)); diff --git a/src/core/fetcher/signals.ts b/src/core/fetcher/signals.ts index c9fcaef..a8d32a2 100644 --- a/src/core/fetcher/signals.ts +++ b/src/core/fetcher/signals.ts @@ -6,17 +6,29 @@ export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abor return { signal: controller.signal, abortId }; } +/** + * Returns an abort signal that is getting aborted when + * at least one of the specified abort signals is aborted. + * + * Requires at least node.js 18. + */ export function anySignal(...args: AbortSignal[] | [AbortSignal[]]): AbortSignal { + // Allowing signals to be passed either as array + // of signals or as multiple arguments. const signals = (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as AbortSignal[]; const controller = new AbortController(); for (const signal of signals) { if (signal.aborted) { + // Exiting early if one of the signals + // is already aborted. controller.abort((signal as any)?.reason); break; } + // Listening for signals and removing the listeners + // when at least one symbol is aborted. signal.addEventListener("abort", () => controller.abort((signal as any)?.reason), { signal: controller.signal, }); diff --git a/src/core/headers.ts b/src/core/headers.ts index 78ed8b5..a723d22 100644 --- a/src/core/headers.ts +++ b/src/core/headers.ts @@ -6,11 +6,10 @@ export function mergeHeaders( for (const [key, value] of headersArray .filter((headers) => headers != null) .flatMap((headers) => Object.entries(headers))) { - const insensitiveKey = key.toLowerCase(); if (value != null) { - result[insensitiveKey] = value; - } else if (insensitiveKey in result) { - delete result[insensitiveKey]; + result[key] = value; + } else if (key in result) { + delete result[key]; } } @@ -25,9 +24,8 @@ export function mergeOnlyDefinedHeaders( for (const [key, value] of headersArray .filter((headers) => headers != null) .flatMap((headers) => Object.entries(headers))) { - const insensitiveKey = key.toLowerCase(); if (value != null) { - result[insensitiveKey] = value; + result[key] = value; } } diff --git a/src/core/index.ts b/src/core/index.ts index afa8351..bbb640d 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,3 @@ export * from "./fetcher/index.js"; -export * as logging from "./logging/index.js"; export * from "./runtime/index.js"; export * as url from "./url/index.js"; diff --git a/src/core/logging/exports.ts b/src/core/logging/exports.ts deleted file mode 100644 index 88f6c00..0000000 --- a/src/core/logging/exports.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as logger from "./logger.js"; - -export namespace logging { - /** - * Configuration for logger instances. - */ - export type LogConfig = logger.LogConfig; - export type LogLevel = logger.LogLevel; - export const LogLevel: typeof logger.LogLevel = logger.LogLevel; - export type ILogger = logger.ILogger; - /** - * Console logger implementation that outputs to the console. - */ - export type ConsoleLogger = logger.ConsoleLogger; - /** - * Console logger implementation that outputs to the console. - */ - export const ConsoleLogger: typeof logger.ConsoleLogger = logger.ConsoleLogger; -} diff --git a/src/core/logging/index.ts b/src/core/logging/index.ts deleted file mode 100644 index d81cc32..0000000 --- a/src/core/logging/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./logger.js"; diff --git a/src/core/logging/logger.ts b/src/core/logging/logger.ts deleted file mode 100644 index a3f3673..0000000 --- a/src/core/logging/logger.ts +++ /dev/null @@ -1,203 +0,0 @@ -export const LogLevel = { - Debug: "debug", - Info: "info", - Warn: "warn", - Error: "error", -} as const; -export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; -const logLevelMap: Record = { - [LogLevel.Debug]: 1, - [LogLevel.Info]: 2, - [LogLevel.Warn]: 3, - [LogLevel.Error]: 4, -}; - -export interface ILogger { - /** - * Logs a debug message. - * @param message - The message to log - * @param args - Additional arguments to log - */ - debug(message: string, ...args: unknown[]): void; - /** - * Logs an info message. - * @param message - The message to log - * @param args - Additional arguments to log - */ - info(message: string, ...args: unknown[]): void; - /** - * Logs a warning message. - * @param message - The message to log - * @param args - Additional arguments to log - */ - warn(message: string, ...args: unknown[]): void; - /** - * Logs an error message. - * @param message - The message to log - * @param args - Additional arguments to log - */ - error(message: string, ...args: unknown[]): void; -} - -/** - * Configuration for logger initialization. - */ -export interface LogConfig { - /** - * Minimum log level to output. - * @default LogLevel.Info - */ - level?: LogLevel; - /** - * Logger implementation to use. - * @default new ConsoleLogger() - */ - logger?: ILogger; - /** - * Whether logging should be silenced. - * @default true - */ - silent?: boolean; -} - -/** - * Default console-based logger implementation. - */ -export class ConsoleLogger implements ILogger { - debug(message: string, ...args: unknown[]): void { - console.debug(message, ...args); - } - info(message: string, ...args: unknown[]): void { - console.info(message, ...args); - } - warn(message: string, ...args: unknown[]): void { - console.warn(message, ...args); - } - error(message: string, ...args: unknown[]): void { - console.error(message, ...args); - } -} - -/** - * Logger class that provides level-based logging functionality. - */ -export class Logger { - private readonly level: number; - private readonly logger: ILogger; - private readonly silent: boolean; - - /** - * Creates a new logger instance. - * @param config - Logger configuration - */ - constructor(config: Required) { - this.level = logLevelMap[config.level]; - this.logger = config.logger; - this.silent = config.silent; - } - - /** - * Checks if a log level should be output based on configuration. - * @param level - The log level to check - * @returns True if the level should be logged - */ - public shouldLog(level: LogLevel): boolean { - return !this.silent && this.level <= logLevelMap[level]; - } - - /** - * Checks if debug logging is enabled. - * @returns True if debug logs should be output - */ - public isDebug(): boolean { - return this.shouldLog(LogLevel.Debug); - } - - /** - * Logs a debug message if debug logging is enabled. - * @param message - The message to log - * @param args - Additional arguments to log - */ - public debug(message: string, ...args: unknown[]): void { - if (this.isDebug()) { - this.logger.debug(message, ...args); - } - } - - /** - * Checks if info logging is enabled. - * @returns True if info logs should be output - */ - public isInfo(): boolean { - return this.shouldLog(LogLevel.Info); - } - - /** - * Logs an info message if info logging is enabled. - * @param message - The message to log - * @param args - Additional arguments to log - */ - public info(message: string, ...args: unknown[]): void { - if (this.isInfo()) { - this.logger.info(message, ...args); - } - } - - /** - * Checks if warning logging is enabled. - * @returns True if warning logs should be output - */ - public isWarn(): boolean { - return this.shouldLog(LogLevel.Warn); - } - - /** - * Logs a warning message if warning logging is enabled. - * @param message - The message to log - * @param args - Additional arguments to log - */ - public warn(message: string, ...args: unknown[]): void { - if (this.isWarn()) { - this.logger.warn(message, ...args); - } - } - - /** - * Checks if error logging is enabled. - * @returns True if error logs should be output - */ - public isError(): boolean { - return this.shouldLog(LogLevel.Error); - } - - /** - * Logs an error message if error logging is enabled. - * @param message - The message to log - * @param args - Additional arguments to log - */ - public error(message: string, ...args: unknown[]): void { - if (this.isError()) { - this.logger.error(message, ...args); - } - } -} - -export function createLogger(config?: LogConfig | Logger): Logger { - if (config == null) { - return defaultLogger; - } - if (config instanceof Logger) { - return config; - } - config = config ?? {}; - config.level ??= LogLevel.Info; - config.logger ??= new ConsoleLogger(); - config.silent ??= true; - return new Logger(config as Required); -} - -const defaultLogger: Logger = new Logger({ - level: LogLevel.Info, - logger: new ConsoleLogger(), - silent: true, -}); diff --git a/src/core/url/encodePathParam.ts b/src/core/url/encodePathParam.ts deleted file mode 100644 index 19b9012..0000000 --- a/src/core/url/encodePathParam.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function encodePathParam(param: unknown): string { - if (param === null) { - return "null"; - } - const typeofParam = typeof param; - switch (typeofParam) { - case "undefined": - return "undefined"; - case "string": - case "number": - case "boolean": - break; - default: - param = String(param); - break; - } - return encodeURIComponent(param as string | number | boolean); -} diff --git a/src/core/url/index.ts b/src/core/url/index.ts index f2e0fa2..ed5aa0f 100644 --- a/src/core/url/index.ts +++ b/src/core/url/index.ts @@ -1,3 +1,2 @@ -export { encodePathParam } from "./encodePathParam.js"; export { join } from "./join.js"; export { toQueryString } from "./qs.js"; diff --git a/src/core/url/join.ts b/src/core/url/join.ts index 7ca7dae..200426b 100644 --- a/src/core/url/join.ts +++ b/src/core/url/join.ts @@ -12,11 +12,12 @@ export function join(base: string, ...segments: string[]): string { try { url = new URL(base); } catch { + // Fallback to path joining if URL is malformed return joinPath(base, ...segments); } const lastSegment = segments[segments.length - 1]; - const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + const shouldPreserveTrailingSlash = lastSegment && lastSegment.endsWith("/"); for (const segment of segments) { const cleanSegment = trimSlashes(segment); @@ -43,7 +44,7 @@ function joinPath(base: string, ...segments: string[]): string { let result = base; const lastSegment = segments[segments.length - 1]; - const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + const shouldPreserveTrailingSlash = lastSegment && lastSegment.endsWith("/"); for (const segment of segments) { const cleanSegment = trimSlashes(segment); @@ -63,7 +64,7 @@ function joinPathSegments(left: string, right: string): string { if (left.endsWith("/")) { return left + right; } - return `${left}/${right}`; + return left + "/" + right; } function trimSlashes(str: string): string { diff --git a/src/errors/FernAutopilotTestApiError.ts b/src/errors/FernAutopilotTestApiError.ts index e16087b..665e8fe 100644 --- a/src/errors/FernAutopilotTestApiError.ts +++ b/src/errors/FernAutopilotTestApiError.ts @@ -1,6 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -import type * as core from "../core/index.js"; +import * as core from "../core/index.js"; import { toJson } from "../core/json.js"; export class FernAutopilotTestApiError extends Error { @@ -36,7 +36,7 @@ function buildMessage({ statusCode: number | undefined; body: unknown | undefined; }): string { - const lines: string[] = []; + let lines: string[] = []; if (message != null) { lines.push(message); } diff --git a/src/exports.ts b/src/exports.ts deleted file mode 100644 index 7b70ee1..0000000 --- a/src/exports.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./core/exports.js"; diff --git a/src/index.ts b/src/index.ts index 830815b..d2212bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ export * as FernAutopilotTestApi from "./api/index.js"; -export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; -export { FernAutopilotTestApiClient } from "./Client.js"; export { FernAutopilotTestApiError, FernAutopilotTestApiTimeoutError } from "./errors/index.js"; -export * from "./exports.js"; +export { FernAutopilotTestApiClient } from "./Client.js"; diff --git a/src/version.ts b/src/version.ts index 478f50d..85cbd4f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = "2.0.0"; +export const SDK_VERSION = "2.0.1"; diff --git a/tests/mock-server/MockServer.ts b/tests/mock-server/MockServer.ts index 5b30fe7..6e258f1 100644 --- a/tests/mock-server/MockServer.ts +++ b/tests/mock-server/MockServer.ts @@ -1,4 +1,4 @@ -import type { RequestHandlerOptions } from "msw"; +import { RequestHandlerOptions } from "msw"; import type { SetupServer } from "msw/node"; import { mockEndpointBuilder } from "./mockEndpointBuilder"; diff --git a/tests/mock-server/MockServerPool.ts b/tests/mock-server/MockServerPool.ts index e1a90f7..8160806 100644 --- a/tests/mock-server/MockServerPool.ts +++ b/tests/mock-server/MockServerPool.ts @@ -22,7 +22,7 @@ async function formatHttpRequest(request: Request, id?: string): Promise } else if (clone.body) { body = await clone.text(); } - } catch (_e) { + } catch (e) { body = "(unable to parse body)"; } @@ -48,7 +48,7 @@ async function formatHttpResponse(response: Response, id?: string): Promise { const formattedRequest = await formatHttpRequest(request, requestId); - console.debug(`request:start\n${formattedRequest}`); + console.debug("request:start\n" + formattedRequest); }); mswServer.events.on("request:unhandled", async ({ request, requestId }) => { const formattedRequest = await formatHttpRequest(request, requestId); - console.debug(`request:unhandled\n${formattedRequest}`); + console.debug("request:unhandled\n" + formattedRequest); }); mswServer.events.on("response:mocked", async ({ request, response, requestId }) => { const formattedResponse = await formatHttpResponse(response, requestId); - console.debug(`response:mocked\n${formattedResponse}`); + console.debug("response:mocked\n" + formattedResponse); }); } } diff --git a/tests/mock-server/mockEndpointBuilder.ts b/tests/mock-server/mockEndpointBuilder.ts index 1b0e510..88368d4 100644 --- a/tests/mock-server/mockEndpointBuilder.ts +++ b/tests/mock-server/mockEndpointBuilder.ts @@ -1,8 +1,7 @@ -import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponseResolver, http } from "msw"; +import { DefaultBodyType, HttpHandler, HttpResponse, HttpResponseResolver, http } from "msw"; import { url } from "../../src/core"; import { toJson } from "../../src/core/json"; -import { withFormUrlEncoded } from "./withFormUrlEncoded"; import { withHeaders } from "./withHeaders"; import { withJson } from "./withJson"; @@ -27,7 +26,6 @@ interface RequestHeadersStage extends RequestBodyStage, ResponseStage { interface RequestBodyStage extends ResponseStage { jsonBody(body: unknown): ResponseStage; - formUrlEncodedBody(body: unknown): ResponseStage; } interface ResponseStage { @@ -137,16 +135,6 @@ class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodySta return this; } - formUrlEncodedBody(body: unknown): ResponseStage { - if (body === undefined) { - throw new Error( - "Undefined is not valid for form-urlencoded. Do not call formUrlEncodedBody if you want an empty body.", - ); - } - this.predicates.push((resolver) => withFormUrlEncoded(body, resolver)); - return this; - } - respondWith(): ResponseStatusStage { return new ResponseBuilder(this.method, this.buildUrl(), this.predicates, this.handlerOptions); } diff --git a/tests/mock-server/withFormUrlEncoded.ts b/tests/mock-server/withFormUrlEncoded.ts deleted file mode 100644 index e9e6ff2..0000000 --- a/tests/mock-server/withFormUrlEncoded.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { type HttpResponseResolver, passthrough } from "msw"; - -import { toJson } from "../../src/core/json"; - -/** - * Creates a request matcher that validates if the request form-urlencoded body exactly matches the expected object - * @param expectedBody - The exact body object to match against - * @param resolver - Response resolver to execute if body matches - */ -export function withFormUrlEncoded(expectedBody: unknown, resolver: HttpResponseResolver): HttpResponseResolver { - return async (args) => { - const { request } = args; - - let clonedRequest: Request; - let bodyText: string | undefined; - let actualBody: Record; - try { - clonedRequest = request.clone(); - bodyText = await clonedRequest.text(); - if (bodyText === "") { - console.error("Request body is empty, expected a form-urlencoded body."); - return passthrough(); - } - const params = new URLSearchParams(bodyText); - actualBody = {}; - for (const [key, value] of params.entries()) { - actualBody[key] = value; - } - } catch (error) { - console.error(`Error processing form-urlencoded request body:\n\tError: ${error}\n\tBody: ${bodyText}`); - return passthrough(); - } - - const mismatches = findMismatches(actualBody, expectedBody); - if (Object.keys(mismatches).length > 0) { - console.error("Form-urlencoded body mismatch:", toJson(mismatches, undefined, 2)); - return passthrough(); - } - - return resolver(args); - }; -} - -function findMismatches(actual: any, expected: any): Record { - const mismatches: Record = {}; - - if (typeof actual !== typeof expected) { - return { value: { actual, expected } }; - } - - if (typeof actual !== "object" || actual === null || expected === null) { - if (actual !== expected) { - return { value: { actual, expected } }; - } - return {}; - } - - const actualKeys = Object.keys(actual); - const expectedKeys = Object.keys(expected); - - const allKeys = new Set([...actualKeys, ...expectedKeys]); - - for (const key of allKeys) { - if (!expectedKeys.includes(key)) { - if (actual[key] === undefined) { - continue; - } - mismatches[key] = { actual: actual[key], expected: undefined }; - } else if (!actualKeys.includes(key)) { - if (expected[key] === undefined) { - continue; - } - mismatches[key] = { actual: undefined, expected: expected[key] }; - } else if (actual[key] !== expected[key]) { - mismatches[key] = { actual: actual[key], expected: expected[key] }; - } - } - - return mismatches; -} diff --git a/tests/mock-server/withHeaders.ts b/tests/mock-server/withHeaders.ts index 6599d2b..e77c837 100644 --- a/tests/mock-server/withHeaders.ts +++ b/tests/mock-server/withHeaders.ts @@ -1,4 +1,4 @@ -import { type HttpResponseResolver, passthrough } from "msw"; +import { HttpResponseResolver, passthrough } from "msw"; /** * Creates a request matcher that validates if request headers match specified criteria diff --git a/tests/mock-server/withJson.ts b/tests/mock-server/withJson.ts index b627638..03f585d 100644 --- a/tests/mock-server/withJson.ts +++ b/tests/mock-server/withJson.ts @@ -1,4 +1,4 @@ -import { type HttpResponseResolver, passthrough } from "msw"; +import { HttpResponseResolver, passthrough } from "msw"; import { fromJson, toJson } from "../../src/core/json"; @@ -67,7 +67,7 @@ function findMismatches(actual: any, expected: any): Record 0) { for (const [mismatchKey, mismatchValue] of Object.entries(itemMismatches)) { - arrayMismatches[`[${i}]${mismatchKey === "value" ? "" : `.${mismatchKey}`}`] = mismatchValue; + arrayMismatches[`[${i}]${mismatchKey === "value" ? "" : "." + mismatchKey}`] = mismatchValue; } } } @@ -99,7 +99,7 @@ function findMismatches(actual: any, expected: any): Record 0) { for (const [nestedKey, nestedValue] of Object.entries(nestedMismatches)) { - mismatches[`${key}${nestedKey === "value" ? "" : `.${nestedKey}`}`] = nestedValue; + mismatches[`${key}${nestedKey === "value" ? "" : "." + nestedKey}`] = nestedValue; } } } else if (actual[key] !== expected[key]) { diff --git a/tests/unit/fetcher/Fetcher.test.ts b/tests/unit/fetcher/Fetcher.test.ts index a8d458f..80665d7 100644 --- a/tests/unit/fetcher/Fetcher.test.ts +++ b/tests/unit/fetcher/Fetcher.test.ts @@ -1,8 +1,9 @@ import fs from "fs"; -import { join } from "path"; import stream from "stream"; +import { join } from "path"; + +import { Fetcher, fetcherImpl } from "../../../src/core/fetcher/Fetcher"; import type { BinaryResponse } from "../../../src/core"; -import { type Fetcher, fetcherImpl } from "../../../src/core/fetcher/Fetcher"; describe("Test fetcherImpl", () => { it("should handle successful request", async () => { @@ -13,7 +14,6 @@ describe("Test fetcherImpl", () => { body: { data: "test" }, contentType: "application/json", requestType: "json", - maxRetries: 0, responseType: "json", }; @@ -48,7 +48,6 @@ describe("Test fetcherImpl", () => { headers: { "X-Test": "x-test-header" }, contentType: "application/octet-stream", requestType: "bytes", - maxRetries: 0, responseType: "json", body: fs.createReadStream(join(__dirname, "test-file.txt")), }; @@ -82,7 +81,6 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, - maxRetries: 0, responseType: "binary-response", }; @@ -128,7 +126,6 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, - maxRetries: 0, responseType: "binary-response", }; @@ -174,7 +171,6 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, - maxRetries: 0, responseType: "binary-response", }; @@ -218,7 +214,6 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, - maxRetries: 0, responseType: "binary-response", }; diff --git a/tests/unit/fetcher/HttpResponsePromise.test.ts b/tests/unit/fetcher/HttpResponsePromise.test.ts index 2ec008e..c48ac30 100644 --- a/tests/unit/fetcher/HttpResponsePromise.test.ts +++ b/tests/unit/fetcher/HttpResponsePromise.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { HttpResponsePromise } from "../../../src/core/fetcher/HttpResponsePromise"; -import type { RawResponse, WithRawResponse } from "../../../src/core/fetcher/RawResponse"; +import { RawResponse, WithRawResponse } from "../../../src/core/fetcher/RawResponse"; describe("HttpResponsePromise", () => { const mockRawResponse: RawResponse = { diff --git a/tests/unit/fetcher/createRequestUrl.test.ts b/tests/unit/fetcher/createRequestUrl.test.ts index a92f1b5..06e03b2 100644 --- a/tests/unit/fetcher/createRequestUrl.test.ts +++ b/tests/unit/fetcher/createRequestUrl.test.ts @@ -1,163 +1,160 @@ import { createRequestUrl } from "../../../src/core/fetcher/createRequestUrl"; describe("Test createRequestUrl", () => { - const BASE_URL = "https://api.example.com"; - - interface TestCase { - description: string; - baseUrl: string; - queryParams?: Record; - expected: string; - } - - const testCases: TestCase[] = [ - { - description: "should return the base URL when no query parameters are provided", - baseUrl: BASE_URL, - expected: BASE_URL, - }, - { - description: "should append simple query parameters", - baseUrl: BASE_URL, - queryParams: { key: "value", another: "param" }, - expected: "https://api.example.com?key=value&another=param", - }, - { - description: "should handle array query parameters", - baseUrl: BASE_URL, - queryParams: { items: ["a", "b", "c"] }, - expected: "https://api.example.com?items=a&items=b&items=c", - }, - { - description: "should handle object query parameters", - baseUrl: BASE_URL, - queryParams: { filter: { name: "John", age: 30 } }, - expected: "https://api.example.com?filter%5Bname%5D=John&filter%5Bage%5D=30", - }, - { - description: "should handle mixed types of query parameters", - baseUrl: BASE_URL, - queryParams: { - simple: "value", - array: ["x", "y"], - object: { key: "value" }, - }, - expected: "https://api.example.com?simple=value&array=x&array=y&object%5Bkey%5D=value", - }, - { - description: "should handle empty query parameters object", - baseUrl: BASE_URL, - queryParams: {}, - expected: BASE_URL, - }, - { - description: "should encode special characters in query parameters", - baseUrl: BASE_URL, - queryParams: { special: "a&b=c d" }, - expected: "https://api.example.com?special=a%26b%3Dc%20d", - }, - { - description: "should handle numeric values", - baseUrl: BASE_URL, - queryParams: { count: 42, price: 19.99, active: 1, inactive: 0 }, - expected: "https://api.example.com?count=42&price=19.99&active=1&inactive=0", - }, - { - description: "should handle boolean values", - baseUrl: BASE_URL, - queryParams: { enabled: true, disabled: false }, - expected: "https://api.example.com?enabled=true&disabled=false", - }, - { - description: "should handle null and undefined values", - baseUrl: BASE_URL, - queryParams: { - valid: "value", - nullValue: null, - undefinedValue: undefined, - emptyString: "", - }, - expected: "https://api.example.com?valid=value&nullValue=&emptyString=", - }, - { - description: "should handle deeply nested objects", - baseUrl: BASE_URL, - queryParams: { - user: { - profile: { - name: "John", - settings: { theme: "dark" }, - }, + it("should return the base URL when no query parameters are provided", () => { + const baseUrl = "https://api.example.com"; + expect(createRequestUrl(baseUrl)).toBe(baseUrl); + }); + + it("should append simple query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { key: "value", another: "param" }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?key=value&another=param"); + }); + + it("should handle array query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { items: ["a", "b", "c"] }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?items=a&items=b&items=c"); + }); + + it("should handle object query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { filter: { name: "John", age: 30 } }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?filter%5Bname%5D=John&filter%5Bage%5D=30", + ); + }); + + it("should handle mixed types of query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + simple: "value", + array: ["x", "y"], + object: { key: "value" }, + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?simple=value&array=x&array=y&object%5Bkey%5D=value", + ); + }); + + it("should handle empty query parameters object", () => { + const baseUrl = "https://api.example.com"; + expect(createRequestUrl(baseUrl, {})).toBe(baseUrl); + }); + + it("should encode special characters in query parameters", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { special: "a&b=c d" }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?special=a%26b%3Dc%20d"); + }); + + // Additional tests for edge cases and different value types + it("should handle numeric values", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { count: 42, price: 19.99, active: 1, inactive: 0 }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?count=42&price=19.99&active=1&inactive=0", + ); + }); + + it("should handle boolean values", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { enabled: true, disabled: false }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?enabled=true&disabled=false"); + }); + + it("should handle null and undefined values", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + valid: "value", + nullValue: null, + undefinedValue: undefined, + emptyString: "", + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?valid=value&nullValue=&emptyString=", + ); + }); + + it("should handle deeply nested objects", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + user: { + profile: { + name: "John", + settings: { theme: "dark" }, }, }, - expected: - "https://api.example.com?user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", - }, - { - description: "should handle arrays of objects", - baseUrl: BASE_URL, - queryParams: { - users: [ - { name: "John", age: 30 }, - { name: "Jane", age: 25 }, - ], - }, - expected: - "https://api.example.com?users%5Bname%5D=John&users%5Bage%5D=30&users%5Bname%5D=Jane&users%5Bage%5D=25", - }, - { - description: "should handle mixed arrays", - baseUrl: BASE_URL, - queryParams: { - mixed: ["string", 42, true, { key: "value" }], - }, - expected: "https://api.example.com?mixed=string&mixed=42&mixed=true&mixed%5Bkey%5D=value", - }, - { - description: "should handle empty arrays", - baseUrl: BASE_URL, - queryParams: { emptyArray: [] }, - expected: BASE_URL, - }, - { - description: "should handle empty objects", - baseUrl: BASE_URL, - queryParams: { emptyObject: {} }, - expected: BASE_URL, - }, - { - description: "should handle special characters in keys", - baseUrl: BASE_URL, - queryParams: { "key with spaces": "value", "key[with]brackets": "value" }, - expected: "https://api.example.com?key%20with%20spaces=value&key%5Bwith%5Dbrackets=value", - }, - { - description: "should handle URL with existing query parameters", - baseUrl: "https://api.example.com?existing=param", - queryParams: { new: "value" }, - expected: "https://api.example.com?existing=param?new=value", - }, - { - description: "should handle complex nested structures", - baseUrl: BASE_URL, - queryParams: { - filters: { - status: ["active", "pending"], - category: { - type: "electronics", - subcategories: ["phones", "laptops"], - }, + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", + ); + }); + + it("should handle arrays of objects", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + users: [ + { name: "John", age: 30 }, + { name: "Jane", age: 25 }, + ], + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?users%5Bname%5D=John&users%5Bage%5D=30&users%5Bname%5D=Jane&users%5Bage%5D=25", + ); + }); + + it("should handle mixed arrays", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + mixed: ["string", 42, true, { key: "value" }], + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?mixed=string&mixed=42&mixed=true&mixed%5Bkey%5D=value", + ); + }); + + it("should handle empty arrays", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { emptyArray: [] }; + expect(createRequestUrl(baseUrl, queryParams)).toBe(baseUrl); + }); + + it("should handle empty objects", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { emptyObject: {} }; + expect(createRequestUrl(baseUrl, queryParams)).toBe(baseUrl); + }); + + it("should handle special characters in keys", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { "key with spaces": "value", "key[with]brackets": "value" }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?key%20with%20spaces=value&key%5Bwith%5Dbrackets=value", + ); + }); + + it("should handle URL with existing query parameters", () => { + const baseUrl = "https://api.example.com?existing=param"; + const queryParams = { new: "value" }; + expect(createRequestUrl(baseUrl, queryParams)).toBe("https://api.example.com?existing=param?new=value"); + }); + + it("should handle complex nested structures", () => { + const baseUrl = "https://api.example.com"; + const queryParams = { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], }, - sort: { field: "name", direction: "asc" }, }, - expected: - "https://api.example.com?filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", - }, - ]; - - testCases.forEach(({ description, baseUrl, queryParams, expected }) => { - it(description, () => { - expect(createRequestUrl(baseUrl, queryParams)).toBe(expected); - }); + sort: { field: "name", direction: "asc" }, + }; + expect(createRequestUrl(baseUrl, queryParams)).toBe( + "https://api.example.com?filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + ); }); }); diff --git a/tests/unit/fetcher/getRequestBody.test.ts b/tests/unit/fetcher/getRequestBody.test.ts index 8a6c3a5..e864c8b 100644 --- a/tests/unit/fetcher/getRequestBody.test.ts +++ b/tests/unit/fetcher/getRequestBody.test.ts @@ -2,117 +2,15 @@ import { getRequestBody } from "../../../src/core/fetcher/getRequestBody"; import { RUNTIME } from "../../../src/core/runtime"; describe("Test getRequestBody", () => { - interface TestCase { - description: string; - input: any; - type: "json" | "form" | "file" | "bytes" | "other"; - expected: any; - skipCondition?: () => boolean; - } - - const testCases: TestCase[] = [ - { - description: "should stringify body if not FormData in Node environment", - input: { key: "value" }, - type: "json", - expected: '{"key":"value"}', - skipCondition: () => RUNTIME.type !== "node", - }, - { - description: "should stringify body if not FormData in browser environment", - input: { key: "value" }, - type: "json", - expected: '{"key":"value"}', - skipCondition: () => RUNTIME.type !== "browser", - }, - { - description: "should return the Uint8Array", - input: new Uint8Array([1, 2, 3]), - type: "bytes", - expected: new Uint8Array([1, 2, 3]), - }, - { - description: "should serialize objects for form-urlencoded content type", - input: { username: "johndoe", email: "john@example.com" }, - type: "form", - expected: "username=johndoe&email=john%40example.com", - }, - { - description: "should serialize complex nested objects and arrays for form-urlencoded content type", - input: { - user: { - profile: { - name: "John Doe", - settings: { - theme: "dark", - notifications: true, - }, - }, - tags: ["admin", "user"], - contacts: [ - { type: "email", value: "john@example.com" }, - { type: "phone", value: "+1234567890" }, - ], - }, - filters: { - status: ["active", "pending"], - metadata: { - created: "2024-01-01", - categories: ["electronics", "books"], - }, - }, - preferences: ["notifications", "updates"], - }, - type: "form", - expected: - "user%5Bprofile%5D%5Bname%5D=John%20Doe&" + - "user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark&" + - "user%5Bprofile%5D%5Bsettings%5D%5Bnotifications%5D=true&" + - "user%5Btags%5D=admin&" + - "user%5Btags%5D=user&" + - "user%5Bcontacts%5D%5Btype%5D=email&" + - "user%5Bcontacts%5D%5Bvalue%5D=john%40example.com&" + - "user%5Bcontacts%5D%5Btype%5D=phone&" + - "user%5Bcontacts%5D%5Bvalue%5D=%2B1234567890&" + - "filters%5Bstatus%5D=active&" + - "filters%5Bstatus%5D=pending&" + - "filters%5Bmetadata%5D%5Bcreated%5D=2024-01-01&" + - "filters%5Bmetadata%5D%5Bcategories%5D=electronics&" + - "filters%5Bmetadata%5D%5Bcategories%5D=books&" + - "preferences=notifications&" + - "preferences=updates", - }, - { - description: "should return the input for pre-serialized form-urlencoded strings", - input: "key=value&another=param", - type: "other", - expected: "key=value&another=param", - }, - { - description: "should JSON stringify objects", - input: { key: "value" }, - type: "json", - expected: '{"key":"value"}', - }, - ]; - - testCases.forEach(({ description, input, type, expected, skipCondition }) => { - it(description, async () => { - if (skipCondition?.()) { - return; - } - + it("should stringify body if not FormData in Node environment", async () => { + if (RUNTIME.type === "node") { + const body = { key: "value" }; const result = await getRequestBody({ - body: input, - type, + body, + type: "json", }); - - if (input instanceof Uint8Array) { - expect(result).toBe(input); - } else { - expect(result).toBe(expected); - } - }); + expect(result).toBe('{"key":"value"}'); + } }); it("should return FormData in browser environment", async () => { @@ -126,4 +24,42 @@ describe("Test getRequestBody", () => { expect(result).toBe(formData); } }); + + it("should stringify body if not FormData in browser environment", async () => { + if (RUNTIME.type === "browser") { + const body = { key: "value" }; + const result = await getRequestBody({ + body, + type: "json", + }); + expect(result).toBe('{"key":"value"}'); + } + }); + + it("should return the Uint8Array", async () => { + const input = new Uint8Array([1, 2, 3]); + const result = await getRequestBody({ + body: input, + type: "bytes", + }); + expect(result).toBe(input); + }); + + it("should return the input for content-type 'application/x-www-form-urlencoded'", async () => { + const input = "key=value&another=param"; + const result = await getRequestBody({ + body: input, + type: "other", + }); + expect(result).toBe(input); + }); + + it("should JSON stringify objects", async () => { + const input = { key: "value" }; + const result = await getRequestBody({ + body: input, + type: "json", + }); + expect(result).toBe('{"key":"value"}'); + }); }); diff --git a/tests/unit/fetcher/getResponseBody.test.ts b/tests/unit/fetcher/getResponseBody.test.ts index ad6be7f..400782f 100644 --- a/tests/unit/fetcher/getResponseBody.test.ts +++ b/tests/unit/fetcher/getResponseBody.test.ts @@ -1,61 +1,7 @@ -import { getResponseBody } from "../../../src/core/fetcher/getResponseBody"; - import { RUNTIME } from "../../../src/core/runtime"; +import { getResponseBody } from "../../../src/core/fetcher/getResponseBody"; describe("Test getResponseBody", () => { - interface SimpleTestCase { - description: string; - responseData: string | Record; - responseType?: "blob" | "sse" | "streaming" | "text"; - expected: any; - skipCondition?: () => boolean; - } - - const simpleTestCases: SimpleTestCase[] = [ - { - description: "should handle text response type", - responseData: "test text", - responseType: "text", - expected: "test text", - }, - { - description: "should handle JSON response", - responseData: { key: "value" }, - expected: { key: "value" }, - }, - { - description: "should handle empty response", - responseData: "", - expected: undefined, - }, - { - description: "should handle non-JSON response", - responseData: "invalid json", - expected: { - ok: false, - error: { - reason: "non-json", - statusCode: 200, - rawBody: "invalid json", - }, - }, - }, - ]; - - simpleTestCases.forEach(({ description, responseData, responseType, expected, skipCondition }) => { - it(description, async () => { - if (skipCondition?.()) { - return; - } - - const mockResponse = new Response( - typeof responseData === "string" ? responseData : JSON.stringify(responseData), - ); - const result = await getResponseBody(mockResponse, responseType); - expect(result).toEqual(expected); - }); - }); - it("should handle blob response type", async () => { const mockBlob = new Blob(["test"], { type: "text/plain" }); const mockResponse = new Response(mockBlob); @@ -74,6 +20,7 @@ describe("Test getResponseBody", () => { }); it("should handle streaming response type", async () => { + // Create a ReadableStream with some test data const encoder = new TextEncoder(); const testData = "test stream data"; const mockStream = new ReadableStream({ @@ -88,10 +35,43 @@ describe("Test getResponseBody", () => { expect(result).toBeInstanceOf(ReadableStream); + // Read and verify the stream content const reader = result.getReader(); const decoder = new TextDecoder(); const { value } = await reader.read(); const streamContent = decoder.decode(value); expect(streamContent).toBe(testData); }); + + it("should handle text response type", async () => { + const mockResponse = new Response("test text"); + const result = await getResponseBody(mockResponse, "text"); + expect(result).toBe("test text"); + }); + + it("should handle JSON response", async () => { + const mockJson = { key: "value" }; + const mockResponse = new Response(JSON.stringify(mockJson)); + const result = await getResponseBody(mockResponse); + expect(result).toEqual(mockJson); + }); + + it("should handle empty response", async () => { + const mockResponse = new Response(""); + const result = await getResponseBody(mockResponse); + expect(result).toBeUndefined(); + }); + + it("should handle non-JSON response", async () => { + const mockResponse = new Response("invalid json"); + const result = await getResponseBody(mockResponse); + expect(result).toEqual({ + ok: false, + error: { + reason: "non-json", + statusCode: 200, + rawBody: "invalid json", + }, + }); + }); }); diff --git a/tests/unit/fetcher/logging.test.ts b/tests/unit/fetcher/logging.test.ts deleted file mode 100644 index 4fbac8e..0000000 --- a/tests/unit/fetcher/logging.test.ts +++ /dev/null @@ -1,517 +0,0 @@ -import { fetcherImpl } from "../../../src/core/fetcher/Fetcher"; - -function createMockLogger() { - return { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; -} - -function mockSuccessResponse(data: unknown = { data: "test" }, status = 200, statusText = "OK") { - global.fetch = vi.fn().mockResolvedValue( - new Response(JSON.stringify(data), { - status, - statusText, - }), - ); -} - -function mockErrorResponse(data: unknown = { error: "Error" }, status = 404, statusText = "Not Found") { - global.fetch = vi.fn().mockResolvedValue( - new Response(JSON.stringify(data), { - status, - statusText, - }), - ); -} - -describe("Fetcher Logging Integration", () => { - describe("Request Logging", () => { - it("should log successful request at debug level", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "POST", - headers: { "Content-Type": "application/json" }, - body: { test: "data" }, - contentType: "application/json", - requestType: "json", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - method: "POST", - url: "https://example.com/api", - headers: expect.objectContaining({ - "Content-Type": "application/json", - }), - hasBody: true, - }), - ); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "HTTP request succeeded", - expect.objectContaining({ - method: "POST", - url: "https://example.com/api", - statusCode: 200, - }), - ); - }); - - it("should not log debug messages at info level for successful requests", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "info", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - expect(mockLogger.info).not.toHaveBeenCalled(); - }); - - it("should log request with body flag", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "POST", - body: { data: "test" }, - contentType: "application/json", - requestType: "json", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - hasBody: true, - }), - ); - }); - - it("should log request without body flag", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - hasBody: false, - }), - ); - }); - - it("should not log when silent mode is enabled", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: true, - }, - }); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - expect(mockLogger.info).not.toHaveBeenCalled(); - expect(mockLogger.warn).not.toHaveBeenCalled(); - expect(mockLogger.error).not.toHaveBeenCalled(); - }); - - it("should not log when no logging config is provided", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - }); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - }); - }); - - describe("Error Logging", () => { - it("should log 4xx errors at error level", async () => { - const mockLogger = createMockLogger(); - mockErrorResponse({ error: "Not found" }, 404, "Not Found"); - - const result = await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(result.ok).toBe(false); - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request failed with error status", - expect.objectContaining({ - method: "GET", - url: "https://example.com/api", - statusCode: 404, - }), - ); - }); - - it("should log 5xx errors at error level", async () => { - const mockLogger = createMockLogger(); - mockErrorResponse({ error: "Internal error" }, 500, "Internal Server Error"); - - const result = await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(result.ok).toBe(false); - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request failed with error status", - expect.objectContaining({ - method: "GET", - url: "https://example.com/api", - statusCode: 500, - }), - ); - }); - - it("should log aborted request errors", async () => { - const mockLogger = createMockLogger(); - - const abortController = new AbortController(); - abortController.abort(); - - global.fetch = vi.fn().mockRejectedValue(new Error("Aborted")); - - const result = await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - abortSignal: abortController.signal, - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(result.ok).toBe(false); - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request was aborted", - expect.objectContaining({ - method: "GET", - url: "https://example.com/api", - }), - ); - }); - - it("should log timeout errors", async () => { - const mockLogger = createMockLogger(); - - const timeoutError = new Error("Request timeout"); - timeoutError.name = "AbortError"; - - global.fetch = vi.fn().mockRejectedValue(timeoutError); - - const result = await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(result.ok).toBe(false); - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request timed out", - expect.objectContaining({ - method: "GET", - url: "https://example.com/api", - timeoutMs: undefined, - }), - ); - }); - - it("should log unknown errors", async () => { - const mockLogger = createMockLogger(); - - const unknownError = new Error("Unknown error"); - - global.fetch = vi.fn().mockRejectedValue(unknownError); - - const result = await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(result.ok).toBe(false); - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request failed with error", - expect.objectContaining({ - method: "GET", - url: "https://example.com/api", - errorMessage: "Unknown error", - }), - ); - }); - }); - - describe("Logging with Redaction", () => { - it("should redact sensitive data in error logs", async () => { - const mockLogger = createMockLogger(); - mockErrorResponse({ error: "Unauthorized" }, 401, "Unauthorized"); - - await fetcherImpl({ - url: "https://example.com/api?api_key=secret", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request failed with error status", - expect.objectContaining({ - url: "https://example.com/api?api_key=[REDACTED]", - }), - ); - }); - }); - - describe("Different HTTP Methods", () => { - it("should log GET requests", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - method: "GET", - }), - ); - }); - - it("should log POST requests", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse({ data: "test" }, 201, "Created"); - - await fetcherImpl({ - url: "https://example.com/api", - method: "POST", - body: { data: "test" }, - contentType: "application/json", - requestType: "json", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - method: "POST", - }), - ); - }); - - it("should log PUT requests", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "PUT", - body: { data: "test" }, - contentType: "application/json", - requestType: "json", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - method: "PUT", - }), - ); - }); - - it("should log DELETE requests", async () => { - const mockLogger = createMockLogger(); - global.fetch = vi.fn().mockResolvedValue( - new Response(null, { - status: 200, - statusText: "OK", - }), - ); - - await fetcherImpl({ - url: "https://example.com/api", - method: "DELETE", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - method: "DELETE", - }), - ); - }); - }); - - describe("Status Code Logging", () => { - it("should log 2xx success status codes", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse({ data: "test" }, 201, "Created"); - - await fetcherImpl({ - url: "https://example.com/api", - method: "POST", - body: { data: "test" }, - contentType: "application/json", - requestType: "json", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "HTTP request succeeded", - expect.objectContaining({ - statusCode: 201, - }), - ); - }); - - it("should log 3xx redirect status codes as success", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse({ data: "test" }, 301, "Moved Permanently"); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "HTTP request succeeded", - expect.objectContaining({ - statusCode: 301, - }), - ); - }); - }); -}); diff --git a/tests/unit/fetcher/redacting.test.ts b/tests/unit/fetcher/redacting.test.ts deleted file mode 100644 index 9b9775b..0000000 --- a/tests/unit/fetcher/redacting.test.ts +++ /dev/null @@ -1,1115 +0,0 @@ -import { fetcherImpl } from "../../../src/core/fetcher/Fetcher"; - -function createMockLogger() { - return { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; -} - -function mockSuccessResponse(data: unknown = { data: "test" }, status = 200, statusText = "OK") { - global.fetch = vi.fn().mockResolvedValue( - new Response(JSON.stringify(data), { - status, - statusText, - }), - ); -} - -describe("Redacting Logic", () => { - describe("Header Redaction", () => { - it("should redact authorization header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { Authorization: "Bearer secret-token-12345" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: "[REDACTED]", - }), - }), - ); - }); - - it("should redact api-key header (case-insensitive)", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { "X-API-KEY": "secret-api-key" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "X-API-KEY": "[REDACTED]", - }), - }), - ); - }); - - it("should redact cookie header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { Cookie: "session=abc123; token=xyz789" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - Cookie: "[REDACTED]", - }), - }), - ); - }); - - it("should redact x-auth-token header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { "x-auth-token": "auth-token-12345" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "x-auth-token": "[REDACTED]", - }), - }), - ); - }); - - it("should redact proxy-authorization header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { "Proxy-Authorization": "Basic credentials" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "Proxy-Authorization": "[REDACTED]", - }), - }), - ); - }); - - it("should redact x-csrf-token header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { "X-CSRF-Token": "csrf-token-abc" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "X-CSRF-Token": "[REDACTED]", - }), - }), - ); - }); - - it("should redact www-authenticate header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { "WWW-Authenticate": "Bearer realm=example" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "WWW-Authenticate": "[REDACTED]", - }), - }), - ); - }); - - it("should redact x-session-token header", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { "X-Session-Token": "session-token-xyz" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "X-Session-Token": "[REDACTED]", - }), - }), - ); - }); - - it("should not redact non-sensitive headers", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { - "Content-Type": "application/json", - "User-Agent": "Test/1.0", - Accept: "application/json", - }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - "Content-Type": "application/json", - "User-Agent": "Test/1.0", - Accept: "application/json", - }), - }), - ); - }); - - it("should redact multiple sensitive headers at once", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - headers: { - Authorization: "Bearer token", - "X-API-Key": "api-key", - Cookie: "session=123", - "Content-Type": "application/json", - }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: "[REDACTED]", - "X-API-Key": "[REDACTED]", - Cookie: "[REDACTED]", - "Content-Type": "application/json", - }), - }), - ); - }); - }); - - describe("Response Header Redaction", () => { - it("should redact Set-Cookie in response headers", async () => { - const mockLogger = createMockLogger(); - - const mockHeaders = new Headers(); - mockHeaders.set("Set-Cookie", "session=abc123; HttpOnly; Secure"); - mockHeaders.set("Content-Type", "application/json"); - - global.fetch = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ data: "test" }), { - status: 200, - statusText: "OK", - headers: mockHeaders, - }), - ); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "HTTP request succeeded", - expect.objectContaining({ - responseHeaders: expect.objectContaining({ - "set-cookie": "[REDACTED]", - "content-type": "application/json", - }), - }), - ); - }); - - it("should redact authorization in response headers", async () => { - const mockLogger = createMockLogger(); - - const mockHeaders = new Headers(); - mockHeaders.set("Authorization", "Bearer token-123"); - mockHeaders.set("Content-Type", "application/json"); - - global.fetch = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ data: "test" }), { - status: 200, - statusText: "OK", - headers: mockHeaders, - }), - ); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "HTTP request succeeded", - expect.objectContaining({ - responseHeaders: expect.objectContaining({ - authorization: "[REDACTED]", - "content-type": "application/json", - }), - }), - ); - }); - - it("should redact response headers in error responses", async () => { - const mockLogger = createMockLogger(); - - const mockHeaders = new Headers(); - mockHeaders.set("WWW-Authenticate", "Bearer realm=example"); - mockHeaders.set("Content-Type", "application/json"); - - global.fetch = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - statusText: "Unauthorized", - headers: mockHeaders, - }), - ); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "error", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.error).toHaveBeenCalledWith( - "HTTP request failed with error status", - expect.objectContaining({ - responseHeaders: expect.objectContaining({ - "www-authenticate": "[REDACTED]", - "content-type": "application/json", - }), - }), - ); - }); - }); - - describe("Query Parameter Redaction", () => { - it("should redact api_key query parameter", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { api_key: "secret-key" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - api_key: "[REDACTED]", - }), - }), - ); - }); - - it("should redact token query parameter", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { token: "secret-token" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - token: "[REDACTED]", - }), - }), - ); - }); - - it("should redact access_token query parameter", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { access_token: "secret-access-token" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - access_token: "[REDACTED]", - }), - }), - ); - }); - - it("should redact password query parameter", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { password: "secret-password" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - password: "[REDACTED]", - }), - }), - ); - }); - - it("should redact secret query parameter", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { secret: "secret-value" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - secret: "[REDACTED]", - }), - }), - ); - }); - - it("should redact session_id query parameter", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { session_id: "session-123" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - session_id: "[REDACTED]", - }), - }), - ); - }); - - it("should not redact non-sensitive query parameters", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { - page: "1", - limit: "10", - sort: "name", - }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - page: "1", - limit: "10", - sort: "name", - }), - }), - ); - }); - - it("should not redact parameters containing 'auth' substring like 'author'", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { - author: "john", - authenticate: "false", - authorization_level: "user", - }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - author: "john", - authenticate: "false", - authorization_level: "user", - }), - }), - ); - }); - - it("should handle undefined query parameters", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: undefined, - }), - ); - }); - - it("should redact case-insensitive query parameters", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - queryParameters: { API_KEY: "secret-key", Token: "secret-token" }, - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - queryParameters: expect.objectContaining({ - API_KEY: "[REDACTED]", - Token: "[REDACTED]", - }), - }), - ); - }); - }); - - describe("URL Redaction", () => { - it("should redact credentials in URL", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://user:password@example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://[REDACTED]@example.com/api", - }), - ); - }); - - it("should redact api_key in query string", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?api_key=secret-key&page=1", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?api_key=[REDACTED]&page=1", - }), - ); - }); - - it("should redact token in query string", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?token=secret-token", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?token=[REDACTED]", - }), - ); - }); - - it("should redact password in query string", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?username=user&password=secret", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?username=user&password=[REDACTED]", - }), - ); - }); - - it("should not redact non-sensitive query strings", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?page=1&limit=10&sort=name", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?page=1&limit=10&sort=name", - }), - ); - }); - - it("should not redact URL parameters containing 'auth' substring like 'author'", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?author=john&authenticate=false&page=1", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?author=john&authenticate=false&page=1", - }), - ); - }); - - it("should handle URL with fragment", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?token=secret#section", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?token=[REDACTED]#section", - }), - ); - }); - - it("should redact URL-encoded query parameters", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?api%5Fkey=secret", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?api%5Fkey=[REDACTED]", - }), - ); - }); - - it("should handle URL without query string", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api", - }), - ); - }); - - it("should handle empty query string", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?", - }), - ); - }); - - it("should redact multiple sensitive parameters in URL", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?api_key=secret1&token=secret2&page=1", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?api_key=[REDACTED]&token=[REDACTED]&page=1", - }), - ); - }); - - it("should redact both credentials and query parameters", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://user:pass@example.com/api?token=secret", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://[REDACTED]@example.com/api?token=[REDACTED]", - }), - ); - }); - - it("should use fast path for URLs without sensitive keywords", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?page=1&limit=10&sort=name&filter=value", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?page=1&limit=10&sort=name&filter=value", - }), - ); - }); - - it("should handle query parameter without value", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?flag&token=secret", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?flag&token=[REDACTED]", - }), - ); - }); - - it("should handle URL with multiple @ symbols in credentials", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://user@example.com:pass@host.com/api", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://[REDACTED]@host.com/api", - }), - ); - }); - - it("should handle URL with @ in query parameter but not in credentials", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://example.com/api?email=user@example.com", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://example.com/api?email=user@example.com", - }), - ); - }); - - it("should handle URL with both credentials and @ in path", async () => { - const mockLogger = createMockLogger(); - mockSuccessResponse(); - - await fetcherImpl({ - url: "https://user:pass@example.com/users/@username", - method: "GET", - responseType: "json", - maxRetries: 0, - logging: { - level: "debug", - logger: mockLogger, - silent: false, - }, - }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - "Making HTTP request", - expect.objectContaining({ - url: "https://[REDACTED]@example.com/users/@username", - }), - ); - }); - }); -}); diff --git a/tests/unit/fetcher/requestWithRetries.test.ts b/tests/unit/fetcher/requestWithRetries.test.ts index 978557f..7d46082 100644 --- a/tests/unit/fetcher/requestWithRetries.test.ts +++ b/tests/unit/fetcher/requestWithRetries.test.ts @@ -1,14 +1,15 @@ import { requestWithRetries } from "../../../src/core/fetcher/requestWithRetries"; describe("requestWithRetries", () => { - let mockFetch: import("vi").Mock; + let mockFetch: import("vitest").Mock; let originalMathRandom: typeof Math.random; - let setTimeoutSpy: import("vi").MockInstance; + let setTimeoutSpy: import("vitest").MockInstance; beforeEach(() => { mockFetch = vi.fn(); originalMathRandom = Math.random; + // Mock Math.random for consistent jitter Math.random = vi.fn(() => 0.5); vi.useFakeTimers({ @@ -98,67 +99,6 @@ describe("requestWithRetries", () => { } }); - interface RetryHeaderTestCase { - description: string; - headerName: string; - headerValue: string | (() => string); - expectedDelayMin: number; - expectedDelayMax: number; - } - - const retryHeaderTests: RetryHeaderTestCase[] = [ - { - description: "should respect retry-after header with seconds value", - headerName: "retry-after", - headerValue: "5", - expectedDelayMin: 4000, - expectedDelayMax: 6000, - }, - { - description: "should respect retry-after header with HTTP date value", - headerName: "retry-after", - headerValue: () => new Date(Date.now() + 3000).toUTCString(), - expectedDelayMin: 2000, - expectedDelayMax: 4000, - }, - { - description: "should respect x-ratelimit-reset header", - headerName: "x-ratelimit-reset", - headerValue: () => Math.floor((Date.now() + 4000) / 1000).toString(), - expectedDelayMin: 3000, - expectedDelayMax: 6000, - }, - ]; - - retryHeaderTests.forEach(({ description, headerName, headerValue, expectedDelayMin, expectedDelayMax }) => { - it(description, async () => { - setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { - process.nextTick(callback); - return null as any; - }); - - const value = typeof headerValue === "function" ? headerValue() : headerValue; - mockFetch - .mockResolvedValueOnce( - new Response("", { - status: 429, - headers: new Headers({ [headerName]: value }), - }), - ) - .mockResolvedValueOnce(new Response("", { status: 200 })); - - const responsePromise = requestWithRetries(() => mockFetch(), 1); - await vi.runAllTimersAsync(); - const response = await responsePromise; - - expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expect.any(Number)); - const actualDelay = setTimeoutSpy.mock.calls[0][1]; - expect(actualDelay).toBeGreaterThan(expectedDelayMin); - expect(actualDelay).toBeLessThan(expectedDelayMax); - expect(response.status).toBe(200); - }); - }); - it("should apply correct exponential backoff with jitter", async () => { setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { process.nextTick(callback); @@ -173,6 +113,7 @@ describe("requestWithRetries", () => { await vi.runAllTimersAsync(); await responsePromise; + // Verify setTimeout calls expect(setTimeoutSpy).toHaveBeenCalledTimes(expectedDelays.length); expectedDelays.forEach((delay, index) => { @@ -204,6 +145,85 @@ describe("requestWithRetries", () => { expect(response2.status).toBe(200); }); + it("should respect retry-after header with seconds value", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ "retry-after": "5" }), + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5000); // 5 seconds = 5000ms + expect(response.status).toBe(200); + }); + + it("should respect retry-after header with HTTP date value", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const futureDate = new Date(Date.now() + 3000); // 3 seconds from now + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ "retry-after": futureDate.toUTCString() }), + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + // Should use the date-based delay (approximately 3000ms, but with jitter) + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expect.any(Number)); + const actualDelay = setTimeoutSpy.mock.calls[0][1]; + expect(actualDelay).toBeGreaterThan(2000); + expect(actualDelay).toBeLessThan(4000); + expect(response.status).toBe(200); + }); + + it("should respect x-ratelimit-reset header", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const resetTime = Math.floor((Date.now() + 4000) / 1000); // 4 seconds from now in Unix timestamp + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ "x-ratelimit-reset": resetTime.toString() }), + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + // Should use the x-ratelimit-reset delay (approximately 4000ms, but with positive jitter) + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expect.any(Number)); + const actualDelay = setTimeoutSpy.mock.calls[0][1]; + expect(actualDelay).toBeGreaterThan(3000); + expect(actualDelay).toBeLessThan(6000); + expect(response.status).toBe(200); + }); + it("should cap delay at MAX_RETRY_DELAY for large header values", async () => { setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { process.nextTick(callback); @@ -223,7 +243,8 @@ describe("requestWithRetries", () => { await vi.runAllTimersAsync(); const response = await responsePromise; - expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60000); + // Should be capped at MAX_RETRY_DELAY (60000ms) with jitter applied + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60000); // Exactly MAX_RETRY_DELAY since jitter with 0.5 random keeps it at 60000 expect(response.status).toBe(200); }); }); diff --git a/tests/unit/logging/logger.test.ts b/tests/unit/logging/logger.test.ts deleted file mode 100644 index 2e0b5fe..0000000 --- a/tests/unit/logging/logger.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { ConsoleLogger, createLogger, Logger, LogLevel } from "../../../src/core/logging/logger"; - -function createMockLogger() { - return { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; -} - -describe("Logger", () => { - describe("LogLevel", () => { - it("should have correct log levels", () => { - expect(LogLevel.Debug).toBe("debug"); - expect(LogLevel.Info).toBe("info"); - expect(LogLevel.Warn).toBe("warn"); - expect(LogLevel.Error).toBe("error"); - }); - }); - - describe("ConsoleLogger", () => { - let consoleLogger: ConsoleLogger; - let consoleSpy: { - debug: ReturnType; - info: ReturnType; - warn: ReturnType; - error: ReturnType; - }; - - beforeEach(() => { - consoleLogger = new ConsoleLogger(); - consoleSpy = { - debug: vi.spyOn(console, "debug").mockImplementation(() => {}), - info: vi.spyOn(console, "info").mockImplementation(() => {}), - warn: vi.spyOn(console, "warn").mockImplementation(() => {}), - error: vi.spyOn(console, "error").mockImplementation(() => {}), - }; - }); - - afterEach(() => { - consoleSpy.debug.mockRestore(); - consoleSpy.info.mockRestore(); - consoleSpy.warn.mockRestore(); - consoleSpy.error.mockRestore(); - }); - - it("should log debug messages", () => { - consoleLogger.debug("debug message", { data: "test" }); - expect(consoleSpy.debug).toHaveBeenCalledWith("debug message", { data: "test" }); - }); - - it("should log info messages", () => { - consoleLogger.info("info message", { data: "test" }); - expect(consoleSpy.info).toHaveBeenCalledWith("info message", { data: "test" }); - }); - - it("should log warn messages", () => { - consoleLogger.warn("warn message", { data: "test" }); - expect(consoleSpy.warn).toHaveBeenCalledWith("warn message", { data: "test" }); - }); - - it("should log error messages", () => { - consoleLogger.error("error message", { data: "test" }); - expect(consoleSpy.error).toHaveBeenCalledWith("error message", { data: "test" }); - }); - - it("should handle multiple arguments", () => { - consoleLogger.debug("message", "arg1", "arg2", { key: "value" }); - expect(consoleSpy.debug).toHaveBeenCalledWith("message", "arg1", "arg2", { key: "value" }); - }); - }); - - describe("Logger with level filtering", () => { - let mockLogger: { - debug: ReturnType; - info: ReturnType; - warn: ReturnType; - error: ReturnType; - }; - - beforeEach(() => { - mockLogger = createMockLogger(); - }); - - describe("Debug level", () => { - it("should log all levels when set to debug", () => { - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - logger.debug("debug"); - logger.info("info"); - logger.warn("warn"); - logger.error("error"); - - expect(mockLogger.debug).toHaveBeenCalledWith("debug"); - expect(mockLogger.info).toHaveBeenCalledWith("info"); - expect(mockLogger.warn).toHaveBeenCalledWith("warn"); - expect(mockLogger.error).toHaveBeenCalledWith("error"); - }); - - it("should report correct level checks", () => { - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - expect(logger.isDebug()).toBe(true); - expect(logger.isInfo()).toBe(true); - expect(logger.isWarn()).toBe(true); - expect(logger.isError()).toBe(true); - }); - }); - - describe("Info level", () => { - it("should log info, warn, and error when set to info", () => { - const logger = new Logger({ - level: LogLevel.Info, - logger: mockLogger, - silent: false, - }); - - logger.debug("debug"); - logger.info("info"); - logger.warn("warn"); - logger.error("error"); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - expect(mockLogger.info).toHaveBeenCalledWith("info"); - expect(mockLogger.warn).toHaveBeenCalledWith("warn"); - expect(mockLogger.error).toHaveBeenCalledWith("error"); - }); - - it("should report correct level checks", () => { - const logger = new Logger({ - level: LogLevel.Info, - logger: mockLogger, - silent: false, - }); - - expect(logger.isDebug()).toBe(false); - expect(logger.isInfo()).toBe(true); - expect(logger.isWarn()).toBe(true); - expect(logger.isError()).toBe(true); - }); - }); - - describe("Warn level", () => { - it("should log warn and error when set to warn", () => { - const logger = new Logger({ - level: LogLevel.Warn, - logger: mockLogger, - silent: false, - }); - - logger.debug("debug"); - logger.info("info"); - logger.warn("warn"); - logger.error("error"); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - expect(mockLogger.info).not.toHaveBeenCalled(); - expect(mockLogger.warn).toHaveBeenCalledWith("warn"); - expect(mockLogger.error).toHaveBeenCalledWith("error"); - }); - - it("should report correct level checks", () => { - const logger = new Logger({ - level: LogLevel.Warn, - logger: mockLogger, - silent: false, - }); - - expect(logger.isDebug()).toBe(false); - expect(logger.isInfo()).toBe(false); - expect(logger.isWarn()).toBe(true); - expect(logger.isError()).toBe(true); - }); - }); - - describe("Error level", () => { - it("should only log error when set to error", () => { - const logger = new Logger({ - level: LogLevel.Error, - logger: mockLogger, - silent: false, - }); - - logger.debug("debug"); - logger.info("info"); - logger.warn("warn"); - logger.error("error"); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - expect(mockLogger.info).not.toHaveBeenCalled(); - expect(mockLogger.warn).not.toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalledWith("error"); - }); - - it("should report correct level checks", () => { - const logger = new Logger({ - level: LogLevel.Error, - logger: mockLogger, - silent: false, - }); - - expect(logger.isDebug()).toBe(false); - expect(logger.isInfo()).toBe(false); - expect(logger.isWarn()).toBe(false); - expect(logger.isError()).toBe(true); - }); - }); - - describe("Silent mode", () => { - it("should not log anything when silent is true", () => { - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: true, - }); - - logger.debug("debug"); - logger.info("info"); - logger.warn("warn"); - logger.error("error"); - - expect(mockLogger.debug).not.toHaveBeenCalled(); - expect(mockLogger.info).not.toHaveBeenCalled(); - expect(mockLogger.warn).not.toHaveBeenCalled(); - expect(mockLogger.error).not.toHaveBeenCalled(); - }); - - it("should report all level checks as false when silent", () => { - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: true, - }); - - expect(logger.isDebug()).toBe(false); - expect(logger.isInfo()).toBe(false); - expect(logger.isWarn()).toBe(false); - expect(logger.isError()).toBe(false); - }); - }); - - describe("shouldLog", () => { - it("should correctly determine if level should be logged", () => { - const logger = new Logger({ - level: LogLevel.Info, - logger: mockLogger, - silent: false, - }); - - expect(logger.shouldLog(LogLevel.Debug)).toBe(false); - expect(logger.shouldLog(LogLevel.Info)).toBe(true); - expect(logger.shouldLog(LogLevel.Warn)).toBe(true); - expect(logger.shouldLog(LogLevel.Error)).toBe(true); - }); - - it("should return false for all levels when silent", () => { - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: true, - }); - - expect(logger.shouldLog(LogLevel.Debug)).toBe(false); - expect(logger.shouldLog(LogLevel.Info)).toBe(false); - expect(logger.shouldLog(LogLevel.Warn)).toBe(false); - expect(logger.shouldLog(LogLevel.Error)).toBe(false); - }); - }); - - describe("Multiple arguments", () => { - it("should pass multiple arguments to logger", () => { - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - logger.debug("message", "arg1", { key: "value" }, 123); - expect(mockLogger.debug).toHaveBeenCalledWith("message", "arg1", { key: "value" }, 123); - }); - }); - }); - - describe("createLogger", () => { - it("should return default logger when no config provided", () => { - const logger = createLogger(); - expect(logger).toBeInstanceOf(Logger); - }); - - it("should return same logger instance when Logger is passed", () => { - const customLogger = new Logger({ - level: LogLevel.Debug, - logger: new ConsoleLogger(), - silent: false, - }); - - const result = createLogger(customLogger); - expect(result).toBe(customLogger); - }); - - it("should create logger with custom config", () => { - const mockLogger = createMockLogger(); - - const logger = createLogger({ - level: LogLevel.Warn, - logger: mockLogger, - silent: false, - }); - - expect(logger).toBeInstanceOf(Logger); - logger.warn("test"); - expect(mockLogger.warn).toHaveBeenCalledWith("test"); - }); - - it("should use default values for missing config", () => { - const logger = createLogger({}); - expect(logger).toBeInstanceOf(Logger); - }); - - it("should override default level", () => { - const mockLogger = createMockLogger(); - - const logger = createLogger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - logger.debug("test"); - expect(mockLogger.debug).toHaveBeenCalledWith("test"); - }); - - it("should override default silent mode", () => { - const mockLogger = createMockLogger(); - - const logger = createLogger({ - logger: mockLogger, - silent: false, - }); - - logger.info("test"); - expect(mockLogger.info).toHaveBeenCalledWith("test"); - }); - - it("should use provided logger implementation", () => { - const customLogger = createMockLogger(); - - const logger = createLogger({ - logger: customLogger, - level: LogLevel.Debug, - silent: false, - }); - - logger.debug("test"); - expect(customLogger.debug).toHaveBeenCalledWith("test"); - }); - - it("should default to silent: true", () => { - const mockLogger = createMockLogger(); - - const logger = createLogger({ - logger: mockLogger, - level: LogLevel.Debug, - }); - - logger.debug("test"); - expect(mockLogger.debug).not.toHaveBeenCalled(); - }); - }); - - describe("Default logger", () => { - it("should have silent: true by default", () => { - const logger = createLogger(); - expect(logger.shouldLog(LogLevel.Info)).toBe(false); - }); - - it("should not log when using default logger", () => { - const logger = createLogger(); - - logger.info("test"); - expect(logger.isInfo()).toBe(false); - }); - }); - - describe("Edge cases", () => { - it("should handle empty message", () => { - const mockLogger = createMockLogger(); - - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - logger.debug(""); - expect(mockLogger.debug).toHaveBeenCalledWith(""); - }); - - it("should handle no arguments", () => { - const mockLogger = createMockLogger(); - - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - logger.debug("message"); - expect(mockLogger.debug).toHaveBeenCalledWith("message"); - }); - - it("should handle complex objects", () => { - const mockLogger = createMockLogger(); - - const logger = new Logger({ - level: LogLevel.Debug, - logger: mockLogger, - silent: false, - }); - - const complexObject = { - nested: { key: "value" }, - array: [1, 2, 3], - fn: () => "test", - }; - - logger.debug("message", complexObject); - expect(mockLogger.debug).toHaveBeenCalledWith("message", complexObject); - }); - - it("should handle errors as arguments", () => { - const mockLogger = createMockLogger(); - - const logger = new Logger({ - level: LogLevel.Error, - logger: mockLogger, - silent: false, - }); - - const error = new Error("Test error"); - logger.error("Error occurred", error); - expect(mockLogger.error).toHaveBeenCalledWith("Error occurred", error); - }); - }); -}); diff --git a/tests/unit/url/join.test.ts b/tests/unit/url/join.test.ts index 123488f..984cfe6 100644 --- a/tests/unit/url/join.test.ts +++ b/tests/unit/url/join.test.ts @@ -1,223 +1,88 @@ import { join } from "../../../src/core/url/index"; describe("join", () => { - interface TestCase { - description: string; - base: string; - segments: string[]; - expected: string; - } - describe("basic functionality", () => { - const basicTests: TestCase[] = [ - { description: "should return empty string for empty base", base: "", segments: [], expected: "" }, - { - description: "should return empty string for empty base with path", - base: "", - segments: ["path"], - expected: "", - }, - { - description: "should handle single segment", - base: "base", - segments: ["segment"], - expected: "base/segment", - }, - { - description: "should handle single segment with trailing slash on base", - base: "base/", - segments: ["segment"], - expected: "base/segment", - }, - { - description: "should handle single segment with leading slash", - base: "base", - segments: ["/segment"], - expected: "base/segment", - }, - { - description: "should handle single segment with both slashes", - base: "base/", - segments: ["/segment"], - expected: "base/segment", - }, - { - description: "should handle multiple segments", - base: "base", - segments: ["path1", "path2", "path3"], - expected: "base/path1/path2/path3", - }, - { - description: "should handle multiple segments with slashes", - base: "base/", - segments: ["/path1/", "/path2/", "/path3/"], - expected: "base/path1/path2/path3/", - }, - ]; + it("should return empty string for empty base", () => { + expect(join("")).toBe(""); + expect(join("", "path")).toBe(""); + }); + + it("should handle single segment", () => { + expect(join("base", "segment")).toBe("base/segment"); + expect(join("base/", "segment")).toBe("base/segment"); + expect(join("base", "/segment")).toBe("base/segment"); + expect(join("base/", "/segment")).toBe("base/segment"); + }); - basicTests.forEach(({ description, base, segments, expected }) => { - it(description, () => { - expect(join(base, ...segments)).toBe(expected); - }); + it("should handle multiple segments", () => { + expect(join("base", "path1", "path2", "path3")).toBe("base/path1/path2/path3"); + expect(join("base/", "/path1/", "/path2/", "/path3/")).toBe("base/path1/path2/path3/"); }); }); describe("URL handling", () => { - const urlTests: TestCase[] = [ - { - description: "should handle absolute URLs", - base: "https://example.com", - segments: ["api", "v1"], - expected: "https://example.com/api/v1", - }, - { - description: "should handle absolute URLs with slashes", - base: "https://example.com/", - segments: ["/api/", "/v1/"], - expected: "https://example.com/api/v1/", - }, - { - description: "should handle absolute URLs with base path", - base: "https://example.com/base", - segments: ["api", "v1"], - expected: "https://example.com/base/api/v1", - }, - { - description: "should preserve URL query parameters", - base: "https://example.com?query=1", - segments: ["api"], - expected: "https://example.com/api?query=1", - }, - { - description: "should preserve URL fragments", - base: "https://example.com#fragment", - segments: ["api"], - expected: "https://example.com/api#fragment", - }, - { - description: "should preserve URL query and fragments", - base: "https://example.com?query=1#fragment", - segments: ["api"], - expected: "https://example.com/api?query=1#fragment", - }, - { - description: "should handle http protocol", - base: "http://example.com", - segments: ["api"], - expected: "http://example.com/api", - }, - { - description: "should handle ftp protocol", - base: "ftp://example.com", - segments: ["files"], - expected: "ftp://example.com/files", - }, - { - description: "should handle ws protocol", - base: "ws://example.com", - segments: ["socket"], - expected: "ws://example.com/socket", - }, - { - description: "should fallback to path joining for malformed URLs", - base: "not-a-url://", - segments: ["path"], - expected: "not-a-url:///path", - }, - ]; + it("should handle absolute URLs", () => { + expect(join("https://example.com", "api", "v1")).toBe("https://example.com/api/v1"); + expect(join("https://example.com/", "/api/", "/v1/")).toBe("https://example.com/api/v1/"); + expect(join("https://example.com/base", "api", "v1")).toBe("https://example.com/base/api/v1"); + }); - urlTests.forEach(({ description, base, segments, expected }) => { - it(description, () => { - expect(join(base, ...segments)).toBe(expected); - }); + it("should preserve URL query parameters and fragments", () => { + expect(join("https://example.com?query=1", "api")).toBe("https://example.com/api?query=1"); + expect(join("https://example.com#fragment", "api")).toBe("https://example.com/api#fragment"); + expect(join("https://example.com?query=1#fragment", "api")).toBe( + "https://example.com/api?query=1#fragment", + ); + }); + + it("should handle different protocols", () => { + expect(join("http://example.com", "api")).toBe("http://example.com/api"); + expect(join("ftp://example.com", "files")).toBe("ftp://example.com/files"); + expect(join("ws://example.com", "socket")).toBe("ws://example.com/socket"); + }); + + it("should fallback to path joining for malformed URLs", () => { + expect(join("not-a-url://", "path")).toBe("not-a-url:///path"); }); }); describe("edge cases", () => { - const edgeCaseTests: TestCase[] = [ - { - description: "should handle empty segments", - base: "base", - segments: ["", "path"], - expected: "base/path", - }, - { - description: "should handle null segments", - base: "base", - segments: [null as any, "path"], - expected: "base/path", - }, - { - description: "should handle undefined segments", - base: "base", - segments: [undefined as any, "path"], - expected: "base/path", - }, - { - description: "should handle segments with only single slash", - base: "base", - segments: ["/", "path"], - expected: "base/path", - }, - { - description: "should handle segments with only double slash", - base: "base", - segments: ["//", "path"], - expected: "base/path", - }, - { - description: "should handle base paths with trailing slashes", - base: "base/", - segments: ["path"], - expected: "base/path", - }, - { - description: "should handle complex nested paths", - base: "api/v1/", - segments: ["/users/", "/123/", "/profile"], - expected: "api/v1/users/123/profile", - }, - ]; + it("should handle empty segments", () => { + expect(join("base", "", "path")).toBe("base/path"); + expect(join("base", null as any, "path")).toBe("base/path"); + expect(join("base", undefined as any, "path")).toBe("base/path"); + }); + + it("should handle segments with only slashes", () => { + expect(join("base", "/", "path")).toBe("base/path"); + expect(join("base", "//", "path")).toBe("base/path"); + }); + + it("should handle base paths with trailing slashes", () => { + expect(join("base/", "path")).toBe("base/path"); + }); - edgeCaseTests.forEach(({ description, base, segments, expected }) => { - it(description, () => { - expect(join(base, ...segments)).toBe(expected); - }); + it("should handle complex nested paths", () => { + expect(join("api/v1/", "/users/", "/123/", "/profile")).toBe("api/v1/users/123/profile"); }); }); describe("real-world scenarios", () => { - const realWorldTests: TestCase[] = [ - { - description: "should handle API endpoint construction", - base: "https://api.example.com/v1", - segments: ["users", "123", "posts"], - expected: "https://api.example.com/v1/users/123/posts", - }, - { - description: "should handle file path construction", - base: "/var/www", - segments: ["html", "assets", "images"], - expected: "/var/www/html/assets/images", - }, - { - description: "should handle relative path construction", - base: "../parent", - segments: ["child", "grandchild"], - expected: "../parent/child/grandchild", - }, - { - description: "should handle Windows-style paths", - base: "C:\\Users", - segments: ["Documents", "file.txt"], - expected: "C:\\Users/Documents/file.txt", - }, - ]; + it("should handle API endpoint construction", () => { + const baseUrl = "https://api.example.com/v1"; + expect(join(baseUrl, "users", "123", "posts")).toBe("https://api.example.com/v1/users/123/posts"); + }); + + it("should handle file path construction", () => { + expect(join("/var/www", "html", "assets", "images")).toBe("/var/www/html/assets/images"); + }); - realWorldTests.forEach(({ description, base, segments, expected }) => { - it(description, () => { - expect(join(base, ...segments)).toBe(expected); - }); + it("should handle relative path construction", () => { + expect(join("../parent", "child", "grandchild")).toBe("../parent/child/grandchild"); + }); + + it("should handle Windows-style paths", () => { + expect(join("C:\\Users", "Documents", "file.txt")).toBe("C:\\Users/Documents/file.txt"); }); }); @@ -225,7 +90,7 @@ describe("join", () => { it("should handle many segments efficiently", () => { const segments = Array(100).fill("segment"); const result = join("base", ...segments); - expect(result).toBe(`base/${segments.join("/")}`); + expect(result).toBe("base/" + segments.join("/")); }); it("should handle long URLs", () => { @@ -235,50 +100,21 @@ describe("join", () => { }); describe("trailing slash preservation", () => { - const trailingSlashTests: TestCase[] = [ - { - description: - "should preserve trailing slash on final result when base has trailing slash and no segments", - base: "https://api.example.com/", - segments: [], - expected: "https://api.example.com/", - }, - { - description: "should preserve trailing slash on v1 path", - base: "https://api.example.com/v1/", - segments: [], - expected: "https://api.example.com/v1/", - }, - { - description: "should preserve trailing slash when last segment has trailing slash", - base: "https://api.example.com", - segments: ["users/"], - expected: "https://api.example.com/users/", - }, - { - description: "should preserve trailing slash with relative path", - base: "api/v1", - segments: ["users/"], - expected: "api/v1/users/", - }, - { - description: "should preserve trailing slash with multiple segments", - base: "https://api.example.com", - segments: ["v1", "collections/"], - expected: "https://api.example.com/v1/collections/", - }, - { - description: "should preserve trailing slash with base path", - base: "base", - segments: ["path1", "path2/"], - expected: "base/path1/path2/", - }, - ]; + it("should preserve trailing slash on final result when base has trailing slash and no segments", () => { + expect(join("https://api.example.com/")).toBe("https://api.example.com/"); + expect(join("https://api.example.com/v1/")).toBe("https://api.example.com/v1/"); + }); + + it("should preserve trailing slash when last segment has trailing slash", () => { + expect(join("https://api.example.com", "users/")).toBe("https://api.example.com/users/"); + expect(join("api/v1", "users/")).toBe("api/v1/users/"); + }); - trailingSlashTests.forEach(({ description, base, segments, expected }) => { - it(description, () => { - expect(join(base, ...segments)).toBe(expected); - }); + it("should preserve trailing slash with multiple segments where last has trailing slash", () => { + expect(join("https://api.example.com", "v1", "collections/")).toBe( + "https://api.example.com/v1/collections/", + ); + expect(join("base", "path1", "path2/")).toBe("base/path1/path2/"); }); }); }); diff --git a/tests/unit/url/qs.test.ts b/tests/unit/url/qs.test.ts index 42cdffb..80e7e04 100644 --- a/tests/unit/url/qs.test.ts +++ b/tests/unit/url/qs.test.ts @@ -1,278 +1,187 @@ import { toQueryString } from "../../../src/core/url/index"; describe("Test qs toQueryString", () => { - interface BasicTestCase { - description: string; - input: any; - expected: string; - } - describe("Basic functionality", () => { - const basicTests: BasicTestCase[] = [ - { description: "should return empty string for null", input: null, expected: "" }, - { description: "should return empty string for undefined", input: undefined, expected: "" }, - { description: "should return empty string for string primitive", input: "hello", expected: "" }, - { description: "should return empty string for number primitive", input: 42, expected: "" }, - { description: "should return empty string for true boolean", input: true, expected: "" }, - { description: "should return empty string for false boolean", input: false, expected: "" }, - { description: "should handle empty objects", input: {}, expected: "" }, - { - description: "should handle simple key-value pairs", - input: { name: "John", age: 30 }, - expected: "name=John&age=30", - }, - ]; + it("should return empty string for null/undefined", () => { + expect(toQueryString(null)).toBe(""); + expect(toQueryString(undefined)).toBe(""); + }); - basicTests.forEach(({ description, input, expected }) => { - it(description, () => { - expect(toQueryString(input)).toBe(expected); - }); + it("should return empty string for primitive values", () => { + expect(toQueryString("hello")).toBe(""); + expect(toQueryString(42)).toBe(""); + expect(toQueryString(true)).toBe(""); + expect(toQueryString(false)).toBe(""); + }); + + it("should handle empty objects", () => { + expect(toQueryString({})).toBe(""); + }); + + it("should handle simple key-value pairs", () => { + const obj = { name: "John", age: 30 }; + expect(toQueryString(obj)).toBe("name=John&age=30"); }); }); describe("Array handling", () => { - interface ArrayTestCase { - description: string; - input: any; - options?: { arrayFormat?: "repeat" | "indices" }; - expected: string; - } + it("should handle arrays with indices format (default)", () => { + const obj = { items: ["a", "b", "c"] }; + expect(toQueryString(obj)).toBe("items%5B0%5D=a&items%5B1%5D=b&items%5B2%5D=c"); + }); + + it("should handle arrays with repeat format", () => { + const obj = { items: ["a", "b", "c"] }; + expect(toQueryString(obj, { arrayFormat: "repeat" })).toBe("items=a&items=b&items=c"); + }); - const arrayTests: ArrayTestCase[] = [ - { - description: "should handle arrays with indices format (default)", - input: { items: ["a", "b", "c"] }, - expected: "items%5B0%5D=a&items%5B1%5D=b&items%5B2%5D=c", - }, - { - description: "should handle arrays with repeat format", - input: { items: ["a", "b", "c"] }, - options: { arrayFormat: "repeat" }, - expected: "items=a&items=b&items=c", - }, - { - description: "should handle empty arrays", - input: { items: [] }, - expected: "", - }, - { - description: "should handle arrays with mixed types", - input: { mixed: ["string", 42, true, false] }, - expected: "mixed%5B0%5D=string&mixed%5B1%5D=42&mixed%5B2%5D=true&mixed%5B3%5D=false", - }, - { - description: "should handle arrays with objects", - input: { users: [{ name: "John" }, { name: "Jane" }] }, - expected: "users%5B0%5D%5Bname%5D=John&users%5B1%5D%5Bname%5D=Jane", - }, - { - description: "should handle arrays with objects in repeat format", - input: { users: [{ name: "John" }, { name: "Jane" }] }, - options: { arrayFormat: "repeat" }, - expected: "users%5Bname%5D=John&users%5Bname%5D=Jane", - }, - ]; + it("should handle empty arrays", () => { + const obj = { items: [] }; + expect(toQueryString(obj)).toBe(""); + }); + + it("should handle arrays with mixed types", () => { + const obj = { mixed: ["string", 42, true, false] }; + expect(toQueryString(obj)).toBe("mixed%5B0%5D=string&mixed%5B1%5D=42&mixed%5B2%5D=true&mixed%5B3%5D=false"); + }); + + it("should handle arrays with objects", () => { + const obj = { users: [{ name: "John" }, { name: "Jane" }] }; + expect(toQueryString(obj)).toBe("users%5B0%5D%5Bname%5D=John&users%5B1%5D%5Bname%5D=Jane"); + }); - arrayTests.forEach(({ description, input, options, expected }) => { - it(description, () => { - expect(toQueryString(input, options)).toBe(expected); - }); + it("should handle arrays with objects in repeat format", () => { + const obj = { users: [{ name: "John" }, { name: "Jane" }] }; + expect(toQueryString(obj, { arrayFormat: "repeat" })).toBe("users%5Bname%5D=John&users%5Bname%5D=Jane"); }); }); describe("Nested objects", () => { - const nestedTests: BasicTestCase[] = [ - { - description: "should handle nested objects", - input: { user: { name: "John", age: 30 } }, - expected: "user%5Bname%5D=John&user%5Bage%5D=30", - }, - { - description: "should handle deeply nested objects", - input: { user: { profile: { name: "John", settings: { theme: "dark" } } } }, - expected: "user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", - }, - { - description: "should handle empty nested objects", - input: { user: {} }, - expected: "", - }, - ]; + it("should handle nested objects", () => { + const obj = { user: { name: "John", age: 30 } }; + expect(toQueryString(obj)).toBe("user%5Bname%5D=John&user%5Bage%5D=30"); + }); + + it("should handle deeply nested objects", () => { + const obj = { user: { profile: { name: "John", settings: { theme: "dark" } } } }; + expect(toQueryString(obj)).toBe( + "user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", + ); + }); - nestedTests.forEach(({ description, input, expected }) => { - it(description, () => { - expect(toQueryString(input)).toBe(expected); - }); + it("should handle empty nested objects", () => { + const obj = { user: {} }; + expect(toQueryString(obj)).toBe(""); }); }); describe("Encoding", () => { - interface EncodingTestCase { - description: string; - input: any; - options?: { encode?: boolean }; - expected: string; - } + it("should encode by default", () => { + const obj = { name: "John Doe", email: "john@example.com" }; + expect(toQueryString(obj)).toBe("name=John%20Doe&email=john%40example.com"); + }); - const encodingTests: EncodingTestCase[] = [ - { - description: "should encode by default", - input: { name: "John Doe", email: "john@example.com" }, - expected: "name=John%20Doe&email=john%40example.com", - }, - { - description: "should not encode when encode is false", - input: { name: "John Doe", email: "john@example.com" }, - options: { encode: false }, - expected: "name=John Doe&email=john@example.com", - }, - { - description: "should encode special characters in keys", - input: { "user name": "John", "email[primary]": "john@example.com" }, - expected: "user%20name=John&email%5Bprimary%5D=john%40example.com", - }, - { - description: "should not encode special characters in keys when encode is false", - input: { "user name": "John", "email[primary]": "john@example.com" }, - options: { encode: false }, - expected: "user name=John&email[primary]=john@example.com", - }, - ]; + it("should not encode when encode is false", () => { + const obj = { name: "John Doe", email: "john@example.com" }; + expect(toQueryString(obj, { encode: false })).toBe("name=John Doe&email=john@example.com"); + }); + + it("should encode special characters in keys", () => { + const obj = { "user name": "John", "email[primary]": "john@example.com" }; + expect(toQueryString(obj)).toBe("user%20name=John&email%5Bprimary%5D=john%40example.com"); + }); - encodingTests.forEach(({ description, input, options, expected }) => { - it(description, () => { - expect(toQueryString(input, options)).toBe(expected); - }); + it("should not encode special characters in keys when encode is false", () => { + const obj = { "user name": "John", "email[primary]": "john@example.com" }; + expect(toQueryString(obj, { encode: false })).toBe("user name=John&email[primary]=john@example.com"); }); }); describe("Mixed scenarios", () => { - interface MixedTestCase { - description: string; - input: any; - options?: { arrayFormat?: "repeat" | "indices" }; - expected: string; - } - - const mixedTests: MixedTestCase[] = [ - { - description: "should handle complex nested structures", - input: { - filters: { - status: ["active", "pending"], - category: { - type: "electronics", - subcategories: ["phones", "laptops"], - }, + it("should handle complex nested structures", () => { + const obj = { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], }, - sort: { field: "name", direction: "asc" }, }, - expected: - "filters%5Bstatus%5D%5B0%5D=active&filters%5Bstatus%5D%5B1%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D%5B0%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D%5B1%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", - }, - { - description: "should handle complex nested structures with repeat format", - input: { - filters: { - status: ["active", "pending"], - category: { - type: "electronics", - subcategories: ["phones", "laptops"], - }, + sort: { field: "name", direction: "asc" }, + }; + expect(toQueryString(obj)).toBe( + "filters%5Bstatus%5D%5B0%5D=active&filters%5Bstatus%5D%5B1%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D%5B0%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D%5B1%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + ); + }); + + it("should handle complex nested structures with repeat format", () => { + const obj = { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], }, - sort: { field: "name", direction: "asc" }, }, - options: { arrayFormat: "repeat" }, - expected: - "filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", - }, - { - description: "should handle arrays with null/undefined values", - input: { items: ["a", null, "c", undefined, "e"] }, - expected: "items%5B0%5D=a&items%5B1%5D=&items%5B2%5D=c&items%5B4%5D=e", - }, - { - description: "should handle objects with null/undefined values", - input: { name: "John", age: null, email: undefined, active: true }, - expected: "name=John&age=&active=true", - }, - ]; + sort: { field: "name", direction: "asc" }, + }; + expect(toQueryString(obj, { arrayFormat: "repeat" })).toBe( + "filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + ); + }); + + it("should handle arrays with null/undefined values", () => { + const obj = { items: ["a", null, "c", undefined, "e"] }; + expect(toQueryString(obj)).toBe("items%5B0%5D=a&items%5B1%5D=&items%5B2%5D=c&items%5B4%5D=e"); + }); - mixedTests.forEach(({ description, input, options, expected }) => { - it(description, () => { - expect(toQueryString(input, options)).toBe(expected); - }); + it("should handle objects with null/undefined values", () => { + const obj = { name: "John", age: null, email: undefined, active: true }; + expect(toQueryString(obj)).toBe("name=John&age=&active=true"); }); }); describe("Edge cases", () => { - const edgeCaseTests: BasicTestCase[] = [ - { - description: "should handle numeric keys", - input: { "0": "zero", "1": "one" }, - expected: "0=zero&1=one", - }, - { - description: "should handle boolean values in objects", - input: { enabled: true, disabled: false }, - expected: "enabled=true&disabled=false", - }, - { - description: "should handle empty strings", - input: { name: "", description: "test" }, - expected: "name=&description=test", - }, - { - description: "should handle zero values", - input: { count: 0, price: 0.0 }, - expected: "count=0&price=0", - }, - { - description: "should handle arrays with empty strings", - input: { items: ["a", "", "c"] }, - expected: "items%5B0%5D=a&items%5B1%5D=&items%5B2%5D=c", - }, - ]; + it("should handle numeric keys", () => { + const obj = { "0": "zero", "1": "one" }; + expect(toQueryString(obj)).toBe("0=zero&1=one"); + }); + + it("should handle boolean values in objects", () => { + const obj = { enabled: true, disabled: false }; + expect(toQueryString(obj)).toBe("enabled=true&disabled=false"); + }); + + it("should handle empty strings", () => { + const obj = { name: "", description: "test" }; + expect(toQueryString(obj)).toBe("name=&description=test"); + }); - edgeCaseTests.forEach(({ description, input, expected }) => { - it(description, () => { - expect(toQueryString(input)).toBe(expected); - }); + it("should handle zero values", () => { + const obj = { count: 0, price: 0.0 }; + expect(toQueryString(obj)).toBe("count=0&price=0"); + }); + + it("should handle arrays with empty strings", () => { + const obj = { items: ["a", "", "c"] }; + expect(toQueryString(obj)).toBe("items%5B0%5D=a&items%5B1%5D=&items%5B2%5D=c"); }); }); describe("Options combinations", () => { - interface OptionsTestCase { - description: string; - input: any; - options?: { arrayFormat?: "repeat" | "indices"; encode?: boolean }; - expected: string; - } + it("should respect both arrayFormat and encode options", () => { + const obj = { items: ["a & b", "c & d"] }; + expect(toQueryString(obj, { arrayFormat: "repeat", encode: false })).toBe("items=a & b&items=c & d"); + }); - const optionsTests: OptionsTestCase[] = [ - { - description: "should respect both arrayFormat and encode options", - input: { items: ["a & b", "c & d"] }, - options: { arrayFormat: "repeat", encode: false }, - expected: "items=a & b&items=c & d", - }, - { - description: "should use default options when none provided", - input: { items: ["a", "b"] }, - expected: "items%5B0%5D=a&items%5B1%5D=b", - }, - { - description: "should merge provided options with defaults", - input: { items: ["a", "b"], name: "John Doe" }, - options: { encode: false }, - expected: "items[0]=a&items[1]=b&name=John Doe", - }, - ]; + it("should use default options when none provided", () => { + const obj = { items: ["a", "b"] }; + expect(toQueryString(obj)).toBe("items%5B0%5D=a&items%5B1%5D=b"); + }); - optionsTests.forEach(({ description, input, options, expected }) => { - it(description, () => { - expect(toQueryString(input, options)).toBe(expected); - }); + it("should merge provided options with defaults", () => { + const obj = { items: ["a", "b"], name: "John Doe" }; + expect(toQueryString(obj, { encode: false })).toBe("items[0]=a&items[1]=b&name=John Doe"); }); }); }); diff --git a/tests/wire/imdb.test.ts b/tests/wire/imdb.test.ts index bf56e56..dbb9d53 100644 --- a/tests/wire/imdb.test.ts +++ b/tests/wire/imdb.test.ts @@ -1,14 +1,14 @@ // This file was auto-generated by Fern from our API Definition. -import * as FernAutopilotTestApi from "../../src/api/index"; -import { FernAutopilotTestApiClient } from "../../src/Client"; import { mockServerPool } from "../mock-server/MockServerPool"; +import { FernAutopilotTestApiClient } from "../../src/Client"; +import * as FernAutopilotTestApi from "../../src/api/index"; describe("Imdb", () => { test("createMovie", async () => { const server = mockServerPool.createServer(); const client = new FernAutopilotTestApiClient({ environment: server.baseUrl }); - const rawRequestBody = { title: "title", rating: 1.1 }; + const rawRequestBody = { title: "title", rating: 1.1, metadata: "metadata", more_metadata: "more_metadata" }; const rawResponseBody = "string"; server .mockEndpoint() @@ -22,6 +22,8 @@ describe("Imdb", () => { const response = await client.imdb.createMovie({ title: "title", rating: 1.1, + metadata: "metadata", + more_metadata: "more_metadata", }); expect(response).toEqual("string"); }); @@ -30,13 +32,7 @@ describe("Imdb", () => { const server = mockServerPool.createServer(); const client = new FernAutopilotTestApiClient({ environment: server.baseUrl }); - const rawResponseBody = { - id: "tt0111161", - title: "The Shawshank Redemption", - rating: 9.3, - description: "A story of hope and redemption.", - metadata: "hey", - }; + const rawResponseBody = { id: "tt0111161", title: "The Shawshank Redemption", rating: 9.3, metadata: "hey" }; server.mockEndpoint().get("/movies/tt0111161").respondWith().statusCode(200).jsonBody(rawResponseBody).build(); const response = await client.imdb.getMovie("tt0111161"); @@ -44,7 +40,6 @@ describe("Imdb", () => { id: "tt0111161", title: "The Shawshank Redemption", rating: 9.3, - description: "A story of hope and redemption.", metadata: "hey", }); }); diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 6ce9097..95a5eb7 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -2,8 +2,7 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "module": "esnext", - "outDir": "dist/esm", - "verbatimModuleSyntax": true + "outDir": "dist/esm" }, "include": ["src"], "exclude": [] diff --git a/vitest.config.mts b/vitest.config.ts similarity index 100% rename from vitest.config.mts rename to vitest.config.ts