diff --git a/.fern/metadata.json b/.fern/metadata.json new file mode 100644 index 0000000..6edaf21 --- /dev/null +++ b/.fern/metadata.json @@ -0,0 +1,10 @@ +{ + "cliVersion": "3.5.0", + "generatorName": "fernapi/fern-typescript-node-sdk", + "generatorVersion": "3.38.0", + "generatorConfig": { + "includeApiReference": true, + "namespaceExport": "Pipedream", + "noSerdeLayer": false + } +} diff --git a/.npmignore b/.npmignore deleted file mode 100644 index c0c40ac..0000000 --- a/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules -src -tests -.gitignore -.github -.fernignore -.prettierrc.yml -biome.json -tsconfig.json -yarn.lock -pnpm-lock.yaml \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fe5bc2f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# 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 195154b..6a78bc1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The Pipedream TypeScript library provides convenient access to the Pipedream API - [Reference](#reference) - [Migration From V 1 X](#migration-from-v-1-x) - [Usage](#usage) +- [Authentication](#authentication) - [Request and Response Types](#request-and-response-types) - [Exception Handling](#exception-handling) - [Binary Response](#binary-response) @@ -22,6 +23,7 @@ The Pipedream TypeScript library provides convenient access to the Pipedream API - [Timeouts](#timeouts) - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) + - [Logging](#logging) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -54,6 +56,37 @@ await client.actions.run({ }); ``` +## Authentication + +The SDK supports OAuth authentication with two options: + +**Option 1: OAuth Client Credentials Flow** + +Use this when you want the SDK to automatically handle OAuth token retrieval and refreshing: + +```typescript +import { PipedreamClient } from "@pipedream/sdk"; + +const client = new PipedreamClient({ + clientId: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + ... +}); +``` + +**Option 2: Token Override** + +Use this when you already have a valid bearer token and want to skip the OAuth flow: + +```typescript +import { PipedreamClient } from "@pipedream/sdk"; + +const client = new PipedreamClient({ + token: "my-pre-generated-bearer-token", + ... +}); +``` + ## Request and Response Types The SDK exports all request and response types as TypeScript interfaces. Simply import them with the @@ -484,7 +517,7 @@ List endpoints are paginated. The SDK provides an iterator so that you can simpl import { PipedreamClient } from "@pipedream/sdk"; const client = new PipedreamClient({ clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", projectEnvironment: "YOUR_PROJECT_ENVIRONMENT", projectId: "YOUR_PROJECT_ID" }); -const response = await client.apps.list({ +const pageableResponse = await client.apps.list({ after: "after", before: "before", limit: 1, @@ -492,7 +525,7 @@ const response = await client.apps.list({ sortKey: "name", sortDirection: "asc" }); -for await (const item of response) { +for await (const item of pageableResponse) { console.log(item); } @@ -508,6 +541,9 @@ let page = await client.apps.list({ while (page.hasNextPage()) { page = page.getNextPage(); } + +// You can also access the underlying response +const response = page.response; ``` ## Advanced @@ -517,6 +553,15 @@ while (page.hasNextPage()) { If you would like to send additional headers as part of the request, use the `headers` request option. ```typescript +import { PipedreamClient } from "@pipedream/sdk"; + +const client = new PipedreamClient({ + ... + headers: { + 'X-Custom-Header': 'custom value' + } +}); + const response = await client.actions.run(..., { headers: { 'X-Custom-Header': 'custom value' @@ -590,6 +635,69 @@ 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 { PipedreamClient, logging } from "@pipedream/sdk"; + +const client = new PipedreamClient({ + ... + 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 diff --git a/biome.json b/biome.json index b6890df..a777468 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", "root": true, "vcs": { "enabled": false @@ -7,16 +7,21 @@ "files": { "ignoreUnknown": true, "includes": [ - "./**", - "!dist", - "!lib", - "!*.tsbuildinfo", - "!_tmp_*", - "!*.tmp", - "!.tmp/", - "!*.log", - "!.DS_Store", - "!Thumbs.db" + "**", + "!!dist", + "!!**/dist", + "!!lib", + "!!**/lib", + "!!_tmp_*", + "!!**/_tmp_*", + "!!*.tmp", + "!!**/*.tmp", + "!!.tmp/", + "!!**/.tmp/", + "!!*.log", + "!!**/*.log", + "!!**/.DS_Store", + "!!**/Thumbs.db" ] }, "formatter": { diff --git a/package.json b/package.json index fd1f118..cc33b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sdk", - "version": "2.3.2", + "version": "2.3.3", "private": false, "repository": "github:PipedreamHQ/pipedream-sdk-typescript", "type": "commonjs", @@ -10,11 +10,6 @@ "exports": { ".": { "types": "./dist/cjs/index.d.ts", - "browser": { - "types": "./dist/esm/browser/index.d.mts", - "import": "./dist/esm/browser/index.mjs", - "default": "./dist/esm/browser/index.mjs" - }, "import": { "types": "./dist/esm/index.d.mts", "default": "./dist/esm/index.mjs" @@ -25,18 +20,6 @@ }, "default": "./dist/cjs/index.js" }, - "./browser": { - "types": "./dist/esm/browser/index.d.mts", - "import": { - "types": "./dist/esm/browser/index.d.mts", - "default": "./dist/esm/browser/index.mjs" - }, - "require": { - "types": "./dist/cjs/browser/index.d.ts", - "default": "./dist/cjs/browser/index.js" - }, - "default": "./dist/esm/browser/index.mjs" - }, "./serialization": { "types": "./dist/cjs/serialization/index.d.ts", "import": { @@ -49,18 +32,6 @@ }, "default": "./dist/cjs/serialization/index.js" }, - "./server": { - "types": "./dist/cjs/index.d.ts", - "import": { - "types": "./dist/esm/index.d.mts", - "default": "./dist/esm/index.mjs" - }, - "require": { - "types": "./dist/cjs/index.d.ts", - "default": "./dist/cjs/index.js" - }, - "default": "./dist/cjs/index.js" - }, "./package.json": "./package.json" }, "files": [ @@ -83,6 +54,7 @@ "test:unit": "vitest --project unit", "test:wire": "vitest --project wire" }, + "dependencies": {}, "devDependencies": { "webpack": "^5.97.1", "ts-loader": "^9.5.1", @@ -90,17 +62,15 @@ "msw": "2.11.2", "@types/node": "^18.19.70", "typescript": "~5.7.2", - "@biomejs/biome": "2.2.5" + "@biomejs/biome": "2.3.1" }, "browser": { - "./dist/cjs/wrapper/utils/getBaseUrl.js": "./dist/cjs/wrapper/utils/getBaseUrl.browser.js", - "./dist/esm/wrapper/utils/getBaseUrl.mjs": "./dist/esm/wrapper/utils/getBaseUrl.browser.mjs", "fs": false, "os": false, "path": false, "stream": false }, - "packageManager": "pnpm@10.14.0", + "packageManager": "pnpm@10.20.0", "engines": { "node": ">=18.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6cd1ea..3e1df92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: 2.2.5 - version: 2.2.5 + specifier: 2.3.1 + version: 2.3.1 '@types/node': specifier: ^18.19.70 version: 18.19.130 @@ -19,68 +19,68 @@ importers: version: 2.11.2(@types/node@18.19.130)(typescript@5.7.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.7.3)(webpack@5.102.1) + version: 9.5.4(typescript@5.7.3)(webpack@5.103.0) typescript: specifier: ~5.7.2 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.0) + 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) webpack: specifier: ^5.97.1 - version: 5.102.1 + version: 5.103.0 packages: - '@biomejs/biome@2.2.5': - resolution: {integrity: sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==} + '@biomejs/biome@2.3.1': + resolution: {integrity: sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.5': - resolution: {integrity: sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ==} + '@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.2.5': - resolution: {integrity: sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg==} + '@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.2.5': - resolution: {integrity: sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw==} + '@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.2.5': - resolution: {integrity: sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ==} + '@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.2.5': - resolution: {integrity: sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ==} + '@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.2.5': - resolution: {integrity: sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew==} + '@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.2.5': - resolution: {integrity: sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg==} + '@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.2.5': - resolution: {integrity: sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw==} + '@biomejs/cli-win32-x64@2.3.1': + resolution: {integrity: sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -91,168 +91,168 @@ packages: '@bundled-es-modules/statuses@1.0.1': resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@inquirer/ansi@1.0.1': - resolution: {integrity: sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} - '@inquirer/confirm@5.1.19': - resolution: {integrity: sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -260,8 +260,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.3.0': - resolution: {integrity: sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -269,12 +269,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.14': - resolution: {integrity: sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/type@3.0.9': - resolution: {integrity: sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -311,113 +311,113 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@rollup/rollup-android-arm-eabi@4.52.5': - resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.5': - resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.5': - resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.5': - resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.5': - resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.5': - resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': - resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.5': - resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.5': - resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.5': - resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.5': - resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.5': - resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.5': - resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.5': - resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.5': - resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.5': - resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.5': - resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.5': - resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.5': - resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.5': - resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.5': - resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.5': - resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} cpu: [x64] os: [win32] @@ -567,16 +567,16 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - baseline-browser-mapping@2.8.19: - resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} + baseline-browser-mapping@2.9.5: + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} 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.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -587,8 +587,8 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - caniuse-lite@1.0.30001751: - resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -641,8 +641,8 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - electron-to-chromium@1.5.238: - resolution: {integrity: sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -654,8 +654,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -686,8 +686,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} fast-deep-equal@3.1.3: @@ -724,8 +724,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.11.0: - resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} has-flag@4.0.0: @@ -766,8 +766,8 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -809,8 +809,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.26: - resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -854,8 +854,8 @@ packages: rettime@0.7.0: resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} - rollup@4.52.5: - resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -932,8 +932,8 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.14: - resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + terser-webpack-plugin@5.3.15: + resolution: {integrity: sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -948,8 +948,8 @@ packages: uglify-js: optional: true - terser@5.44.0: - resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + terser@5.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} hasBin: true @@ -975,11 +975,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.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} - tldts@7.0.17: - resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} hasBin: true to-regex-range@5.0.1: @@ -1009,8 +1009,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.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1020,8 +1020,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@7.1.11: - resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} + vite@7.2.7: + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1096,8 +1096,8 @@ packages: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} - webpack@5.102.1: - resolution: {integrity: sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==} + webpack@5.103.0: + resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -1137,39 +1137,39 @@ packages: snapshots: - '@biomejs/biome@2.2.5': + '@biomejs/biome@2.3.1': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.5 - '@biomejs/cli-darwin-x64': 2.2.5 - '@biomejs/cli-linux-arm64': 2.2.5 - '@biomejs/cli-linux-arm64-musl': 2.2.5 - '@biomejs/cli-linux-x64': 2.2.5 - '@biomejs/cli-linux-x64-musl': 2.2.5 - '@biomejs/cli-win32-arm64': 2.2.5 - '@biomejs/cli-win32-x64': 2.2.5 - - '@biomejs/cli-darwin-arm64@2.2.5': + '@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.2.5': + '@biomejs/cli-darwin-x64@2.3.1': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.5': + '@biomejs/cli-linux-arm64-musl@2.3.1': optional: true - '@biomejs/cli-linux-arm64@2.2.5': + '@biomejs/cli-linux-arm64@2.3.1': optional: true - '@biomejs/cli-linux-x64-musl@2.2.5': + '@biomejs/cli-linux-x64-musl@2.3.1': optional: true - '@biomejs/cli-linux-x64@2.2.5': + '@biomejs/cli-linux-x64@2.3.1': optional: true - '@biomejs/cli-win32-arm64@2.2.5': + '@biomejs/cli-win32-arm64@2.3.1': optional: true - '@biomejs/cli-win32-x64@2.2.5': + '@biomejs/cli-win32-x64@2.3.1': optional: true '@bundled-es-modules/cookie@2.0.1': @@ -1180,98 +1180,98 @@ snapshots: dependencies: statuses: 2.0.2 - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.25.12': optional: true - '@inquirer/ansi@1.0.1': {} + '@inquirer/ansi@1.0.2': {} - '@inquirer/confirm@5.1.19(@types/node@18.19.130)': + '@inquirer/confirm@5.1.21(@types/node@18.19.130)': dependencies: - '@inquirer/core': 10.3.0(@types/node@18.19.130) - '@inquirer/type': 3.0.9(@types/node@18.19.130) + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) optionalDependencies: '@types/node': 18.19.130 - '@inquirer/core@10.3.0(@types/node@18.19.130)': + '@inquirer/core@10.3.2(@types/node@18.19.130)': 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.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@18.19.130) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 @@ -1280,9 +1280,9 @@ snapshots: optionalDependencies: '@types/node': 18.19.130 - '@inquirer/figures@1.0.14': {} + '@inquirer/figures@1.0.15': {} - '@inquirer/type@3.0.9(@types/node@18.19.130)': + '@inquirer/type@3.0.10(@types/node@18.19.130)': optionalDependencies: '@types/node': 18.19.130 @@ -1323,70 +1323,70 @@ snapshots: '@open-draft/until@2.1.0': {} - '@rollup/rollup-android-arm-eabi@4.52.5': + '@rollup/rollup-android-arm-eabi@4.53.3': optional: true - '@rollup/rollup-android-arm64@4.52.5': + '@rollup/rollup-android-arm64@4.53.3': optional: true - '@rollup/rollup-darwin-arm64@4.52.5': + '@rollup/rollup-darwin-arm64@4.53.3': optional: true - '@rollup/rollup-darwin-x64@4.52.5': + '@rollup/rollup-darwin-x64@4.53.3': optional: true - '@rollup/rollup-freebsd-arm64@4.52.5': + '@rollup/rollup-freebsd-arm64@4.53.3': optional: true - '@rollup/rollup-freebsd-x64@4.52.5': + '@rollup/rollup-freebsd-x64@4.53.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.5': + '@rollup/rollup-linux-arm-musleabihf@4.53.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.5': + '@rollup/rollup-linux-arm64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.5': + '@rollup/rollup-linux-arm64-musl@4.53.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.5': + '@rollup/rollup-linux-loong64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.5': + '@rollup/rollup-linux-ppc64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.5': + '@rollup/rollup-linux-riscv64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.5': + '@rollup/rollup-linux-riscv64-musl@4.53.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.5': + '@rollup/rollup-linux-s390x-gnu@4.53.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.5': + '@rollup/rollup-linux-x64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-x64-musl@4.52.5': + '@rollup/rollup-linux-x64-musl@4.53.3': optional: true - '@rollup/rollup-openharmony-arm64@4.52.5': + '@rollup/rollup-openharmony-arm64@4.53.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.5': + '@rollup/rollup-win32-arm64-msvc@4.53.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.5': + '@rollup/rollup-win32-ia32-msvc@4.53.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.5': + '@rollup/rollup-win32-x64-gnu@4.53.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.5': + '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true '@types/chai@5.2.3': @@ -1426,14 +1426,14 @@ snapshots: 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.1.11(@types/node@18.19.130)(terser@5.44.0))': + '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.2.7(@types/node@18.19.130)(terser@5.44.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 optionalDependencies: msw: 2.11.2(@types/node@18.19.130)(typescript@5.7.3) - vite: 7.1.11(@types/node@18.19.130)(terser@5.44.0) + vite: 7.2.7(@types/node@18.19.130)(terser@5.44.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -1448,7 +1448,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.2.4': @@ -1571,25 +1571,25 @@ snapshots: assertion-error@2.0.1: {} - baseline-browser-mapping@2.8.19: {} + baseline-browser-mapping@2.9.5: {} braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.27.0: + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.8.19 - caniuse-lite: 1.0.30001751 - electron-to-chromium: 1.5.238 - node-releases: 2.0.26 - update-browserslist-db: 1.1.4(browserslist@4.27.0) + baseline-browser-mapping: 2.9.5 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) buffer-from@1.1.2: {} cac@6.7.14: {} - caniuse-lite@1.0.30001751: {} + caniuse-lite@1.0.30001759: {} chai@5.3.3: dependencies: @@ -1632,7 +1632,7 @@ snapshots: deep-eql@5.0.2: {} - electron-to-chromium@1.5.238: {} + electron-to-chromium@1.5.267: {} emoji-regex@8.0.0: {} @@ -1643,34 +1643,34 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild@0.25.11: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@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 escalade@3.2.0: {} @@ -1693,7 +1693,7 @@ snapshots: events@3.3.0: {} - expect-type@1.2.2: {} + expect-type@1.3.0: {} fast-deep-equal@3.1.3: {} @@ -1716,7 +1716,7 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.11.0: {} + graphql@16.12.0: {} has-flag@4.0.0: {} @@ -1744,7 +1744,7 @@ snapshots: loupe@3.2.1: {} - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1767,13 +1767,13 @@ snapshots: 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) + '@inquirer/confirm': 5.1.21(@types/node@18.19.130) '@mswjs/interceptors': 0.39.8 '@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.11.0 + graphql: 16.12.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -1795,7 +1795,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.26: {} + node-releases@2.0.27: {} outvariant@1.4.3: {} @@ -1827,32 +1827,32 @@ snapshots: rettime@0.7.0: {} - rollup@4.52.5: + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.5 - '@rollup/rollup-android-arm64': 4.52.5 - '@rollup/rollup-darwin-arm64': 4.52.5 - '@rollup/rollup-darwin-x64': 4.52.5 - '@rollup/rollup-freebsd-arm64': 4.52.5 - '@rollup/rollup-freebsd-x64': 4.52.5 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 - '@rollup/rollup-linux-arm-musleabihf': 4.52.5 - '@rollup/rollup-linux-arm64-gnu': 4.52.5 - '@rollup/rollup-linux-arm64-musl': 4.52.5 - '@rollup/rollup-linux-loong64-gnu': 4.52.5 - '@rollup/rollup-linux-ppc64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-musl': 4.52.5 - '@rollup/rollup-linux-s390x-gnu': 4.52.5 - '@rollup/rollup-linux-x64-gnu': 4.52.5 - '@rollup/rollup-linux-x64-musl': 4.52.5 - '@rollup/rollup-openharmony-arm64': 4.52.5 - '@rollup/rollup-win32-arm64-msvc': 4.52.5 - '@rollup/rollup-win32-ia32-msvc': 4.52.5 - '@rollup/rollup-win32-x64-gnu': 4.52.5 - '@rollup/rollup-win32-x64-msvc': 4.52.5 + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 safe-buffer@5.2.1: {} @@ -1917,16 +1917,16 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.14(webpack@5.102.1): + terser-webpack-plugin@5.3.15(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.44.0 - webpack: 5.102.1 + terser: 5.44.1 + webpack: 5.103.0 - terser@5.44.0: + terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 @@ -1948,11 +1948,11 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@7.0.17: {} + tldts-core@7.0.19: {} - tldts@7.0.17: + tldts@7.0.19: dependencies: - tldts-core: 7.0.17 + tldts-core: 7.0.19 to-regex-range@5.0.1: dependencies: @@ -1960,9 +1960,9 @@ snapshots: tough-cookie@6.0.0: dependencies: - tldts: 7.0.17 + tldts: 7.0.19 - ts-loader@9.5.4(typescript@5.7.3)(webpack@5.102.1): + ts-loader@9.5.4(typescript@5.7.3)(webpack@5.103.0): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.3 @@ -1970,7 +1970,7 @@ snapshots: semver: 7.7.3 source-map: 0.7.6 typescript: 5.7.3 - webpack: 5.102.1 + webpack: 5.103.0 type-fest@4.41.0: {} @@ -1978,19 +1978,19 @@ snapshots: undici-types@5.26.5: {} - update-browserslist-db@1.1.4(browserslist@4.27.0): + update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 - vite-node@3.2.4(@types/node@18.19.130)(terser@5.44.0): + vite-node@3.2.4(@types/node@18.19.130)(terser@5.44.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.11(@types/node@18.19.130)(terser@5.44.0) + vite: 7.2.7(@types/node@18.19.130)(terser@5.44.1) transitivePeerDependencies: - '@types/node' - jiti @@ -2005,24 +2005,24 @@ snapshots: - tsx - yaml - vite@7.1.11(@types/node@18.19.130)(terser@5.44.0): + vite@7.2.7(@types/node@18.19.130)(terser@5.44.1): dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.5 + rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 18.19.130 fsevents: 2.3.3 - terser: 5.44.0 + terser: 5.44.1 - 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.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): dependencies: '@types/chai': 5.2.3 '@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.1.11(@types/node@18.19.130)(terser@5.44.0)) + '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.2.7(@types/node@18.19.130)(terser@5.44.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2030,8 +2030,8 @@ snapshots: '@vitest/utils': 3.2.4 chai: 5.3.3 debug: 4.4.3 - expect-type: 1.2.2 - magic-string: 0.30.19 + expect-type: 1.3.0 + magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 @@ -2040,8 +2040,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.11(@types/node@18.19.130)(terser@5.44.0) - vite-node: 3.2.4(@types/node@18.19.130)(terser@5.44.0) + vite: 7.2.7(@types/node@18.19.130)(terser@5.44.1) + vite-node: 3.2.4(@types/node@18.19.130)(terser@5.44.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 18.19.130 @@ -2066,7 +2066,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.102.1: + webpack@5.103.0: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -2076,7 +2076,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.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 @@ -2090,7 +2090,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.102.1) + terser-webpack-plugin: 5.3.15(webpack@5.103.0) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: diff --git a/src/BaseClient.ts b/src/BaseClient.ts index 8792c37..213de5e 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -1,10 +1,12 @@ // This file was auto-generated by Fern from our API Definition. import type * as Pipedream from "./api/index.js"; -import type * as core from "./core/index.js"; +import { OAuthAuthProvider } from "./auth/OAuthAuthProvider.js"; +import { mergeHeaders } from "./core/headers.js"; +import * as core from "./core/index.js"; import type * as environments from "./environments.js"; -export interface BaseClientOptions { +export type BaseClientOptions = { environment?: core.Supplier; /** Specify a custom URL to connect the client to. */ baseUrl?: core.Supplier; @@ -17,7 +19,11 @@ export interface BaseClientOptions { 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; +} & OAuthAuthProvider.AuthOptions; export interface BaseRequestOptions { /** The maximum time to wait for a response in seconds. */ @@ -33,3 +39,51 @@ export interface BaseRequestOptions { /** Additional headers to include in the request. */ headers?: Record | null | undefined>; } + +export type NormalizedClientOptions = T & { + logging: core.logging.Logger; + authProvider?: core.AuthProvider; +}; + +export type NormalizedClientOptionsWithAuth = NormalizedClientOptions & { + authProvider: core.AuthProvider; +}; + +export function normalizeClientOptions(options: T): NormalizedClientOptions { + const headers = mergeHeaders( + { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@pipedream/sdk", + "X-Fern-SDK-Version": "2.3.3", + "User-Agent": "@pipedream/sdk/2.3.3", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + "x-pd-environment": options?.projectEnvironment, + }, + options?.headers, + ); + + return { + ...options, + logging: core.logging.createLogger(options?.logging), + headers, + } as NormalizedClientOptions; +} + +export function normalizeClientOptionsWithAuth( + options: T, +): NormalizedClientOptionsWithAuth { + const normalized = normalizeClientOptions(options) as NormalizedClientOptionsWithAuth; + const normalizedWithNoOpAuthProvider = withNoOpAuthProvider(normalized); + normalized.authProvider ??= OAuthAuthProvider.createInstance(normalizedWithNoOpAuthProvider); + return normalized; +} + +function withNoOpAuthProvider( + options: NormalizedClientOptions, +): NormalizedClientOptionsWithAuth { + return { + ...options, + authProvider: new core.NoOpAuthProvider(), + }; +} diff --git a/src/api/errors/BadRequestError.ts b/src/api/errors/BadRequestError.ts new file mode 100644 index 0000000..35aa54c --- /dev/null +++ b/src/api/errors/BadRequestError.ts @@ -0,0 +1,21 @@ +// 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"; + +export class BadRequestError extends errors.PipedreamError { + constructor(body?: unknown, rawResponse?: core.RawResponse) { + super({ + message: "BadRequestError", + statusCode: 400, + body: body, + rawResponse: rawResponse, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = this.constructor.name; + } +} diff --git a/src/api/errors/NotFoundError.ts b/src/api/errors/NotFoundError.ts new file mode 100644 index 0000000..3e70295 --- /dev/null +++ b/src/api/errors/NotFoundError.ts @@ -0,0 +1,21 @@ +// 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"; + +export class NotFoundError extends errors.PipedreamError { + constructor(body?: unknown, rawResponse?: core.RawResponse) { + super({ + message: "NotFoundError", + statusCode: 404, + body: body, + rawResponse: rawResponse, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = this.constructor.name; + } +} diff --git a/src/api/errors/TooManyRequestsError.ts b/src/api/errors/TooManyRequestsError.ts index c1b8de3..1099027 100644 --- a/src/api/errors/TooManyRequestsError.ts +++ b/src/api/errors/TooManyRequestsError.ts @@ -11,6 +11,11 @@ export class TooManyRequestsError extends errors.PipedreamError { body: body, rawResponse: rawResponse, }); - Object.setPrototypeOf(this, TooManyRequestsError.prototype); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = this.constructor.name; } } diff --git a/src/api/errors/index.ts b/src/api/errors/index.ts index 06b0452..e6e1fe8 100644 --- a/src/api/errors/index.ts +++ b/src/api/errors/index.ts @@ -1 +1,3 @@ +export * from "./BadRequestError.js"; +export * from "./NotFoundError.js"; export * from "./TooManyRequestsError.js"; diff --git a/src/api/resources/accounts/client/Client.ts b/src/api/resources/accounts/client/Client.ts index 5020985..27d57b1 100644 --- a/src/api/resources/accounts/client/Client.ts +++ b/src/api/resources/accounts/client/Client.ts @@ -1,33 +1,33 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Accounts { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace AccountsClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Accounts { - protected readonly _options: Accounts.Options; +export class AccountsClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Accounts.Options) { - this._options = _options; + constructor(options: AccountsClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve all connected accounts for the project with optional filtering * * @param {Pipedream.AccountsListRequest} request - * @param {Accounts.RequestOptions} requestOptions - Request-specific configuration. + * @param {AccountsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -44,8 +44,8 @@ export class Accounts { */ public async list( request: Pipedream.AccountsListRequest = {}, - requestOptions?: Accounts.RequestOptions, - ): Promise> { + requestOptions?: AccountsClient.RequestOptions, + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async ( request: Pipedream.AccountsListRequest, @@ -73,10 +73,11 @@ export class Accounts { if (includeCredentials != null) { _queryParams.include_credentials = includeCredentials.toString(); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -94,6 +95,8 @@ export class Accounts { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -119,27 +122,16 @@ export class Accounts { }); } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/accounts.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/accounts", + ); }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -156,7 +148,7 @@ export class Accounts { * Connect a new account for an external user in the project * * @param {Pipedream.CreateAccountOpts} request - * @param {Accounts.RequestOptions} requestOptions - Request-specific configuration. + * @param {AccountsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -171,14 +163,14 @@ export class Accounts { */ public create( request: Pipedream.CreateAccountOpts, - requestOptions?: Accounts.RequestOptions, + requestOptions?: AccountsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); } private async __create( request: Pipedream.CreateAccountOpts, - requestOptions?: Accounts.RequestOptions, + requestOptions?: AccountsClient.RequestOptions, ): Promise> { const { externalUserId, oauthAppId, ..._body } = request; const _queryParams: Record = {}; @@ -190,10 +182,11 @@ export class Accounts { _queryParams.oauth_app_id = oauthAppId; } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -217,6 +210,8 @@ export class Accounts { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -244,31 +239,20 @@ export class Accounts { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/accounts.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/accounts", + ); } /** * Get the details for a specific connected account * - * @param {string} accountId + * @param {string} account_id * @param {Pipedream.AccountsRetrieveRequest} request - * @param {Accounts.RequestOptions} requestOptions - Request-specific configuration. + * @param {AccountsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -278,17 +262,17 @@ export class Accounts { * }) */ public retrieve( - accountId: string, + account_id: string, request: Pipedream.AccountsRetrieveRequest = {}, - requestOptions?: Accounts.RequestOptions, + requestOptions?: AccountsClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__retrieve(accountId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__retrieve(account_id, request, requestOptions)); } private async __retrieve( - accountId: string, + account_id: string, request: Pipedream.AccountsRetrieveRequest = {}, - requestOptions?: Accounts.RequestOptions, + requestOptions?: AccountsClient.RequestOptions, ): Promise> { const { includeCredentials } = request; const _queryParams: Record = {}; @@ -296,10 +280,11 @@ export class Accounts { _queryParams.include_credentials = includeCredentials.toString(); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -309,7 +294,7 @@ export class Accounts { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/accounts/${core.url.encodePathParam(accountId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/accounts/${core.url.encodePathParam(account_id)}`, ), method: "GET", headers: _headers, @@ -317,6 +302,8 @@ export class Accounts { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -344,48 +331,38 @@ export class Accounts { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/accounts/{account_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/accounts/{account_id}", + ); } /** * Remove a connected account and its associated credentials * - * @param {string} accountId - * @param {Accounts.RequestOptions} requestOptions - Request-specific configuration. + * @param {string} account_id + * @param {AccountsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * * @example * await client.accounts.delete("account_id") */ - public delete(accountId: string, requestOptions?: Accounts.RequestOptions): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__delete(accountId, requestOptions)); + public delete(account_id: string, requestOptions?: AccountsClient.RequestOptions): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__delete(account_id, requestOptions)); } private async __delete( - accountId: string, - requestOptions?: Accounts.RequestOptions, + account_id: string, + requestOptions?: AccountsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -395,7 +372,7 @@ export class Accounts { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/accounts/${core.url.encodePathParam(accountId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/accounts/${core.url.encodePathParam(account_id)}`, ), method: "DELETE", headers: _headers, @@ -403,6 +380,8 @@ export class Accounts { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: undefined, rawResponse: _response.rawResponse }; @@ -421,48 +400,38 @@ export class Accounts { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling DELETE /v1/connect/{project_id}/accounts/{account_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "DELETE", + "/v1/connect/{project_id}/accounts/{account_id}", + ); } /** * Remove all connected accounts for a specific app * - * @param {string} appId - * @param {Accounts.RequestOptions} requestOptions - Request-specific configuration. + * @param {string} app_id + * @param {AccountsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * * @example * await client.accounts.deleteByApp("app_id") */ - public deleteByApp(appId: string, requestOptions?: Accounts.RequestOptions): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__deleteByApp(appId, requestOptions)); + public deleteByApp(app_id: string, requestOptions?: AccountsClient.RequestOptions): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__deleteByApp(app_id, requestOptions)); } private async __deleteByApp( - appId: string, - requestOptions?: Accounts.RequestOptions, + app_id: string, + requestOptions?: AccountsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -472,7 +441,7 @@ export class Accounts { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/apps/${core.url.encodePathParam(appId)}/accounts`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/apps/${core.url.encodePathParam(app_id)}/accounts`, ), method: "DELETE", headers: _headers, @@ -480,6 +449,8 @@ export class Accounts { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: undefined, rawResponse: _response.rawResponse }; @@ -498,31 +469,11 @@ export class Accounts { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling DELETE /v1/connect/{project_id}/apps/{app_id}/accounts.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "DELETE", + "/v1/connect/{project_id}/apps/{app_id}/accounts", + ); } } diff --git a/src/api/resources/actions/client/Client.ts b/src/api/resources/actions/client/Client.ts index bd6bbe8..3f9875f 100644 --- a/src/api/resources/actions/client/Client.ts +++ b/src/api/resources/actions/client/Client.ts @@ -1,34 +1,35 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Actions { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace ActionsClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Actions { - protected readonly _options: Actions.Options; +export class ActionsClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Actions.Options) { - this._options = _options; + constructor(options: ActionsClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve available actions with optional search and app filtering * * @param {Pipedream.ActionsListRequest} request - * @param {Actions.RequestOptions} requestOptions - Request-specific configuration. + * @param {ActionsClient.RequestOptions} requestOptions - Request-specific configuration. * + * @throws {@link Pipedream.BadRequestError} * @throws {@link Pipedream.TooManyRequestsError} * * @example @@ -43,8 +44,8 @@ export class Actions { */ public async list( request: Pipedream.ActionsListRequest = {}, - requestOptions?: Actions.RequestOptions, - ): Promise> { + requestOptions?: ActionsClient.RequestOptions, + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async ( request: Pipedream.ActionsListRequest, @@ -72,10 +73,11 @@ export class Actions { omitUndefined: true, }); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -93,6 +95,8 @@ export class Actions { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -108,6 +112,8 @@ export class Actions { } if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { + case 400: + throw new Pipedream.BadRequestError(_response.error.body, _response.rawResponse); case 429: throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); default: @@ -118,27 +124,16 @@ export class Actions { }); } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/actions.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/actions", + ); }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -154,9 +149,9 @@ export class Actions { /** * Get detailed configuration for a specific action by its key * - * @param {string} componentId - The key that uniquely identifies the component (e.g., 'slack-send-message') + * @param {string} component_id - The key that uniquely identifies the component (e.g., 'slack-send-message') * @param {Pipedream.ActionsRetrieveRequest} request - * @param {Actions.RequestOptions} requestOptions - Request-specific configuration. + * @param {ActionsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -166,17 +161,17 @@ export class Actions { * }) */ public retrieve( - componentId: string, + component_id: string, request: Pipedream.ActionsRetrieveRequest = {}, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__retrieve(componentId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__retrieve(component_id, request, requestOptions)); } private async __retrieve( - componentId: string, + component_id: string, request: Pipedream.ActionsRetrieveRequest = {}, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): Promise> { const { version } = request; const _queryParams: Record = {}; @@ -184,10 +179,11 @@ export class Actions { _queryParams.version = version; } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -197,7 +193,7 @@ export class Actions { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/actions/${core.url.encodePathParam(componentId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/actions/${core.url.encodePathParam(component_id)}`, ), method: "GET", headers: _headers, @@ -205,6 +201,8 @@ export class Actions { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -232,30 +230,19 @@ export class Actions { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/actions/{component_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/actions/{component_id}", + ); } /** * Retrieve remote options for a given prop for a action * * @param {Pipedream.ConfigurePropOpts} request - * @param {Actions.RequestOptions} requestOptions - Request-specific configuration. + * @param {ActionsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -268,19 +255,20 @@ export class Actions { */ public configureProp( request: Pipedream.ConfigurePropOpts, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__configureProp(request, requestOptions)); } private async __configureProp( request: Pipedream.ConfigurePropOpts, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -304,6 +292,8 @@ export class Actions { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -331,30 +321,19 @@ export class Actions { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/actions/configure.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/actions/configure", + ); } /** * Reload the prop definition based on the currently configured props * * @param {Pipedream.ReloadPropsOpts} request - * @param {Actions.RequestOptions} requestOptions - Request-specific configuration. + * @param {ActionsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -366,19 +345,20 @@ export class Actions { */ public reloadProps( request: Pipedream.ReloadPropsOpts, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__reloadProps(request, requestOptions)); } private async __reloadProps( request: Pipedream.ReloadPropsOpts, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -402,6 +382,8 @@ export class Actions { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -429,30 +411,19 @@ export class Actions { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/actions/props.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/actions/props", + ); } /** * Execute an action with the provided configuration and return results * * @param {Pipedream.RunActionOpts} request - * @param {Actions.RequestOptions} requestOptions - Request-specific configuration. + * @param {ActionsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -464,19 +435,20 @@ export class Actions { */ public run( request: Pipedream.RunActionOpts, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__run(request, requestOptions)); } private async __run( request: Pipedream.RunActionOpts, - requestOptions?: Actions.RequestOptions, + requestOptions?: ActionsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -500,6 +472,8 @@ export class Actions { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -527,31 +501,11 @@ export class Actions { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/actions/run.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/actions/run", + ); } } diff --git a/src/api/resources/appCategories/client/Client.ts b/src/api/resources/appCategories/client/Client.ts index c7db14c..f89ec4c 100644 --- a/src/api/resources/appCategories/client/Client.ts +++ b/src/api/resources/appCategories/client/Client.ts @@ -1,49 +1,50 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import type * as Pipedream from "../../../index.js"; -export declare namespace AppCategories { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace AppCategoriesClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class AppCategories { - protected readonly _options: AppCategories.Options; +export class AppCategoriesClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: AppCategories.Options) { - this._options = _options; + constructor(options: AppCategoriesClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve all available categories for integrated apps * - * @param {AppCategories.RequestOptions} requestOptions - Request-specific configuration. + * @param {AppCategoriesClient.RequestOptions} requestOptions - Request-specific configuration. * * @example * await client.appCategories.list() */ public list( - requestOptions?: AppCategories.RequestOptions, + requestOptions?: AppCategoriesClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__list(requestOptions)); } private async __list( - requestOptions?: AppCategories.RequestOptions, + requestOptions?: AppCategoriesClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -61,6 +62,8 @@ export class AppCategories { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -83,47 +86,34 @@ export class AppCategories { }); } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError("Timeout exceeded when calling GET /v1/connect/app_categories."); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/connect/app_categories"); } /** * Get details of a specific app category by its ID * * @param {string} id - The ID of the app category to retrieve - * @param {AppCategories.RequestOptions} requestOptions - Request-specific configuration. + * @param {AppCategoriesClient.RequestOptions} requestOptions - Request-specific configuration. * * @example * await client.appCategories.retrieve("id") */ public retrieve( id: string, - requestOptions?: AppCategories.RequestOptions, + requestOptions?: AppCategoriesClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__retrieve(id, requestOptions)); } private async __retrieve( id: string, - requestOptions?: AppCategories.RequestOptions, + requestOptions?: AppCategoriesClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -141,6 +131,8 @@ export class AppCategories { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -163,31 +155,11 @@ export class AppCategories { }); } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/app_categories/{id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/app_categories/{id}", + ); } } diff --git a/src/api/resources/apps/client/Client.ts b/src/api/resources/apps/client/Client.ts index 2598a5f..d53355e 100644 --- a/src/api/resources/apps/client/Client.ts +++ b/src/api/resources/apps/client/Client.ts @@ -1,33 +1,33 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import type * as Pipedream from "../../../index.js"; -export declare namespace Apps { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace AppsClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Apps { - protected readonly _options: Apps.Options; +export class AppsClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Apps.Options) { - this._options = _options; + constructor(options: AppsClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve all available apps with optional filtering and sorting * * @param {Pipedream.AppsListRequest} request - * @param {Apps.RequestOptions} requestOptions - Request-specific configuration. + * @param {AppsClient.RequestOptions} requestOptions - Request-specific configuration. * * @example * await client.apps.list({ @@ -41,8 +41,8 @@ export class Apps { */ public async list( request: Pipedream.AppsListRequest = {}, - requestOptions?: Apps.RequestOptions, - ): Promise> { + requestOptions?: AppsClient.RequestOptions, + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async (request: Pipedream.AppsListRequest): Promise> => { const { after, before, limit, q, sortKey, sortDirection, categoryIds } = request; @@ -78,10 +78,11 @@ export class Apps { _queryParams.category_ids = categoryIds; } } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -99,6 +100,8 @@ export class Apps { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -119,25 +122,11 @@ export class Apps { rawResponse: _response.rawResponse, }); } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError("Timeout exceeded when calling GET /v1/connect/apps."); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/connect/apps"); }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -153,27 +142,28 @@ export class Apps { /** * Get detailed information about a specific app by ID or name slug * - * @param {string} appId - The name slug or ID of the app (e.g., 'slack', 'github') - * @param {Apps.RequestOptions} requestOptions - Request-specific configuration. + * @param {string} app_id - The name slug or ID of the app (e.g., 'slack', 'github') + * @param {AppsClient.RequestOptions} requestOptions - Request-specific configuration. * * @example * await client.apps.retrieve("app_id") */ public retrieve( - appId: string, - requestOptions?: Apps.RequestOptions, + app_id: string, + requestOptions?: AppsClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__retrieve(appId, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__retrieve(app_id, requestOptions)); } private async __retrieve( - appId: string, - requestOptions?: Apps.RequestOptions, + app_id: string, + requestOptions?: AppsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -183,7 +173,7 @@ export class Apps { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/apps/${core.url.encodePathParam(appId)}`, + `v1/connect/apps/${core.url.encodePathParam(app_id)}`, ), method: "GET", headers: _headers, @@ -191,6 +181,8 @@ export class Apps { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -213,29 +205,6 @@ export class Apps { }); } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError("Timeout exceeded when calling GET /v1/connect/apps/{app_id}."); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/connect/apps/{app_id}"); } } diff --git a/src/api/resources/components/client/Client.ts b/src/api/resources/components/client/Client.ts index d29f658..66caede 100644 --- a/src/api/resources/components/client/Client.ts +++ b/src/api/resources/components/client/Client.ts @@ -1,34 +1,35 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Components { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace ComponentsClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Components { - protected readonly _options: Components.Options; +export class ComponentsClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Components.Options) { - this._options = _options; + constructor(options: ComponentsClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve available components with optional search and app filtering * * @param {Pipedream.ComponentsListRequest} request - * @param {Components.RequestOptions} requestOptions - Request-specific configuration. + * @param {ComponentsClient.RequestOptions} requestOptions - Request-specific configuration. * + * @throws {@link Pipedream.BadRequestError} * @throws {@link Pipedream.TooManyRequestsError} * * @example @@ -44,8 +45,8 @@ export class Components { */ public async list( request: Pipedream.ComponentsListRequest = {}, - requestOptions?: Components.RequestOptions, - ): Promise> { + requestOptions?: ComponentsClient.RequestOptions, + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async ( request: Pipedream.ComponentsListRequest, @@ -79,10 +80,11 @@ export class Components { omitUndefined: true, }); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -100,6 +102,8 @@ export class Components { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -115,6 +119,8 @@ export class Components { } if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { + case 400: + throw new Pipedream.BadRequestError(_response.error.body, _response.rawResponse); case 429: throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); default: @@ -125,27 +131,16 @@ export class Components { }); } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/components.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/components", + ); }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -161,9 +156,9 @@ export class Components { /** * Get detailed configuration for a specific component by its key * - * @param {string} componentId - The key that uniquely identifies the component (e.g., 'slack-send-message') + * @param {string} component_id - The key that uniquely identifies the component (e.g., 'slack-send-message') * @param {Pipedream.ComponentsRetrieveRequest} request - * @param {Components.RequestOptions} requestOptions - Request-specific configuration. + * @param {ComponentsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -173,17 +168,17 @@ export class Components { * }) */ public retrieve( - componentId: string, + component_id: string, request: Pipedream.ComponentsRetrieveRequest = {}, - requestOptions?: Components.RequestOptions, + requestOptions?: ComponentsClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__retrieve(componentId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__retrieve(component_id, request, requestOptions)); } private async __retrieve( - componentId: string, + component_id: string, request: Pipedream.ComponentsRetrieveRequest = {}, - requestOptions?: Components.RequestOptions, + requestOptions?: ComponentsClient.RequestOptions, ): Promise> { const { version } = request; const _queryParams: Record = {}; @@ -191,10 +186,11 @@ export class Components { _queryParams.version = version; } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -204,7 +200,7 @@ export class Components { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/components/${core.url.encodePathParam(componentId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/components/${core.url.encodePathParam(component_id)}`, ), method: "GET", headers: _headers, @@ -212,6 +208,8 @@ export class Components { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -239,30 +237,19 @@ export class Components { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/components/{component_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/components/{component_id}", + ); } /** * Retrieve remote options for a given prop for a component * * @param {Pipedream.ConfigurePropOpts} request - * @param {Components.RequestOptions} requestOptions - Request-specific configuration. + * @param {ComponentsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -275,19 +262,20 @@ export class Components { */ public configureProp( request: Pipedream.ConfigurePropOpts, - requestOptions?: Components.RequestOptions, + requestOptions?: ComponentsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__configureProp(request, requestOptions)); } private async __configureProp( request: Pipedream.ConfigurePropOpts, - requestOptions?: Components.RequestOptions, + requestOptions?: ComponentsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -311,6 +299,8 @@ export class Components { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -338,30 +328,19 @@ export class Components { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/components/configure.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/components/configure", + ); } /** * Reload the prop definition based on the currently configured props * * @param {Pipedream.ReloadPropsOpts} request - * @param {Components.RequestOptions} requestOptions - Request-specific configuration. + * @param {ComponentsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -373,19 +352,20 @@ export class Components { */ public reloadProps( request: Pipedream.ReloadPropsOpts, - requestOptions?: Components.RequestOptions, + requestOptions?: ComponentsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__reloadProps(request, requestOptions)); } private async __reloadProps( request: Pipedream.ReloadPropsOpts, - requestOptions?: Components.RequestOptions, + requestOptions?: ComponentsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -409,6 +389,8 @@ export class Components { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -436,31 +418,11 @@ export class Components { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/components/props.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/components/props", + ); } } diff --git a/src/api/resources/deployedTriggers/client/Client.ts b/src/api/resources/deployedTriggers/client/Client.ts index dd2e0be..5eb9945 100644 --- a/src/api/resources/deployedTriggers/client/Client.ts +++ b/src/api/resources/deployedTriggers/client/Client.ts @@ -1,33 +1,33 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace DeployedTriggers { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace DeployedTriggersClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class DeployedTriggers { - protected readonly _options: DeployedTriggers.Options; +export class DeployedTriggersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: DeployedTriggers.Options) { - this._options = _options; + constructor(options: DeployedTriggersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve all deployed triggers for a specific external user * * @param {Pipedream.DeployedTriggersListRequest} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -42,8 +42,8 @@ export class DeployedTriggers { */ public async list( request: Pipedream.DeployedTriggersListRequest, - requestOptions?: DeployedTriggers.RequestOptions, - ): Promise> { + requestOptions?: DeployedTriggersClient.RequestOptions, + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async ( request: Pipedream.DeployedTriggersListRequest, @@ -66,10 +66,11 @@ export class DeployedTriggers { omitUndefined: true, }); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -87,6 +88,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -112,27 +115,16 @@ export class DeployedTriggers { }); } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/deployed-triggers.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/deployed-triggers", + ); }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -148,9 +140,9 @@ export class DeployedTriggers { /** * Get details of a specific deployed trigger by its ID * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.DeployedTriggersRetrieveRequest} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -160,25 +152,26 @@ export class DeployedTriggers { * }) */ public retrieve( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersRetrieveRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__retrieve(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__retrieve(trigger_id, request, requestOptions)); } private async __retrieve( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersRetrieveRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId } = request; const _queryParams: Record = {}; _queryParams.external_user_id = externalUserId; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -188,7 +181,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}`, ), method: "GET", headers: _headers, @@ -196,6 +189,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -223,31 +218,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/deployed-triggers/{trigger_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}", + ); } /** * Modify the configuration of a deployed trigger, including active status * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.UpdateTriggerOpts} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -257,25 +241,26 @@ export class DeployedTriggers { * }) */ public update( - triggerId: string, + trigger_id: string, request: Pipedream.UpdateTriggerOpts, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__update(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__update(trigger_id, request, requestOptions)); } private async __update( - triggerId: string, + trigger_id: string, request: Pipedream.UpdateTriggerOpts, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId, ..._body } = request; const _queryParams: Record = {}; _queryParams.external_user_id = externalUserId; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -285,7 +270,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}`, ), method: "PUT", headers: _headers, @@ -299,6 +284,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -326,31 +313,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling PUT /v1/connect/{project_id}/deployed-triggers/{trigger_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "PUT", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}", + ); } /** * Remove a deployed trigger and stop receiving events * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.DeployedTriggersDeleteRequest} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -361,17 +337,17 @@ export class DeployedTriggers { * }) */ public delete( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersDeleteRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__delete(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__delete(trigger_id, request, requestOptions)); } private async __delete( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersDeleteRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId, ignoreHookErrors } = request; const _queryParams: Record = {}; @@ -380,10 +356,11 @@ export class DeployedTriggers { _queryParams.ignore_hook_errors = ignoreHookErrors.toString(); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -393,7 +370,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}`, ), method: "DELETE", headers: _headers, @@ -401,6 +378,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: undefined, rawResponse: _response.rawResponse }; @@ -419,31 +398,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling DELETE /v1/connect/{project_id}/deployed-triggers/{trigger_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "DELETE", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}", + ); } /** * Retrieve recent events emitted by a deployed trigger * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.DeployedTriggersListEventsRequest} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -454,17 +422,17 @@ export class DeployedTriggers { * }) */ public listEvents( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersListEventsRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__listEvents(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__listEvents(trigger_id, request, requestOptions)); } private async __listEvents( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersListEventsRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId, n } = request; const _queryParams: Record = {}; @@ -473,10 +441,11 @@ export class DeployedTriggers { _queryParams.n = n.toString(); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -486,7 +455,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}/events`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}/events`, ), method: "GET", headers: _headers, @@ -494,6 +463,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -521,31 +492,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/deployed-triggers/{trigger_id}/events.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}/events", + ); } /** * Get workflows connected to receive events from this trigger * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.DeployedTriggersListWorkflowsRequest} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -555,25 +515,26 @@ export class DeployedTriggers { * }) */ public listWorkflows( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersListWorkflowsRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__listWorkflows(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__listWorkflows(trigger_id, request, requestOptions)); } private async __listWorkflows( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersListWorkflowsRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId } = request; const _queryParams: Record = {}; _queryParams.external_user_id = externalUserId; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -583,7 +544,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}/pipelines`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}/pipelines`, ), method: "GET", headers: _headers, @@ -591,6 +552,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -618,31 +581,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/deployed-triggers/{trigger_id}/pipelines.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}/pipelines", + ); } /** * Connect or disconnect workflows to receive trigger events * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.UpdateTriggerWorkflowsOpts} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -653,25 +605,26 @@ export class DeployedTriggers { * }) */ public updateWorkflows( - triggerId: string, + trigger_id: string, request: Pipedream.UpdateTriggerWorkflowsOpts, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__updateWorkflows(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__updateWorkflows(trigger_id, request, requestOptions)); } private async __updateWorkflows( - triggerId: string, + trigger_id: string, request: Pipedream.UpdateTriggerWorkflowsOpts, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId, ..._body } = request; const _queryParams: Record = {}; _queryParams.external_user_id = externalUserId; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -681,7 +634,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}/pipelines`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}/pipelines`, ), method: "PUT", headers: _headers, @@ -695,6 +648,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -722,31 +677,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling PUT /v1/connect/{project_id}/deployed-triggers/{trigger_id}/pipelines.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "PUT", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}/pipelines", + ); } /** * Get webhook URLs configured to receive trigger events * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.DeployedTriggersListWebhooksRequest} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -756,25 +700,26 @@ export class DeployedTriggers { * }) */ public listWebhooks( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersListWebhooksRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__listWebhooks(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__listWebhooks(trigger_id, request, requestOptions)); } private async __listWebhooks( - triggerId: string, + trigger_id: string, request: Pipedream.DeployedTriggersListWebhooksRequest, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId } = request; const _queryParams: Record = {}; _queryParams.external_user_id = externalUserId; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -784,7 +729,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}/webhooks`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}/webhooks`, ), method: "GET", headers: _headers, @@ -792,6 +737,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -819,31 +766,20 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/deployed-triggers/{trigger_id}/webhooks.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}/webhooks", + ); } /** * Configure webhook URLs to receive trigger events * - * @param {string} triggerId + * @param {string} trigger_id * @param {Pipedream.UpdateTriggerWebhooksOpts} request - * @param {DeployedTriggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {DeployedTriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -854,25 +790,26 @@ export class DeployedTriggers { * }) */ public updateWebhooks( - triggerId: string, + trigger_id: string, request: Pipedream.UpdateTriggerWebhooksOpts, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__updateWebhooks(triggerId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__updateWebhooks(trigger_id, request, requestOptions)); } private async __updateWebhooks( - triggerId: string, + trigger_id: string, request: Pipedream.UpdateTriggerWebhooksOpts, - requestOptions?: DeployedTriggers.RequestOptions, + requestOptions?: DeployedTriggersClient.RequestOptions, ): Promise> { const { externalUserId, ..._body } = request; const _queryParams: Record = {}; _queryParams.external_user_id = externalUserId; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -882,7 +819,7 @@ export class DeployedTriggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(triggerId)}/webhooks`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/deployed-triggers/${core.url.encodePathParam(trigger_id)}/webhooks`, ), method: "PUT", headers: _headers, @@ -896,6 +833,8 @@ export class DeployedTriggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -923,31 +862,11 @@ export class DeployedTriggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling PUT /v1/connect/{project_id}/deployed-triggers/{trigger_id}/webhooks.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "PUT", + "/v1/connect/{project_id}/deployed-triggers/{trigger_id}/webhooks", + ); } } diff --git a/src/api/resources/fileStash/client/Client.ts b/src/api/resources/fileStash/client/Client.ts index 6031153..5190224 100644 --- a/src/api/resources/fileStash/client/Client.ts +++ b/src/api/resources/fileStash/client/Client.ts @@ -1,25 +1,25 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace FileStash { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace FileStashClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class FileStash { - protected readonly _options: FileStash.Options; +export class FileStashClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: FileStash.Options) { - this._options = _options; + constructor(options: FileStashClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** @@ -28,22 +28,23 @@ export class FileStash { */ public downloadFile( request: Pipedream.FileStashDownloadFileRequest, - requestOptions?: FileStash.RequestOptions, + requestOptions?: FileStashClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__downloadFile(request, requestOptions)); } private async __downloadFile( request: Pipedream.FileStashDownloadFileRequest, - requestOptions?: FileStash.RequestOptions, + requestOptions?: FileStashClient.RequestOptions, ): Promise> { const { s3Key } = request; const _queryParams: Record = {}; _queryParams.s3_key = s3Key; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -62,6 +63,8 @@ export class FileStash { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body, rawResponse: _response.rawResponse }; @@ -80,31 +83,11 @@ export class FileStash { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/file_stash/download.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/file_stash/download", + ); } } diff --git a/src/api/resources/oauthTokens/client/Client.ts b/src/api/resources/oauthTokens/client/Client.ts index 34bfef5..5a3d31b 100644 --- a/src/api/resources/oauthTokens/client/Client.ts +++ b/src/api/resources/oauthTokens/client/Client.ts @@ -1,33 +1,33 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import type * as Pipedream from "../../../index.js"; -export declare namespace OauthTokens { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace OauthTokensClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class OauthTokens { - protected readonly _options: OauthTokens.Options; +export class OauthTokensClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: OauthTokens.Options) { - this._options = _options; + constructor(options: OauthTokensClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Exchange OAuth credentials for an access token * * @param {Pipedream.CreateOAuthTokenOpts} request - * @param {OauthTokens.RequestOptions} requestOptions - Request-specific configuration. + * @param {OauthTokensClient.RequestOptions} requestOptions - Request-specific configuration. * * @example * await client.oauthTokens.create({ @@ -37,19 +37,18 @@ export class OauthTokens { */ public create( request: Pipedream.CreateOAuthTokenOpts, - requestOptions?: OauthTokens.RequestOptions, + requestOptions?: OauthTokensClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); } private async __create( request: Pipedream.CreateOAuthTokenOpts, - requestOptions?: OauthTokens.RequestOptions, + requestOptions?: OauthTokensClient.RequestOptions, ): Promise> { const _headers: core.Fetcher.Args["headers"] = mergeHeaders( this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -76,6 +75,8 @@ export class OauthTokens { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -98,29 +99,6 @@ export class OauthTokens { }); } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError("Timeout exceeded when calling POST /v1/oauth/token."); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/oauth/token"); } } diff --git a/src/api/resources/projects/client/Client.ts b/src/api/resources/projects/client/Client.ts index 0e68abc..dceabf3 100644 --- a/src/api/resources/projects/client/Client.ts +++ b/src/api/resources/projects/client/Client.ts @@ -1,32 +1,534 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Projects { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace ProjectsClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Projects { - protected readonly _options: Projects.Options; +export class ProjectsClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Projects.Options) { - this._options = _options; + constructor(options: ProjectsClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + /** + * List the projects that are available to the authenticated Connect client + * + * @param {Pipedream.ProjectsListRequest} request + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Pipedream.NotFoundError} + * @throws {@link Pipedream.TooManyRequestsError} + * + * @example + * await client.projects.list({ + * after: "after", + * before: "before", + * limit: 1, + * q: "q" + * }) + */ + public async list( + request: Pipedream.ProjectsListRequest = {}, + requestOptions?: ProjectsClient.RequestOptions, + ): Promise> { + const list = core.HttpResponsePromise.interceptFunction( + async ( + request: Pipedream.ProjectsListRequest, + ): Promise> => { + const { after, before, limit, q } = request; + const _queryParams: Record = {}; + if (after != null) { + _queryParams.after = after; + } + if (before != null) { + _queryParams.before = before; + } + if (limit != null) { + _queryParams.limit = limit.toString(); + } + if (q != null) { + _queryParams.q = q; + } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + mergeOnlyDefinedHeaders({ + "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, + }), + 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)) ?? + environments.PipedreamEnvironment.Prod, + "v1/connect/projects", + ), + method: "GET", + headers: _headers, + queryParameters: { ..._queryParams, ...requestOptions?.queryParams }, + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.ListProjectsResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 404: + throw new Pipedream.NotFoundError(_response.error.body, _response.rawResponse); + case 429: + throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); + default: + throw new errors.PipedreamError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/connect/projects"); + }, + ); + const dataWithRawResponse = await list(request).withRawResponse(); + return new core.Page({ + response: dataWithRawResponse.data, + rawResponse: dataWithRawResponse.rawResponse, + hasNextPage: (response) => + response?.pageInfo.endCursor != null && + !(typeof response?.pageInfo.endCursor === "string" && response?.pageInfo.endCursor === ""), + getItems: (response) => response?.data ?? [], + loadPage: (response) => { + return list(core.setObjectProperty(request, "after", response?.pageInfo.endCursor)); + }, + }); + } + + /** + * Create a new project for the authenticated workspace + * + * @param {Pipedream.CreateProjectOpts} request + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Pipedream.TooManyRequestsError} + * + * @example + * await client.projects.create({ + * name: "name" + * }) + */ + public create( + request: Pipedream.CreateProjectOpts, + requestOptions?: ProjectsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); + } + + private async __create( + request: Pipedream.CreateProjectOpts, + requestOptions?: ProjectsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + mergeOnlyDefinedHeaders({ + "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, + }), + 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)) ?? + environments.PipedreamEnvironment.Prod, + "v1/connect/projects", + ), + method: "POST", + headers: _headers, + contentType: "application/json", + queryParameters: requestOptions?.queryParams, + requestType: "json", + body: serializers.CreateProjectOpts.jsonOrThrow(request, { + unrecognizedObjectKeys: "strip", + omitUndefined: true, + }), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.Project.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 429: + throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); + default: + throw new errors.PipedreamError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/connect/projects"); + } + + /** + * Get the project details for a specific project + * + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Pipedream.TooManyRequestsError} + * + * @example + * await client.projects.retrieve() + */ + public retrieve(requestOptions?: ProjectsClient.RequestOptions): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__retrieve(requestOptions)); + } + + private async __retrieve( + requestOptions?: ProjectsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + mergeOnlyDefinedHeaders({ + "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, + }), + 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)) ?? + environments.PipedreamEnvironment.Prod, + `v1/connect/projects/${core.url.encodePathParam(this._options.projectId)}`, + ), + method: "GET", + headers: _headers, + queryParameters: requestOptions?.queryParams, + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.Project.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 429: + throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); + default: + throw new errors.PipedreamError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/projects/{project_id}", + ); + } + + /** + * Delete a project owned by the authenticated workspace + * + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Pipedream.TooManyRequestsError} + * + * @example + * await client.projects.delete() + */ + public delete(requestOptions?: ProjectsClient.RequestOptions): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__delete(requestOptions)); + } + + private async __delete(requestOptions?: ProjectsClient.RequestOptions): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + mergeOnlyDefinedHeaders({ + "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, + }), + 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)) ?? + environments.PipedreamEnvironment.Prod, + `v1/connect/projects/${core.url.encodePathParam(this._options.projectId)}`, + ), + method: "DELETE", + headers: _headers, + queryParameters: requestOptions?.queryParams, + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: undefined, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 429: + throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); + default: + throw new errors.PipedreamError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "DELETE", + "/v1/connect/projects/{project_id}", + ); + } + + /** + * Update project details or application information + * + * @param {Pipedream.UpdateProjectOpts} request + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Pipedream.TooManyRequestsError} + * + * @example + * await client.projects.update() + */ + public update( + request: Pipedream.UpdateProjectOpts = {}, + requestOptions?: ProjectsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__update(request, requestOptions)); + } + + private async __update( + request: Pipedream.UpdateProjectOpts = {}, + requestOptions?: ProjectsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + mergeOnlyDefinedHeaders({ + "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, + }), + 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)) ?? + environments.PipedreamEnvironment.Prod, + `v1/connect/projects/${core.url.encodePathParam(this._options.projectId)}`, + ), + method: "PATCH", + headers: _headers, + contentType: "application/json", + queryParameters: requestOptions?.queryParams, + requestType: "json", + body: serializers.UpdateProjectOpts.jsonOrThrow(request, { + unrecognizedObjectKeys: "strip", + omitUndefined: true, + }), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.Project.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 429: + throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); + default: + throw new errors.PipedreamError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "PATCH", + "/v1/connect/projects/{project_id}", + ); + } + + /** + * Upload or replace the project logo + * + * @param {Pipedream.UpdateProjectLogoOpts} request + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link Pipedream.BadRequestError} + * @throws {@link Pipedream.TooManyRequestsError} + * + * @example + * await client.projects.updateLogo({ + * logo: "data:image/png;base64,AAAAAA..." + * }) + */ + public updateLogo( + request: Pipedream.UpdateProjectLogoOpts, + requestOptions?: ProjectsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__updateLogo(request, requestOptions)); + } + + private async __updateLogo( + request: Pipedream.UpdateProjectLogoOpts, + requestOptions?: ProjectsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + mergeOnlyDefinedHeaders({ + "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, + }), + 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)) ?? + environments.PipedreamEnvironment.Prod, + `v1/connect/projects/${core.url.encodePathParam(this._options.projectId)}/logo`, + ), + method: "POST", + headers: _headers, + contentType: "application/json", + queryParameters: requestOptions?.queryParams, + requestType: "json", + body: serializers.UpdateProjectLogoOpts.jsonOrThrow(request, { + unrecognizedObjectKeys: "strip", + omitUndefined: true, + }), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { data: undefined, rawResponse: _response.rawResponse }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Pipedream.BadRequestError(_response.error.body, _response.rawResponse); + case 429: + throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); + default: + throw new errors.PipedreamError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/projects/{project_id}/logo", + ); } /** * Retrieve project configuration and environment details * - * @param {Projects.RequestOptions} requestOptions - Request-specific configuration. + * @param {ProjectsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -34,18 +536,19 @@ export class Projects { * await client.projects.retrieveInfo() */ public retrieveInfo( - requestOptions?: Projects.RequestOptions, + requestOptions?: ProjectsClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__retrieveInfo(requestOptions)); } private async __retrieveInfo( - requestOptions?: Projects.RequestOptions, + requestOptions?: ProjectsClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -63,6 +566,8 @@ export class Projects { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -90,31 +595,11 @@ export class Projects { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/projects/info.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/projects/info", + ); } } diff --git a/src/api/resources/projects/client/index.ts b/src/api/resources/projects/client/index.ts index cb0ff5c..195f9aa 100644 --- a/src/api/resources/projects/client/index.ts +++ b/src/api/resources/projects/client/index.ts @@ -1 +1 @@ -export {}; +export * from "./requests/index.js"; diff --git a/src/api/resources/projects/client/requests/CreateProjectOpts.ts b/src/api/resources/projects/client/requests/CreateProjectOpts.ts new file mode 100644 index 0000000..7499c3b --- /dev/null +++ b/src/api/resources/projects/client/requests/CreateProjectOpts.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * name: "name" + * } + */ +export interface CreateProjectOpts { + /** Name of the project */ + name: string; + /** Display name for the Connect application */ + appName?: string; + /** Support email displayed to end users */ + supportEmail?: string; +} diff --git a/src/api/resources/projects/client/requests/ProjectsListRequest.ts b/src/api/resources/projects/client/requests/ProjectsListRequest.ts new file mode 100644 index 0000000..a61bc3f --- /dev/null +++ b/src/api/resources/projects/client/requests/ProjectsListRequest.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * after: "after", + * before: "before", + * limit: 1, + * q: "q" + * } + */ +export interface ProjectsListRequest { + /** The cursor to start from for pagination */ + after?: string; + /** The cursor to end before for pagination */ + before?: string; + /** The maximum number of results to return */ + limit?: number; + /** A search query to filter the projects */ + q?: string; +} diff --git a/src/api/resources/projects/client/requests/UpdateProjectLogoOpts.ts b/src/api/resources/projects/client/requests/UpdateProjectLogoOpts.ts new file mode 100644 index 0000000..afab4e0 --- /dev/null +++ b/src/api/resources/projects/client/requests/UpdateProjectLogoOpts.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * logo: "data:image/png;base64,AAAAAA..." + * } + */ +export interface UpdateProjectLogoOpts { + /** Data URI containing the new Base64 encoded image */ + logo: string; +} diff --git a/src/api/resources/projects/client/requests/UpdateProjectOpts.ts b/src/api/resources/projects/client/requests/UpdateProjectOpts.ts new file mode 100644 index 0000000..5a5ba1e --- /dev/null +++ b/src/api/resources/projects/client/requests/UpdateProjectOpts.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * {} + */ +export interface UpdateProjectOpts { + /** Name of the project */ + name?: string; + /** Display name for the Connect application */ + appName?: string; + /** Support email displayed to end users */ + supportEmail?: string; +} diff --git a/src/api/resources/projects/client/requests/index.ts b/src/api/resources/projects/client/requests/index.ts new file mode 100644 index 0000000..523a354 --- /dev/null +++ b/src/api/resources/projects/client/requests/index.ts @@ -0,0 +1,4 @@ +export type { CreateProjectOpts } from "./CreateProjectOpts.js"; +export type { ProjectsListRequest } from "./ProjectsListRequest.js"; +export type { UpdateProjectLogoOpts } from "./UpdateProjectLogoOpts.js"; +export type { UpdateProjectOpts } from "./UpdateProjectOpts.js"; diff --git a/src/api/resources/tokens/client/Client.ts b/src/api/resources/tokens/client/Client.ts index 1515718..d42a894 100644 --- a/src/api/resources/tokens/client/Client.ts +++ b/src/api/resources/tokens/client/Client.ts @@ -1,33 +1,33 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Tokens { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace TokensClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Tokens { - protected readonly _options: Tokens.Options; +export class TokensClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Tokens.Options) { - this._options = _options; + constructor(options: TokensClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Generate a Connect token to use for client-side authentication * * @param {Pipedream.CreateTokenOpts} request - * @param {Tokens.RequestOptions} requestOptions - Request-specific configuration. + * @param {TokensClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -38,19 +38,20 @@ export class Tokens { */ public create( request: Pipedream.CreateTokenOpts, - requestOptions?: Tokens.RequestOptions, + requestOptions?: TokensClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); } private async __create( request: Pipedream.CreateTokenOpts, - requestOptions?: Tokens.RequestOptions, + requestOptions?: TokensClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -74,6 +75,8 @@ export class Tokens { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -101,23 +104,12 @@ export class Tokens { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/tokens.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/tokens", + ); } /** @@ -125,7 +117,7 @@ export class Tokens { * * @param {Pipedream.ConnectToken} ctok * @param {Pipedream.TokensValidateRequest} request - * @param {Tokens.RequestOptions} requestOptions - Request-specific configuration. + * @param {TokensClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -138,7 +130,7 @@ export class Tokens { public validate( ctok: Pipedream.ConnectToken, request: Pipedream.TokensValidateRequest, - requestOptions?: Tokens.RequestOptions, + requestOptions?: TokensClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__validate(ctok, request, requestOptions)); } @@ -146,7 +138,7 @@ export class Tokens { private async __validate( ctok: Pipedream.ConnectToken, request: Pipedream.TokensValidateRequest, - requestOptions?: Tokens.RequestOptions, + requestOptions?: TokensClient.RequestOptions, ): Promise> { const { appId, oauthAppId } = request; const _queryParams: Record = {}; @@ -155,10 +147,11 @@ export class Tokens { _queryParams.oauth_app_id = oauthAppId; } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -176,6 +169,8 @@ export class Tokens { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -203,31 +198,11 @@ export class Tokens { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/tokens/{ctok}/validate.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/tokens/{ctok}/validate", + ); } } diff --git a/src/api/resources/triggers/client/Client.ts b/src/api/resources/triggers/client/Client.ts index eccecd9..7d87ed5 100644 --- a/src/api/resources/triggers/client/Client.ts +++ b/src/api/resources/triggers/client/Client.ts @@ -1,34 +1,35 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as serializers from "../../../../serialization/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Triggers { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace TriggersClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Triggers { - protected readonly _options: Triggers.Options; +export class TriggersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Triggers.Options) { - this._options = _options; + constructor(options: TriggersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Retrieve available triggers with optional search and app filtering * * @param {Pipedream.TriggersListRequest} request - * @param {Triggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration. * + * @throws {@link Pipedream.BadRequestError} * @throws {@link Pipedream.TooManyRequestsError} * * @example @@ -43,8 +44,8 @@ export class Triggers { */ public async list( request: Pipedream.TriggersListRequest = {}, - requestOptions?: Triggers.RequestOptions, - ): Promise> { + requestOptions?: TriggersClient.RequestOptions, + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async ( request: Pipedream.TriggersListRequest, @@ -72,10 +73,11 @@ export class Triggers { omitUndefined: true, }); } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -93,6 +95,8 @@ export class Triggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -108,6 +112,8 @@ export class Triggers { } if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { + case 400: + throw new Pipedream.BadRequestError(_response.error.body, _response.rawResponse); case 429: throw new Pipedream.TooManyRequestsError(_response.error.body, _response.rawResponse); default: @@ -118,27 +124,16 @@ export class Triggers { }); } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/triggers.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/triggers", + ); }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -154,9 +149,9 @@ export class Triggers { /** * Get detailed configuration for a specific trigger by its key * - * @param {string} componentId - The key that uniquely identifies the component (e.g., 'slack-send-message') + * @param {string} component_id - The key that uniquely identifies the component (e.g., 'slack-send-message') * @param {Pipedream.TriggersRetrieveRequest} request - * @param {Triggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -166,17 +161,17 @@ export class Triggers { * }) */ public retrieve( - componentId: string, + component_id: string, request: Pipedream.TriggersRetrieveRequest = {}, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__retrieve(componentId, request, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__retrieve(component_id, request, requestOptions)); } private async __retrieve( - componentId: string, + component_id: string, request: Pipedream.TriggersRetrieveRequest = {}, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): Promise> { const { version } = request; const _queryParams: Record = {}; @@ -184,10 +179,11 @@ export class Triggers { _queryParams.version = version; } + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -197,7 +193,7 @@ export class Triggers { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/triggers/${core.url.encodePathParam(componentId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/triggers/${core.url.encodePathParam(component_id)}`, ), method: "GET", headers: _headers, @@ -205,6 +201,8 @@ export class Triggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -232,30 +230,19 @@ export class Triggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling GET /v1/connect/{project_id}/triggers/{component_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/v1/connect/{project_id}/triggers/{component_id}", + ); } /** * Retrieve remote options for a given prop for a trigger * * @param {Pipedream.ConfigurePropOpts} request - * @param {Triggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -268,19 +255,20 @@ export class Triggers { */ public configureProp( request: Pipedream.ConfigurePropOpts, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__configureProp(request, requestOptions)); } private async __configureProp( request: Pipedream.ConfigurePropOpts, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -304,6 +292,8 @@ export class Triggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -331,30 +321,19 @@ export class Triggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/triggers/configure.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/triggers/configure", + ); } /** * Reload the prop definition based on the currently configured props * * @param {Pipedream.ReloadPropsOpts} request - * @param {Triggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -366,19 +345,20 @@ export class Triggers { */ public reloadProps( request: Pipedream.ReloadPropsOpts, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__reloadProps(request, requestOptions)); } private async __reloadProps( request: Pipedream.ReloadPropsOpts, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -402,6 +382,8 @@ export class Triggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -429,30 +411,19 @@ export class Triggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/triggers/props.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/triggers/props", + ); } /** * Deploy a trigger to listen for and emit events * * @param {Pipedream.DeployTriggerOpts} request - * @param {Triggers.RequestOptions} requestOptions - Request-specific configuration. + * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -464,19 +435,20 @@ export class Triggers { */ public deploy( request: Pipedream.DeployTriggerOpts, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise(this.__deploy(request, requestOptions)); } private async __deploy( request: Pipedream.DeployTriggerOpts, - requestOptions?: Triggers.RequestOptions, + requestOptions?: TriggersClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -500,6 +472,8 @@ export class Triggers { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { @@ -527,31 +501,11 @@ export class Triggers { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling POST /v1/connect/{project_id}/triggers/deploy.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/v1/connect/{project_id}/triggers/deploy", + ); } } diff --git a/src/api/resources/users/client/Client.ts b/src/api/resources/users/client/Client.ts index c4a16e0..41dba9a 100644 --- a/src/api/resources/users/client/Client.ts +++ b/src/api/resources/users/client/Client.ts @@ -1,32 +1,32 @@ // This file was auto-generated by Fern from our API Definition. import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; import * as core from "../../../../core/index.js"; import * as environments from "../../../../environments.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; import * as errors from "../../../../errors/index.js"; import * as Pipedream from "../../../index.js"; -export declare namespace Users { - export interface Options extends BaseClientOptions { - token?: core.Supplier; - } +export declare namespace UsersClient { + export type Options = BaseClientOptions; export interface RequestOptions extends BaseRequestOptions {} } -export class Users { - protected readonly _options: Users.Options; +export class UsersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; - constructor(_options: Users.Options) { - this._options = _options; + constructor(options: UsersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); } /** * Remove an external user and all their associated accounts and resources * - * @param {string} externalUserId - * @param {Users.RequestOptions} requestOptions - Request-specific configuration. + * @param {string} external_user_id + * @param {UsersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link Pipedream.TooManyRequestsError} * @@ -34,20 +34,21 @@ export class Users { * await client.users.deleteExternalUser("external_user_id") */ public deleteExternalUser( - externalUserId: string, - requestOptions?: Users.RequestOptions, + external_user_id: string, + requestOptions?: UsersClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__deleteExternalUser(externalUserId, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__deleteExternalUser(external_user_id, requestOptions)); } private async __deleteExternalUser( - externalUserId: string, - requestOptions?: Users.RequestOptions, + external_user_id: string, + requestOptions?: UsersClient.RequestOptions, ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, this._options?.headers, mergeOnlyDefinedHeaders({ - Authorization: await this._getAuthorizationHeader(), "x-pd-environment": requestOptions?.projectEnvironment ?? this._options?.projectEnvironment, }), requestOptions?.headers, @@ -57,7 +58,7 @@ export class Users { (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)) ?? environments.PipedreamEnvironment.Prod, - `v1/connect/${core.url.encodePathParam(this._options.projectId)}/users/${core.url.encodePathParam(externalUserId)}`, + `v1/connect/${core.url.encodePathParam(this._options.projectId)}/users/${core.url.encodePathParam(external_user_id)}`, ), method: "DELETE", headers: _headers, @@ -65,6 +66,8 @@ export class Users { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: undefined, rawResponse: _response.rawResponse }; @@ -83,31 +86,11 @@ export class Users { } } - switch (_response.error.reason) { - case "non-json": - throw new errors.PipedreamError({ - statusCode: _response.error.statusCode, - body: _response.error.rawBody, - rawResponse: _response.rawResponse, - }); - case "timeout": - throw new errors.PipedreamTimeoutError( - "Timeout exceeded when calling DELETE /v1/connect/{project_id}/users/{external_user_id}.", - ); - case "unknown": - throw new errors.PipedreamError({ - message: _response.error.errorMessage, - rawResponse: _response.rawResponse, - }); - } - } - - protected async _getAuthorizationHeader(): Promise { - const bearer = await core.Supplier.get(this._options.token); - if (bearer != null) { - return `Bearer ${bearer}`; - } - - return undefined; + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "DELETE", + "/v1/connect/{project_id}/users/{external_user_id}", + ); } } diff --git a/src/api/types/ConfigurableProp.ts b/src/api/types/ConfigurableProp.ts index 9ace9cf..0018c3d 100644 --- a/src/api/types/ConfigurableProp.ts +++ b/src/api/types/ConfigurableProp.ts @@ -23,7 +23,7 @@ export type ConfigurableProp = | Pipedream.ConfigurableProp.DiscordChannel | Pipedream.ConfigurableProp.DiscordChannelArray | Pipedream.ConfigurableProp.Integer - | Pipedream.ConfigurableProp.Object + | Pipedream.ConfigurableProp.Object_ | Pipedream.ConfigurableProp.String | Pipedream.ConfigurableProp.StringArray; @@ -96,7 +96,7 @@ export namespace ConfigurableProp { type: "integer"; } - export interface Object extends Pipedream.ConfigurablePropObject { + export interface Object_ extends Pipedream.ConfigurablePropObject { type: "object"; } diff --git a/src/api/types/ConfigurablePropType.ts b/src/api/types/ConfigurablePropType.ts deleted file mode 100644 index 0153c34..0000000 --- a/src/api/types/ConfigurablePropType.ts +++ /dev/null @@ -1,28 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -export const ConfigurablePropType = { - AirtableBaseId: "$.airtable.baseId", - AirtableFieldId: "$.airtable.fieldId", - AirtableTableId: "$.airtable.tableId", - AirtableViewId: "$.airtable.viewId", - DiscordChannel: "$.discord.channel", - DiscordChannelArray: "$.discord.channel[]", - InterfaceApphook: "$.interface.apphook", - InterfaceHttp: "$.interface.http", - InterfaceTimer: "$.interface.timer", - ServiceDb: "$.service.db", - Alert: "alert", - Any: "any", - App: "app", - Boolean: "boolean", - DataStore: "data_store", - Dir: "dir", - HttpRequest: "http_request", - Integer: "integer", - IntegerArray: "integer[]", - Object: "object", - Sql: "sql", - String: "string", - StringArray: "string[]", -} as const; -export type ConfigurablePropType = (typeof ConfigurablePropType)[keyof typeof ConfigurablePropType]; diff --git a/src/api/types/ListProjectsResponse.ts b/src/api/types/ListProjectsResponse.ts new file mode 100644 index 0000000..aa422dd --- /dev/null +++ b/src/api/types/ListProjectsResponse.ts @@ -0,0 +1,11 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../index.js"; + +/** + * Response received when listing Connect projects + */ +export interface ListProjectsResponse { + data: Pipedream.Project[]; + pageInfo: Pipedream.PageInfo; +} diff --git a/src/api/types/Project.ts b/src/api/types/Project.ts new file mode 100644 index 0000000..5fc26df --- /dev/null +++ b/src/api/types/Project.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * Project that can be accessed via the Connect API + */ +export interface Project { + /** Hash ID for the project */ + id: string; + /** Display name of the project */ + name: string; + /** App name shown to Connect users */ + appName?: string; + /** Support email configured for the project */ + supportEmail?: string; +} diff --git a/src/api/types/ProxyResponseBinary.ts b/src/api/types/ProxyResponseBinary.ts new file mode 100644 index 0000000..e9d59fa --- /dev/null +++ b/src/api/types/ProxyResponseBinary.ts @@ -0,0 +1,6 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * Binary response from the proxied API + */ +export type ProxyResponseBinary = string; diff --git a/src/auth/OAuthAuthProvider.ts b/src/auth/OAuthAuthProvider.ts new file mode 100644 index 0000000..cc0b915 --- /dev/null +++ b/src/auth/OAuthAuthProvider.ts @@ -0,0 +1,149 @@ +// This file was auto-generated by Fern from our API Definition. + +import { OauthTokensClient } from "../api/resources/oauthTokens/client/Client.js"; +import type { BaseClientOptions } from "../BaseClient.js"; +import * as core from "../core/index.js"; +import * as errors from "../errors/index.js"; + +export class OAuthAuthProvider implements core.AuthProvider { + private readonly BUFFER_IN_MINUTES: number = 2; + private readonly _clientId: core.Supplier | undefined; + private readonly _clientSecret: core.Supplier | undefined; + private readonly _authClient: OauthTokensClient; + private _accessToken: string | undefined; + private _expiresAt: Date; + private _refreshPromise: Promise | undefined; + + constructor(options: OAuthAuthProvider.Options & OAuthAuthProvider.AuthOptions.ClientCredentials) { + this._clientId = options.clientId; + this._clientSecret = options.clientSecret; + this._authClient = new OauthTokensClient(options); + this._expiresAt = new Date(); + } + + public static canCreate( + options: OAuthAuthProvider.Options, + ): options is OAuthAuthProvider.Options & OAuthAuthProvider.AuthOptions.ClientCredentials { + return ( + (("clientId" in options && options.clientId != null) || process.env?.PIPEDREAM_CLIENT_ID != null) && + (("clientSecret" in options && options.clientSecret != null) || + process.env?.PIPEDREAM_CLIENT_SECRET != null) + ); + } + + public async getAuthRequest(arg?: { endpointMetadata?: core.EndpointMetadata }): Promise { + const token = await this.getToken(arg); + + return { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + } + + private async getToken(arg?: { endpointMetadata?: core.EndpointMetadata }): Promise { + if (this._accessToken && this._expiresAt > new Date()) { + return this._accessToken; + } + // If a refresh is already in progress, return the existing promise + if (this._refreshPromise != null) { + return this._refreshPromise; + } + return this.refresh(arg); + } + + private async refresh(_arg?: { endpointMetadata?: core.EndpointMetadata }): Promise { + this._refreshPromise = (async () => { + try { + const clientId = (await core.Supplier.get(this._clientId)) ?? process.env?.PIPEDREAM_CLIENT_ID; + if (clientId == null) { + throw new errors.PipedreamError({ + message: + "clientId is required; either pass it as an argument or set the PIPEDREAM_CLIENT_ID environment variable", + }); + } + + const clientSecret = + (await core.Supplier.get(this._clientSecret)) ?? process.env?.PIPEDREAM_CLIENT_SECRET; + if (clientSecret == null) { + throw new errors.PipedreamError({ + message: + "clientSecret is required; either pass it as an argument or set the PIPEDREAM_CLIENT_SECRET environment variable", + }); + } + const tokenResponse = await this._authClient.create({ + clientId: clientId, + clientSecret: clientSecret, + }); + + this._accessToken = tokenResponse.accessToken; + this._expiresAt = this.getExpiresAt(tokenResponse.expiresIn, this.BUFFER_IN_MINUTES); + return this._accessToken; + } finally { + this._refreshPromise = undefined; + } + })(); + return this._refreshPromise; + } + + private getExpiresAt(expiresInSeconds: number, bufferInMinutes: number): Date { + const now = new Date(); + return new Date(now.getTime() + expiresInSeconds * 1000 - bufferInMinutes * 60 * 1000); + } +} + +export class OAuthTokenOverrideAuthProvider implements core.AuthProvider { + private readonly _token: core.Supplier; + + constructor(options: OAuthAuthProvider.Options & OAuthAuthProvider.AuthOptions.TokenOverride) { + if (options.token == null) { + throw new errors.PipedreamError({ + message: "token is required. Please provide it in options.", + }); + } + this._token = options.token; + } + + public static canCreate( + options: OAuthAuthProvider.Options, + ): options is OAuthAuthProvider.Options & OAuthAuthProvider.AuthOptions.TokenOverride { + return "token" in options && options.token != null; + } + + public async getAuthRequest(_arg?: { endpointMetadata?: core.EndpointMetadata }): Promise { + return { + headers: { + Authorization: `Bearer ${await core.Supplier.get(this._token)}`, + }, + }; + } +} + +export namespace OAuthAuthProvider { + export type AuthOptions = AuthOptions.ClientCredentials | AuthOptions.TokenOverride; + + export namespace AuthOptions { + export interface ClientCredentials { + clientId: core.Supplier | undefined; + clientSecret: core.Supplier | undefined; + } + + export interface TokenOverride { + token: core.Supplier; + } + } + + export type Options = BaseClientOptions; + + export function createInstance(options: Options): core.AuthProvider { + if (OAuthTokenOverrideAuthProvider.canCreate(options)) { + return new OAuthTokenOverrideAuthProvider(options); + } else if (OAuthAuthProvider.canCreate(options)) { + return new OAuthAuthProvider(options); + } + throw new errors.PipedreamError({ + message: + "Insufficient options to create OAuthAuthProvider. Please provide either clientId and clientSecret, or token.", + }); + } +} diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..83e326d --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1 @@ +export { OAuthAuthProvider } from "./OAuthAuthProvider.js"; diff --git a/src/core/auth/AuthProvider.ts b/src/core/auth/AuthProvider.ts index 86f670a..895a50f 100644 --- a/src/core/auth/AuthProvider.ts +++ b/src/core/auth/AuthProvider.ts @@ -1,5 +1,6 @@ +import type { EndpointMetadata } from "../fetcher/EndpointMetadata.js"; import type { AuthRequest } from "./AuthRequest.js"; export interface AuthProvider { - getAuthRequest(): Promise; + getAuthRequest(arg?: { endpointMetadata?: EndpointMetadata }): Promise; } diff --git a/src/core/auth/BearerToken.ts b/src/core/auth/BearerToken.ts index fe987fc..c44a06c 100644 --- a/src/core/auth/BearerToken.ts +++ b/src/core/auth/BearerToken.ts @@ -2,13 +2,18 @@ export type BearerToken = string; const BEARER_AUTH_HEADER_PREFIX = /^Bearer /i; -export const BearerToken = { - toAuthorizationHeader: (token: BearerToken | undefined): string | undefined => { - if (token == null) { - return undefined; - } - return `Bearer ${token}`; - }, +function toAuthorizationHeader(token: string | undefined): string | undefined { + if (token == null) { + return undefined; + } + return `Bearer ${token}`; +} + +export const BearerToken: { + toAuthorizationHeader: typeof toAuthorizationHeader; + fromAuthorizationHeader: (header: string) => BearerToken; +} = { + toAuthorizationHeader: toAuthorizationHeader, fromAuthorizationHeader: (header: string): BearerToken => { return header.replace(BEARER_AUTH_HEADER_PREFIX, "").trim() as BearerToken; }, diff --git a/src/core/auth/NoOpAuthProvider.ts b/src/core/auth/NoOpAuthProvider.ts new file mode 100644 index 0000000..5b7acfd --- /dev/null +++ b/src/core/auth/NoOpAuthProvider.ts @@ -0,0 +1,8 @@ +import type { AuthProvider } from "./AuthProvider.js"; +import type { AuthRequest } from "./AuthRequest.js"; + +export class NoOpAuthProvider implements AuthProvider { + public getAuthRequest(): Promise { + return Promise.resolve({ headers: {} }); + } +} diff --git a/src/core/auth/OAuthTokenProvider.ts b/src/core/auth/OAuthTokenProvider.ts deleted file mode 100644 index 95df826..0000000 --- a/src/core/auth/OAuthTokenProvider.ts +++ /dev/null @@ -1,56 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type { OauthTokens } from "../../api/resources/oauthTokens/client/Client.js"; - -import * as core from "../../core/index.js"; - -/** - * The OAuthTokenProvider retrieves an OAuth access token, refreshing it as needed. - * The access token is then used as the bearer token in every authenticated request. - */ -export class OAuthTokenProvider { - private readonly BUFFER_IN_MINUTES = 2; - private readonly _clientId: core.Supplier; - private readonly _clientSecret: core.Supplier; - private readonly _authClient: OauthTokens; - private _accessToken: string | undefined; - private _expiresAt: Date; - - constructor({ - clientId, - clientSecret, - authClient, - }: { - clientId: core.Supplier; - clientSecret: core.Supplier; - authClient: OauthTokens; - }) { - this._clientId = clientId; - this._clientSecret = clientSecret; - this._authClient = authClient; - this._expiresAt = new Date(); - } - - public async getToken(): Promise { - if (this._accessToken && this._expiresAt > new Date()) { - return this._accessToken; - } - return this.refresh(); - } - - private async refresh(): Promise { - const tokenResponse = await this._authClient.create({ - clientId: await core.Supplier.get(this._clientId), - clientSecret: await core.Supplier.get(this._clientSecret), - }); - - this._accessToken = tokenResponse.accessToken; - this._expiresAt = this.getExpiresAt(tokenResponse.expiresIn, this.BUFFER_IN_MINUTES); - return this._accessToken; - } - - private getExpiresAt(expiresInSeconds: number, bufferInMinutes: number): Date { - const now = new Date(); - return new Date(now.getTime() + expiresInSeconds * 1000 - bufferInMinutes * 60 * 1000); - } -} diff --git a/src/core/exports.ts b/src/core/exports.ts new file mode 100644 index 0000000..d277449 --- /dev/null +++ b/src/core/exports.ts @@ -0,0 +1,2 @@ +export * from "./logging/exports.js"; +export * from "./pagination/exports.js"; diff --git a/src/core/fetcher/BinaryResponse.ts b/src/core/fetcher/BinaryResponse.ts index 4b4d0e8..bca7f4c 100644 --- a/src/core/fetcher/BinaryResponse.ts +++ b/src/core/fetcher/BinaryResponse.ts @@ -1,25 +1,23 @@ -import type { ResponseWithBody } from "./ResponseWithBody.js"; - export type BinaryResponse = { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ - bodyUsed: boolean; + bodyUsed: Response["bodyUsed"]; /** * Returns a ReadableStream of the response body. * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ - stream: () => ReadableStream; + stream: () => Response["body"]; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ - arrayBuffer: () => Promise; + arrayBuffer: () => ReturnType; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ - blob: () => Promise; + blob: () => ReturnType; /** * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) * Some versions of the Fetch API may not support this method. */ - bytes?(): Promise; + bytes?(): ReturnType; }; -export function getBinaryResponse(response: ResponseWithBody): BinaryResponse { +export function getBinaryResponse(response: Response): BinaryResponse { const binaryResponse: BinaryResponse = { get bodyUsed() { return response.bodyUsed; diff --git a/src/core/fetcher/Fetcher.ts b/src/core/fetcher/Fetcher.ts index 202e134..58bb0e3 100644 --- a/src/core/fetcher/Fetcher.ts +++ b/src/core/fetcher/Fetcher.ts @@ -1,4 +1,5 @@ import { toJson } from "../json.js"; +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; import type { APIResponse } from "./APIResponse.js"; import { createRequestUrl } from "./createRequestUrl.js"; import type { EndpointMetadata } from "./EndpointMetadata.js"; @@ -7,6 +8,7 @@ import { getErrorResponseBody } from "./getErrorResponseBody.js"; import { getFetchFn } from "./getFetchFn.js"; import { getRequestBody } from "./getRequestBody.js"; import { getResponseBody } from "./getResponseBody.js"; +import { Headers } from "./Headers.js"; import { makeRequest } from "./makeRequest.js"; import { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; import { requestWithRetries } from "./requestWithRetries.js"; @@ -25,13 +27,15 @@ export declare namespace Fetcher { maxRetries?: number; withCredentials?: boolean; abortSignal?: AbortSignal; - requestType?: "json" | "file" | "bytes"; + requestType?: "json" | "file" | "bytes" | "form" | "other"; 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; + export type Error = FailedStatusCodeError | NonJsonError | BodyIsNullError | TimeoutError | UnknownError; export interface FailedStatusCodeError { reason: "status-code"; @@ -45,6 +49,11 @@ export declare namespace Fetcher { rawBody: string; } + export interface BodyIsNullError { + reason: "body-is-null"; + statusCode: number; + } + export interface TimeoutError { reason: "timeout"; } @@ -55,10 +64,164 @@ export declare namespace Fetcher { } } -async function getHeaders(args: Fetcher.Args): Promise> { - const newHeaders: Record = {}; +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: Headers | Record): Record { + const filtered: Record = {}; + for (const [key, value] of headers instanceof Headers ? headers.entries() : 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: Headers = new Headers(); + + newHeaders.set( + "Accept", + args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + ); if (args.body !== undefined && args.contentType != null) { - newHeaders["Content-Type"] = args.contentType; + newHeaders.set("Content-Type", args.contentType); } if (args.headers == null) { @@ -68,13 +231,13 @@ async function getHeaders(args: Fetcher.Args): Promise> { for (const [key, value] of Object.entries(args.headers)) { const result = await EndpointSupplier.get(value, { endpointMetadata: args.endpointMetadata ?? {} }); if (typeof result === "string") { - newHeaders[key] = result; + newHeaders.set(key, result); continue; } if (result == null) { continue; } - newHeaders[key] = `${result}`; + newHeaders.set(key, `${result}`); } return newHeaders; } @@ -83,9 +246,22 @@ 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(response.headers), + }; + logger.debug("HTTP request succeeded", metadata); + } + const body = await getResponseBody(response, args.responseType); return { ok: true, - body: (await getResponseBody(response, args.responseType)) as R, + body: body as R, headers: response.headers, rawResponse: toRawResponse(response), }; } else { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())), + }; + logger.error("HTTP request failed with error status", metadata); + } return { ok: false, error: { @@ -124,6 +319,13 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise; -}; - -export function isResponseWithBody(response: Response): response is ResponseWithBody { - return (response as ResponseWithBody).body != null; -} diff --git a/src/core/fetcher/getRequestBody.ts b/src/core/fetcher/getRequestBody.ts index e38457c..91d9d81 100644 --- a/src/core/fetcher/getRequestBody.ts +++ b/src/core/fetcher/getRequestBody.ts @@ -1,13 +1,17 @@ import { toJson } from "../json.js"; +import { toQueryString } from "../url/qs.js"; export declare namespace GetRequestBody { interface Args { body: unknown; - type: "json" | "file" | "bytes" | "other"; + type: "json" | "file" | "bytes" | "form" | "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..708d557 100644 --- a/src/core/fetcher/getResponseBody.ts +++ b/src/core/fetcher/getResponseBody.ts @@ -1,11 +1,7 @@ import { fromJson } from "../json.js"; import { getBinaryResponse } from "./BinaryResponse.js"; -import { isResponseWithBody } from "./ResponseWithBody.js"; export async function getResponseBody(response: Response, responseType?: string): Promise { - if (!isResponseWithBody(response)) { - return undefined; - } switch (responseType) { case "binary-response": return getBinaryResponse(response); @@ -14,8 +10,27 @@ export async function getResponseBody(response: Response, responseType?: string) case "arrayBuffer": return await response.arrayBuffer(); case "sse": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } return response.body; case "streaming": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + return response.body; case "text": diff --git a/src/core/fetcher/makeRequest.ts b/src/core/fetcher/makeRequest.ts index 5edce69..921565e 100644 --- a/src/core/fetcher/makeRequest.ts +++ b/src/core/fetcher/makeRequest.ts @@ -4,7 +4,7 @@ export const makeRequest = async ( fetchFn: (url: string, init: RequestInit) => Promise, url: string, method: string, - headers: Record, + headers: Headers | Record, requestBody: BodyInit | undefined, timeoutMs?: number, abortSignal?: AbortSignal, @@ -13,15 +13,13 @@ export const makeRequest = async ( ): Promise => { const signals: AbortSignal[] = []; - // Add timeout signal - let timeoutAbortId: NodeJS.Timeout | undefined; + let timeoutAbortId: ReturnType | undefined; if (timeoutMs != null) { const { signal, abortId } = getTimeoutSignal(timeoutMs); timeoutAbortId = abortId; signals.push(signal); } - // Add arbitrary signal if (abortSignal != null) { signals.push(abortSignal); } diff --git a/src/core/fetcher/requestWithRetries.ts b/src/core/fetcher/requestWithRetries.ts index 3d30bd1..1f68968 100644 --- a/src/core/fetcher/requestWithRetries.ts +++ b/src/core/fetcher/requestWithRetries.ts @@ -4,28 +4,23 @@ 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) { 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())) { const delay = retryAfterDate.getTime() - Date.now(); @@ -35,12 +30,10 @@ 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)) { - // Assume Unix timestamp in epoch seconds const delay = resetTime * 1000 - Date.now(); if (delay > 0) { return addPositiveJitter(Math.min(delay, MAX_RETRY_DELAY)); @@ -48,7 +41,6 @@ function getRetryDelayFromHeaders(response: Response, retryAttempt: number): num } } - // Fall back to exponential backoff, with symmetric jitter return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** retryAttempt, MAX_RETRY_DELAY)); } @@ -60,7 +52,6 @@ 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 a8d32a2..7bd3757 100644 --- a/src/core/fetcher/signals.ts +++ b/src/core/fetcher/signals.ts @@ -1,34 +1,22 @@ const TIMEOUT = "timeout"; -export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: NodeJS.Timeout } { +export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: ReturnType } { const controller = new AbortController(); const abortId = setTimeout(() => controller.abort(TIMEOUT), timeoutMs); 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 a723d22..78ed8b5 100644 --- a/src/core/headers.ts +++ b/src/core/headers.ts @@ -6,10 +6,11 @@ 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[key] = value; - } else if (key in result) { - delete result[key]; + result[insensitiveKey] = value; + } else if (insensitiveKey in result) { + delete result[insensitiveKey]; } } @@ -24,8 +25,9 @@ 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[key] = value; + result[insensitiveKey] = value; } } diff --git a/src/core/index.ts b/src/core/index.ts index 21e2365..70de501 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,6 +1,7 @@ export * from "./auth/index.js"; export * from "./base64.js"; export * from "./fetcher/index.js"; +export * as logging from "./logging/index.js"; export * from "./pagination/index.js"; export * from "./runtime/index.js"; export * as serialization from "./schemas/index.js"; diff --git a/src/core/logging/exports.ts b/src/core/logging/exports.ts new file mode 100644 index 0000000..88f6c00 --- /dev/null +++ b/src/core/logging/exports.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..d81cc32 --- /dev/null +++ b/src/core/logging/index.ts @@ -0,0 +1 @@ +export * from "./logger.js"; diff --git a/src/core/logging/logger.ts b/src/core/logging/logger.ts new file mode 100644 index 0000000..a3f3673 --- /dev/null +++ b/src/core/logging/logger.ts @@ -0,0 +1,203 @@ +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/pagination/Page.ts b/src/core/pagination/Page.ts index 1aa08e5..6621a6f 100644 --- a/src/core/pagination/Page.ts +++ b/src/core/pagination/Page.ts @@ -4,15 +4,16 @@ import type { HttpResponsePromise, RawResponse } from "../fetcher/index.js"; * A page of results from a paginated API. * * @template T The type of the items in the page. + * @template R The type of the API response. */ -export class Page implements AsyncIterable { +export class Page implements AsyncIterable { public data: T[]; public rawResponse: RawResponse; + public response: R; - private response: unknown; - private _hasNextPage: (response: unknown) => boolean; - private getItems: (response: unknown) => T[]; - private loadNextPage: (response: unknown) => HttpResponsePromise; + private _hasNextPage: (response: R) => boolean; + private getItems: (response: R) => T[]; + private loadNextPage: (response: R) => HttpResponsePromise; constructor({ response, @@ -21,11 +22,11 @@ export class Page implements AsyncIterable { getItems, loadPage, }: { - response: unknown; + response: R; rawResponse: RawResponse; - hasNextPage: (response: unknown) => boolean; - getItems: (response: unknown) => T[]; - loadPage: (response: unknown) => HttpResponsePromise; + hasNextPage: (response: R) => boolean; + getItems: (response: R) => T[]; + loadPage: (response: R) => HttpResponsePromise; }) { this.response = response; this.rawResponse = rawResponse; diff --git a/src/core/pagination/Pageable.ts b/src/core/pagination/Pageable.ts deleted file mode 100644 index 5689e1e..0000000 --- a/src/core/pagination/Pageable.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RawResponse } from "../fetcher/index.js"; -import { Page } from "./Page.js"; - -export declare namespace Pageable { - interface Args { - response: Response; - rawResponse: RawResponse; - hasNextPage: (response: Response) => boolean; - getItems: (response: Response) => Item[]; - loadPage: (response: Response) => Promise; - } -} - -export class Pageable extends Page { - constructor(args: Pageable.Args) { - super(args as any); - } -} diff --git a/src/core/pagination/exports.ts b/src/core/pagination/exports.ts new file mode 100644 index 0000000..d3acc60 --- /dev/null +++ b/src/core/pagination/exports.ts @@ -0,0 +1 @@ +export type { Page } from "./Page.js"; diff --git a/src/core/pagination/index.ts b/src/core/pagination/index.ts index b0cd68f..7781cbd 100644 --- a/src/core/pagination/index.ts +++ b/src/core/pagination/index.ts @@ -1,2 +1 @@ export { Page } from "./Page.js"; -export { Pageable } from "./Pageable.js"; diff --git a/src/core/runtime/runtime.ts b/src/core/runtime/runtime.ts index 08fd256..56ebbb8 100644 --- a/src/core/runtime/runtime.ts +++ b/src/core/runtime/runtime.ts @@ -99,6 +99,18 @@ function evaluateRuntime(): Runtime { }; } + /** + * A constant that indicates whether the environment the code is running is in React-Native. + * This check should come before Node.js detection since React Native may have a process polyfill. + * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js + */ + const isReactNative = typeof navigator !== "undefined" && navigator?.product === "ReactNative"; + if (isReactNative) { + return { + type: "react-native", + }; + } + /** * A constant that indicates whether the environment the code is running is Node.JS. */ @@ -116,17 +128,6 @@ function evaluateRuntime(): Runtime { }; } - /** - * A constant that indicates whether the environment the code is running is in React-Native. - * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js - */ - const isReactNative = typeof navigator !== "undefined" && navigator?.product === "ReactNative"; - if (isReactNative) { - return { - type: "react-native", - }; - } - return { type: "unknown", }; diff --git a/src/core/schemas/Schema.ts b/src/core/schemas/Schema.ts index 179edf6..4cd8b1d 100644 --- a/src/core/schemas/Schema.ts +++ b/src/core/schemas/Schema.ts @@ -24,6 +24,7 @@ export const SchemaType = { NUMBER: "number", STRING: "string", UNKNOWN: "unknown", + NEVER: "never", RECORD: "record", SET: "set", UNION: "union", @@ -32,6 +33,7 @@ export const SchemaType = { OPTIONAL: "optional", OPTIONAL_NULLABLE: "optionalNullable", } as const; + export type SchemaType = (typeof SchemaType)[keyof typeof SchemaType]; export type MaybeValid = Valid | Invalid; diff --git a/src/core/schemas/builders/object/object.ts b/src/core/schemas/builders/object/object.ts index ca6d227..624d4d3 100644 --- a/src/core/schemas/builders/object/object.ts +++ b/src/core/schemas/builders/object/object.ts @@ -244,17 +244,19 @@ export function getObjectUtils(schema: BaseObjectSchema { return validateAndTransformExtendedObject({ extensionKeys: extension._getRawProperties(), - value: raw as object, + value: raw, transformBase: (rawBase) => schema.parse(rawBase, opts), transformExtension: (rawExtension) => extension.parse(rawExtension, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, }); }, json: (parsed, opts) => { return validateAndTransformExtendedObject({ extensionKeys: extension._getParsedProperties(), - value: parsed as object, + value: parsed, transformBase: (parsedBase) => schema.json(parsedBase, opts), transformExtension: (parsedExtension) => extension.json(parsedExtension, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, }); }, getType: () => SchemaType.OBJECT, @@ -316,12 +318,26 @@ function validateAndTransformExtendedObject MaybeValid; transformExtension: (value: object) => MaybeValid; + breadcrumbsPrefix?: string[]; }): MaybeValid { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + const extensionPropertiesSet = new Set(extensionKeys); const [extensionProperties, baseProperties] = partition(keys(value), (key) => extensionPropertiesSet.has(key as keyof PreTransformedExtension), diff --git a/src/core/schemas/builders/primitives/index.ts b/src/core/schemas/builders/primitives/index.ts index 462a6d1..7a3ee01 100644 --- a/src/core/schemas/builders/primitives/index.ts +++ b/src/core/schemas/builders/primitives/index.ts @@ -1,5 +1,6 @@ export { any } from "./any.js"; export { boolean } from "./boolean.js"; +export { never } from "./never.js"; export { number } from "./number.js"; export { string } from "./string.js"; export { unknown } from "./unknown.js"; diff --git a/src/core/schemas/builders/primitives/never.ts b/src/core/schemas/builders/primitives/never.ts new file mode 100644 index 0000000..91f85d7 --- /dev/null +++ b/src/core/schemas/builders/primitives/never.ts @@ -0,0 +1,15 @@ +import { type Schema, SchemaType } from "../../Schema.js"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator.js"; + +export const never: () => Schema = createIdentitySchemaCreator( + SchemaType.NEVER, + (_value, { breadcrumbsPrefix = [] } = {}) => ({ + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: "Expected never", + }, + ], + }), +); diff --git a/src/core/url/join.ts b/src/core/url/join.ts index b872a90..7ca7dae 100644 --- a/src/core/url/join.ts +++ b/src/core/url/join.ts @@ -12,7 +12,6 @@ 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); } diff --git a/src/errors/PipedreamError.ts b/src/errors/PipedreamError.ts index 9d3f37f..a54a28a 100644 --- a/src/errors/PipedreamError.ts +++ b/src/errors/PipedreamError.ts @@ -20,7 +20,12 @@ export class PipedreamError extends Error { rawResponse?: core.RawResponse; }) { super(buildMessage({ message, statusCode, body })); - Object.setPrototypeOf(this, PipedreamError.prototype); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = this.constructor.name; this.statusCode = statusCode; this.body = body; this.rawResponse = rawResponse; diff --git a/src/errors/PipedreamTimeoutError.ts b/src/errors/PipedreamTimeoutError.ts index cf0d65a..282cb99 100644 --- a/src/errors/PipedreamTimeoutError.ts +++ b/src/errors/PipedreamTimeoutError.ts @@ -3,6 +3,11 @@ export class PipedreamTimeoutError extends Error { constructor(message: string) { super(message); - Object.setPrototypeOf(this, PipedreamTimeoutError.prototype); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = this.constructor.name; } } diff --git a/src/errors/handleNonStatusCodeError.ts b/src/errors/handleNonStatusCodeError.ts new file mode 100644 index 0000000..b1585da --- /dev/null +++ b/src/errors/handleNonStatusCodeError.ts @@ -0,0 +1,37 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import * as errors from "./index.js"; + +export function handleNonStatusCodeError( + error: core.Fetcher.Error, + rawResponse: core.RawResponse, + method: string, + path: string, +): never { + switch (error.reason) { + case "non-json": + throw new errors.PipedreamError({ + statusCode: error.statusCode, + body: error.rawBody, + rawResponse: rawResponse, + }); + case "body-is-null": + throw new errors.PipedreamError({ + statusCode: error.statusCode, + rawResponse: rawResponse, + }); + case "timeout": + throw new errors.PipedreamTimeoutError(`Timeout exceeded when calling ${method} ${path}.`); + case "unknown": + throw new errors.PipedreamError({ + message: error.errorMessage, + rawResponse: rawResponse, + }); + default: + throw new errors.PipedreamError({ + message: "Unknown error", + rawResponse: rawResponse, + }); + } +} diff --git a/src/exports.ts b/src/exports.ts new file mode 100644 index 0000000..7b70ee1 --- /dev/null +++ b/src/exports.ts @@ -0,0 +1 @@ +export * from "./core/exports.js"; diff --git a/src/serialization/resources/index.ts b/src/serialization/resources/index.ts index a9046bd..7de83b8 100644 --- a/src/serialization/resources/index.ts +++ b/src/serialization/resources/index.ts @@ -11,6 +11,8 @@ export * from "./deployedTriggers/client/requests/index.js"; export * as deployedTriggers from "./deployedTriggers/index.js"; export * from "./oauthTokens/client/requests/index.js"; export * as oauthTokens from "./oauthTokens/index.js"; +export * from "./projects/client/requests/index.js"; +export * as projects from "./projects/index.js"; export * as proxy from "./proxy/index.js"; export * from "./tokens/client/requests/index.js"; export * as tokens from "./tokens/index.js"; diff --git a/src/serialization/resources/projects/client/index.ts b/src/serialization/resources/projects/client/index.ts new file mode 100644 index 0000000..195f9aa --- /dev/null +++ b/src/serialization/resources/projects/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/src/serialization/resources/projects/client/requests/CreateProjectOpts.ts b/src/serialization/resources/projects/client/requests/CreateProjectOpts.ts new file mode 100644 index 0000000..82d09e8 --- /dev/null +++ b/src/serialization/resources/projects/client/requests/CreateProjectOpts.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; + +export const CreateProjectOpts: core.serialization.Schema< + serializers.CreateProjectOpts.Raw, + Pipedream.CreateProjectOpts +> = core.serialization.object({ + name: core.serialization.string(), + appName: core.serialization.property("app_name", core.serialization.string().optional()), + supportEmail: core.serialization.property("support_email", core.serialization.string().optional()), +}); + +export declare namespace CreateProjectOpts { + export interface Raw { + name: string; + app_name?: string | null; + support_email?: string | null; + } +} diff --git a/src/serialization/resources/projects/client/requests/UpdateProjectLogoOpts.ts b/src/serialization/resources/projects/client/requests/UpdateProjectLogoOpts.ts new file mode 100644 index 0000000..afdc37c --- /dev/null +++ b/src/serialization/resources/projects/client/requests/UpdateProjectLogoOpts.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; + +export const UpdateProjectLogoOpts: core.serialization.Schema< + serializers.UpdateProjectLogoOpts.Raw, + Pipedream.UpdateProjectLogoOpts +> = core.serialization.object({ + logo: core.serialization.string(), +}); + +export declare namespace UpdateProjectLogoOpts { + export interface Raw { + logo: string; + } +} diff --git a/src/serialization/resources/projects/client/requests/UpdateProjectOpts.ts b/src/serialization/resources/projects/client/requests/UpdateProjectOpts.ts new file mode 100644 index 0000000..5f3060d --- /dev/null +++ b/src/serialization/resources/projects/client/requests/UpdateProjectOpts.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; + +export const UpdateProjectOpts: core.serialization.Schema< + serializers.UpdateProjectOpts.Raw, + Pipedream.UpdateProjectOpts +> = core.serialization.object({ + name: core.serialization.string().optional(), + appName: core.serialization.property("app_name", core.serialization.string().optional()), + supportEmail: core.serialization.property("support_email", core.serialization.string().optional()), +}); + +export declare namespace UpdateProjectOpts { + export interface Raw { + name?: string | null; + app_name?: string | null; + support_email?: string | null; + } +} diff --git a/src/serialization/resources/projects/client/requests/index.ts b/src/serialization/resources/projects/client/requests/index.ts new file mode 100644 index 0000000..26cf1f9 --- /dev/null +++ b/src/serialization/resources/projects/client/requests/index.ts @@ -0,0 +1,3 @@ +export { CreateProjectOpts } from "./CreateProjectOpts.js"; +export { UpdateProjectLogoOpts } from "./UpdateProjectLogoOpts.js"; +export { UpdateProjectOpts } from "./UpdateProjectOpts.js"; diff --git a/src/serialization/resources/projects/index.ts b/src/serialization/resources/projects/index.ts new file mode 100644 index 0000000..914b8c3 --- /dev/null +++ b/src/serialization/resources/projects/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/src/serialization/types/ListProjectsResponse.ts b/src/serialization/types/ListProjectsResponse.ts new file mode 100644 index 0000000..97af4a0 --- /dev/null +++ b/src/serialization/types/ListProjectsResponse.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { PageInfo } from "./PageInfo.js"; +import { Project } from "./Project.js"; + +export const ListProjectsResponse: core.serialization.ObjectSchema< + serializers.ListProjectsResponse.Raw, + Pipedream.ListProjectsResponse +> = core.serialization.object({ + data: core.serialization.list(Project), + pageInfo: core.serialization.property("page_info", PageInfo), +}); + +export declare namespace ListProjectsResponse { + export interface Raw { + data: Project.Raw[]; + page_info: PageInfo.Raw; + } +} diff --git a/src/serialization/types/Project.ts b/src/serialization/types/Project.ts new file mode 100644 index 0000000..f8c83fe --- /dev/null +++ b/src/serialization/types/Project.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const Project: core.serialization.ObjectSchema = + core.serialization.object({ + id: core.serialization.string(), + name: core.serialization.string(), + appName: core.serialization.property("app_name", core.serialization.string().optional()), + supportEmail: core.serialization.property("support_email", core.serialization.string().optional()), + }); + +export declare namespace Project { + export interface Raw { + id: string; + name: string; + app_name?: string | null; + support_email?: string | null; + } +} diff --git a/src/serialization/types/ProxyResponseBinary.ts b/src/serialization/types/ProxyResponseBinary.ts new file mode 100644 index 0000000..8a05a96 --- /dev/null +++ b/src/serialization/types/ProxyResponseBinary.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as Pipedream from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const ProxyResponseBinary: core.serialization.Schema< + serializers.ProxyResponseBinary.Raw, + Pipedream.ProxyResponseBinary +> = core.serialization.string(); + +export declare namespace ProxyResponseBinary { + export type Raw = string; +} diff --git a/src/serialization/types/index.ts b/src/serialization/types/index.ts index 4ebd911..6185aba 100644 --- a/src/serialization/types/index.ts +++ b/src/serialization/types/index.ts @@ -84,9 +84,11 @@ export * from "./HttpInterface.js"; export * from "./ListAccountsResponse.js"; export * from "./ListAppCategoriesResponse.js"; export * from "./ListAppsResponse.js"; +export * from "./ListProjectsResponse.js"; export * from "./Observation.js"; export * from "./ObservationError.js"; export * from "./PageInfo.js"; +export * from "./Project.js"; export * from "./ProjectEnvironment.js"; export * from "./ProjectInfoResponse.js"; export * from "./ProjectInfoResponseApp.js"; @@ -94,6 +96,7 @@ export * from "./PropOption.js"; export * from "./PropOptionNested.js"; export * from "./PropOptionValue.js"; export * from "./ProxyResponse.js"; +export * from "./ProxyResponseBinary.js"; export * from "./ReloadPropsOpts.js"; export * from "./ReloadPropsResponse.js"; export * from "./RunActionOptsStashId.js"; diff --git a/src/version.ts b/src/version.ts index 7812a79..3003e8d 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = "2.3.2"; +export const SDK_VERSION = "2.3.3"; diff --git a/tests/mock-server/MockServer.ts b/tests/mock-server/MockServer.ts index 5b30fe7..9548721 100644 --- a/tests/mock-server/MockServer.ts +++ b/tests/mock-server/MockServer.ts @@ -19,7 +19,7 @@ export class MockServer { public mockEndpoint(options?: RequestHandlerOptions): ReturnType { const builder = mockEndpointBuilder({ - once: options?.once, + once: options?.once ?? true, onBuild: (handler) => { this.server.use(handler); }, diff --git a/tests/mock-server/mockEndpointBuilder.ts b/tests/mock-server/mockEndpointBuilder.ts index 18557ec..1b0e510 100644 --- a/tests/mock-server/mockEndpointBuilder.ts +++ b/tests/mock-server/mockEndpointBuilder.ts @@ -2,6 +2,7 @@ import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponse import { url } from "../../src/core"; import { toJson } from "../../src/core/json"; +import { withFormUrlEncoded } from "./withFormUrlEncoded"; import { withHeaders } from "./withHeaders"; import { withJson } from "./withJson"; @@ -26,6 +27,7 @@ interface RequestHeadersStage extends RequestBodyStage, ResponseStage { interface RequestBodyStage extends ResponseStage { jsonBody(body: unknown): ResponseStage; + formUrlEncodedBody(body: unknown): ResponseStage; } interface ResponseStage { @@ -135,6 +137,16 @@ 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 new file mode 100644 index 0000000..e9e6ff2 --- /dev/null +++ b/tests/mock-server/withFormUrlEncoded.ts @@ -0,0 +1,80 @@ +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/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..a5651f8 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,80 @@ +import { expect } from "vitest"; + +interface CustomMatchers { + toContainHeaders(expectedHeaders: Record): R; +} + +declare module "vitest" { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + +expect.extend({ + toContainHeaders(actual: unknown, expectedHeaders: Record) { + const isHeaders = actual instanceof Headers; + const isPlainObject = typeof actual === "object" && actual !== null && !Array.isArray(actual); + + if (!isHeaders && !isPlainObject) { + throw new TypeError("Received value must be an instance of Headers or a plain object!"); + } + + if (typeof expectedHeaders !== "object" || expectedHeaders === null || Array.isArray(expectedHeaders)) { + throw new TypeError("Expected headers must be a plain object!"); + } + + const missingHeaders: string[] = []; + const mismatchedHeaders: Array<{ key: string; expected: string; actual: string | null }> = []; + + for (const [key, value] of Object.entries(expectedHeaders)) { + let actualValue: string | null = null; + + if (isHeaders) { + // Headers.get() is already case-insensitive + actualValue = (actual as Headers).get(key); + } else { + // For plain objects, do case-insensitive lookup + const actualObj = actual as Record; + const lowerKey = key.toLowerCase(); + const foundKey = Object.keys(actualObj).find((k) => k.toLowerCase() === lowerKey); + actualValue = foundKey ? actualObj[foundKey] : null; + } + + if (actualValue === null || actualValue === undefined) { + missingHeaders.push(key); + } else if (actualValue !== value) { + mismatchedHeaders.push({ key, expected: value, actual: actualValue }); + } + } + + const pass = missingHeaders.length === 0 && mismatchedHeaders.length === 0; + + const actualType = isHeaders ? "Headers" : "object"; + + if (pass) { + return { + message: () => `expected ${actualType} not to contain ${this.utils.printExpected(expectedHeaders)}`, + pass: true, + }; + } else { + const messages: string[] = []; + + if (missingHeaders.length > 0) { + messages.push(`Missing headers: ${this.utils.printExpected(missingHeaders.join(", "))}`); + } + + if (mismatchedHeaders.length > 0) { + const mismatches = mismatchedHeaders.map( + ({ key, expected, actual }) => + `${key}: expected ${this.utils.printExpected(expected)} but got ${this.utils.printReceived(actual)}`, + ); + messages.push(mismatches.join("\n")); + } + + return { + message: () => + `expected ${actualType} to contain ${this.utils.printExpected(expectedHeaders)}\n\n${messages.join("\n")}`, + pass: false, + }; + } + }, +}); diff --git a/tests/unit/auth/BasicAuth.test.ts b/tests/unit/auth/BasicAuth.test.ts index 90bbc7e..9b51233 100644 --- a/tests/unit/auth/BasicAuth.test.ts +++ b/tests/unit/auth/BasicAuth.test.ts @@ -1,58 +1,92 @@ import { BasicAuth } from "../../../src/core/auth/BasicAuth"; describe("BasicAuth", () => { - describe("toAuthorizationHeader", () => { - it("correctly converts to header", () => { - expect( - BasicAuth.toAuthorizationHeader({ - username: "username", - password: "password", - }), - ).toBe("Basic dXNlcm5hbWU6cGFzc3dvcmQ="); - }); - }); - describe("fromAuthorizationHeader", () => { - it("correctly parses header", () => { - expect(BasicAuth.fromAuthorizationHeader("Basic dXNlcm5hbWU6cGFzc3dvcmQ=")).toEqual({ - username: "username", - password: "password", - }); - }); + interface ToHeaderTestCase { + description: string; + input: { username: string; password: string }; + expected: string; + } - it("handles password with colons", () => { - expect(BasicAuth.fromAuthorizationHeader("Basic dXNlcjpwYXNzOndvcmQ=")).toEqual({ - username: "user", - password: "pass:word", - }); - }); + interface FromHeaderTestCase { + description: string; + input: string; + expected: { username: string; password: string }; + } - it("handles empty username and password (just colon)", () => { - expect(BasicAuth.fromAuthorizationHeader("Basic Og==")).toEqual({ - username: "", - password: "", - }); - }); + interface ErrorTestCase { + description: string; + input: string; + expectedError: string; + } + + describe("toAuthorizationHeader", () => { + const toHeaderTests: ToHeaderTestCase[] = [ + { + description: "correctly converts to header", + input: { username: "username", password: "password" }, + expected: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + }, + ]; - it("handles empty username", () => { - expect(BasicAuth.fromAuthorizationHeader("Basic OnBhc3N3b3Jk")).toEqual({ - username: "", - password: "password", + toHeaderTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(BasicAuth.toAuthorizationHeader(input)).toBe(expected); }); }); + }); - it("handles empty password", () => { - expect(BasicAuth.fromAuthorizationHeader("Basic dXNlcm5hbWU6")).toEqual({ - username: "username", - password: "", + describe("fromAuthorizationHeader", () => { + const fromHeaderTests: FromHeaderTestCase[] = [ + { + description: "correctly parses header", + input: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + expected: { username: "username", password: "password" }, + }, + { + description: "handles password with colons", + input: "Basic dXNlcjpwYXNzOndvcmQ=", + expected: { username: "user", password: "pass:word" }, + }, + { + description: "handles empty username and password (just colon)", + input: "Basic Og==", + expected: { username: "", password: "" }, + }, + { + description: "handles empty username", + input: "Basic OnBhc3N3b3Jk", + expected: { username: "", password: "password" }, + }, + { + description: "handles empty password", + input: "Basic dXNlcm5hbWU6", + expected: { username: "username", password: "" }, + }, + ]; + + fromHeaderTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(BasicAuth.fromAuthorizationHeader(input)).toEqual(expected); }); }); - it("throws error for completely empty credentials", () => { - expect(() => BasicAuth.fromAuthorizationHeader("Basic ")).toThrow("Invalid basic auth"); - }); + const errorTests: ErrorTestCase[] = [ + { + description: "throws error for completely empty credentials", + input: "Basic ", + expectedError: "Invalid basic auth", + }, + { + description: "throws error for credentials without colon", + input: "Basic dXNlcm5hbWU=", + expectedError: "Invalid basic auth", + }, + ]; - it("throws error for credentials without colon", () => { - expect(() => BasicAuth.fromAuthorizationHeader("Basic dXNlcm5hbWU=")).toThrow("Invalid basic auth"); + errorTests.forEach(({ description, input, expectedError }) => { + it(description, () => { + expect(() => BasicAuth.fromAuthorizationHeader(input)).toThrow(expectedError); + }); }); }); }); diff --git a/tests/unit/fetcher/Fetcher.test.ts b/tests/unit/fetcher/Fetcher.test.ts index bfc6429..2f98ede 100644 --- a/tests/unit/fetcher/Fetcher.test.ts +++ b/tests/unit/fetcher/Fetcher.test.ts @@ -13,6 +13,7 @@ describe("Test fetcherImpl", () => { body: { data: "test" }, contentType: "application/json", requestType: "json", + maxRetries: 0, responseType: "json", }; @@ -33,7 +34,7 @@ describe("Test fetcherImpl", () => { "https://httpbin.org/post", expect.objectContaining({ method: "POST", - headers: expect.objectContaining({ "X-Test": "x-test-header" }), + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), body: JSON.stringify({ data: "test" }), }), ); @@ -47,6 +48,7 @@ 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")), }; @@ -64,7 +66,7 @@ describe("Test fetcherImpl", () => { url, expect.objectContaining({ method: "POST", - headers: expect.objectContaining({ "X-Test": "x-test-header" }), + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), body: expect.any(fs.ReadStream), }), ); @@ -80,6 +82,7 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, + maxRetries: 0, responseType: "binary-response", }; @@ -99,7 +102,7 @@ describe("Test fetcherImpl", () => { url, expect.objectContaining({ method: "GET", - headers: expect.objectContaining({ "X-Test": "x-test-header" }), + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), }), ); expect(result.ok).toBe(true); @@ -110,7 +113,8 @@ describe("Test fetcherImpl", () => { expect(typeof body.stream).toBe("function"); const stream = body.stream(); expect(stream).toBeInstanceOf(ReadableStream); - const reader = stream.getReader(); + const readableStream = stream as ReadableStream; + const reader = readableStream.getReader(); const { value } = await reader.read(); const decoder = new TextDecoder(); const streamContent = decoder.decode(value); @@ -125,6 +129,7 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, + maxRetries: 0, responseType: "binary-response", }; @@ -144,7 +149,7 @@ describe("Test fetcherImpl", () => { url, expect.objectContaining({ method: "GET", - headers: expect.objectContaining({ "X-Test": "x-test-header" }), + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), }), ); expect(result.ok).toBe(true); @@ -170,6 +175,7 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, + maxRetries: 0, responseType: "binary-response", }; @@ -189,7 +195,7 @@ describe("Test fetcherImpl", () => { url, expect.objectContaining({ method: "GET", - headers: expect.objectContaining({ "X-Test": "x-test-header" }), + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), }), ); expect(result.ok).toBe(true); @@ -213,6 +219,7 @@ describe("Test fetcherImpl", () => { url, method: "GET", headers: { "X-Test": "x-test-header" }, + maxRetries: 0, responseType: "binary-response", }; @@ -232,7 +239,7 @@ describe("Test fetcherImpl", () => { url, expect.objectContaining({ method: "GET", - headers: expect.objectContaining({ "X-Test": "x-test-header" }), + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), }), ); expect(result.ok).toBe(true); diff --git a/tests/unit/fetcher/createRequestUrl.test.ts b/tests/unit/fetcher/createRequestUrl.test.ts index 06e03b2..a92f1b5 100644 --- a/tests/unit/fetcher/createRequestUrl.test.ts +++ b/tests/unit/fetcher/createRequestUrl.test.ts @@ -1,160 +1,163 @@ import { createRequestUrl } from "../../../src/core/fetcher/createRequestUrl"; describe("Test createRequestUrl", () => { - 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" }, + 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" }, + }, }, }, - }; - 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"], + 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"], + }, }, + sort: { field: "name", direction: "asc" }, }, - 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", - ); + 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); + }); }); }); diff --git a/tests/unit/fetcher/getRequestBody.test.ts b/tests/unit/fetcher/getRequestBody.test.ts index e864c8b..8a6c3a5 100644 --- a/tests/unit/fetcher/getRequestBody.test.ts +++ b/tests/unit/fetcher/getRequestBody.test.ts @@ -2,15 +2,117 @@ import { getRequestBody } from "../../../src/core/fetcher/getRequestBody"; import { RUNTIME } from "../../../src/core/runtime"; describe("Test getRequestBody", () => { - it("should stringify body if not FormData in Node environment", async () => { - if (RUNTIME.type === "node") { - const body = { key: "value" }; + 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; + } + const result = await getRequestBody({ - body, - type: "json", + body: input, + type, }); - expect(result).toBe('{"key":"value"}'); - } + + if (input instanceof Uint8Array) { + expect(result).toBe(input); + } else { + expect(result).toBe(expected); + } + }); }); it("should return FormData in browser environment", async () => { @@ -24,42 +126,4 @@ 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 151843a..ad6be7f 100644 --- a/tests/unit/fetcher/getResponseBody.test.ts +++ b/tests/unit/fetcher/getResponseBody.test.ts @@ -1,7 +1,61 @@ import { getResponseBody } from "../../../src/core/fetcher/getResponseBody"; + import { RUNTIME } from "../../../src/core/runtime"; 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); @@ -20,7 +74,6 @@ 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({ @@ -35,43 +88,10 @@ 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 new file mode 100644 index 0000000..366c9b6 --- /dev/null +++ b/tests/unit/fetcher/logging.test.ts @@ -0,0 +1,517 @@ +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.toContainHeaders({ + "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/makeRequest.test.ts b/tests/unit/fetcher/makeRequest.test.ts index f6203cd..ea49466 100644 --- a/tests/unit/fetcher/makeRequest.test.ts +++ b/tests/unit/fetcher/makeRequest.test.ts @@ -1,3 +1,4 @@ +import type { Mock } from "vitest"; import { makeRequest } from "../../../src/core/fetcher/makeRequest"; describe("Test makeRequest", () => { @@ -6,7 +7,7 @@ describe("Test makeRequest", () => { const mockHeaders = { "Content-Type": "application/json" }; const mockBody = JSON.stringify({ key: "value" }); - let mockFetch: import("vitest").Mock; + let mockFetch: Mock; beforeEach(() => { mockFetch = vi.fn(); diff --git a/tests/unit/fetcher/redacting.test.ts b/tests/unit/fetcher/redacting.test.ts new file mode 100644 index 0000000..d599376 --- /dev/null +++ b/tests/unit/fetcher/redacting.test.ts @@ -0,0 +1,1115 @@ +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.toContainHeaders({ + 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.toContainHeaders({ + "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.toContainHeaders({ + 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.toContainHeaders({ + "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.toContainHeaders({ + "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.toContainHeaders({ + "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.toContainHeaders({ + "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.toContainHeaders({ + "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.toContainHeaders({ + "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.toContainHeaders({ + 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.toContainHeaders({ + "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.toContainHeaders({ + 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.toContainHeaders({ + "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 7d46082..d226613 100644 --- a/tests/unit/fetcher/requestWithRetries.test.ts +++ b/tests/unit/fetcher/requestWithRetries.test.ts @@ -1,15 +1,15 @@ +import type { Mock, MockInstance } from "vitest"; import { requestWithRetries } from "../../../src/core/fetcher/requestWithRetries"; describe("requestWithRetries", () => { - let mockFetch: import("vitest").Mock; + let mockFetch: Mock; let originalMathRandom: typeof Math.random; - let setTimeoutSpy: import("vitest").MockInstance; + let setTimeoutSpy: MockInstance; beforeEach(() => { mockFetch = vi.fn(); originalMathRandom = Math.random; - // Mock Math.random for consistent jitter Math.random = vi.fn(() => 0.5); vi.useFakeTimers({ @@ -99,6 +99,67 @@ 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); @@ -113,7 +174,6 @@ describe("requestWithRetries", () => { await vi.runAllTimersAsync(); await responsePromise; - // Verify setTimeout calls expect(setTimeoutSpy).toHaveBeenCalledTimes(expectedDelays.length); expectedDelays.forEach((delay, index) => { @@ -145,85 +205,6 @@ 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); @@ -243,8 +224,7 @@ describe("requestWithRetries", () => { await vi.runAllTimersAsync(); const response = await responsePromise; - // 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(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60000); expect(response.status).toBe(200); }); }); diff --git a/tests/unit/logging/logger.test.ts b/tests/unit/logging/logger.test.ts new file mode 100644 index 0000000..2e0b5fe --- /dev/null +++ b/tests/unit/logging/logger.test.ts @@ -0,0 +1,454 @@ +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/schemas/primitives/never.test.ts b/tests/unit/schemas/primitives/never.test.ts new file mode 100644 index 0000000..1d18eba --- /dev/null +++ b/tests/unit/schemas/primitives/never.test.ts @@ -0,0 +1,54 @@ +import { never } from "../../../../src/core/schemas/builders"; + +describe("never", () => { + it("always fails to parse", () => { + const schema = never(); + const result = schema.parse("test"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toHaveLength(1); + expect(result.errors[0]?.message).toBe("Expected never"); + } + }); + + it("always fails to json", () => { + const schema = never(); + const result = schema.json("test"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toHaveLength(1); + expect(result.errors[0]?.message).toBe("Expected never"); + } + }); + + it("fails with any value including undefined", () => { + const schema = never(); + expect(schema.parse(undefined).ok).toBe(false); + expect(schema.parse(null).ok).toBe(false); + expect(schema.parse(0).ok).toBe(false); + expect(schema.parse("").ok).toBe(false); + expect(schema.parse({}).ok).toBe(false); + expect(schema.parse([]).ok).toBe(false); + }); + + it("works when called without options parameter", () => { + const schema = never(); + // This tests that the default = {} parameter works correctly + const result = schema.parse("test"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toHaveLength(1); + expect(result.errors[0]?.message).toBe("Expected never"); + expect(result.errors[0]?.path).toEqual([]); + } + }); + + it("succeeds with skipValidation", () => { + const schema = never(); + const result = schema.parse("test", { skipValidation: true }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value).toBe("test"); + } + }); +}); diff --git a/tests/unit/url/join.test.ts b/tests/unit/url/join.test.ts index 1956a8c..123488f 100644 --- a/tests/unit/url/join.test.ts +++ b/tests/unit/url/join.test.ts @@ -1,88 +1,223 @@ import { join } from "../../../src/core/url/index"; describe("join", () => { - describe("basic functionality", () => { - it("should return empty string for empty base", () => { - expect(join("")).toBe(""); - expect(join("", "path")).toBe(""); - }); + interface TestCase { + description: string; + base: string; + segments: string[]; + expected: string; + } - 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"); - }); + 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 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/"); + basicTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); }); }); describe("URL handling", () => { - 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"); - }); + 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 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"); + urlTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); }); }); describe("edge cases", () => { - 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"); - }); + 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 complex nested paths", () => { - expect(join("api/v1/", "/users/", "/123/", "/profile")).toBe("api/v1/users/123/profile"); + edgeCaseTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); }); }); describe("real-world scenarios", () => { - 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"); - }); + 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 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"); + realWorldTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); }); }); @@ -100,21 +235,50 @@ describe("join", () => { }); describe("trailing slash preservation", () => { - 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/"); - }); + 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 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/"); + trailingSlashTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); }); }); }); diff --git a/tests/unit/url/qs.test.ts b/tests/unit/url/qs.test.ts index 80e7e04..42cdffb 100644 --- a/tests/unit/url/qs.test.ts +++ b/tests/unit/url/qs.test.ts @@ -1,187 +1,278 @@ import { toQueryString } from "../../../src/core/url/index"; describe("Test qs toQueryString", () => { - describe("Basic functionality", () => { - it("should return empty string for null/undefined", () => { - expect(toQueryString(null)).toBe(""); - expect(toQueryString(undefined)).toBe(""); - }); + interface BasicTestCase { + description: string; + input: any; + expected: string; + } - 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(""); - }); + 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 handle simple key-value pairs", () => { - const obj = { name: "John", age: 30 }; - expect(toQueryString(obj)).toBe("name=John&age=30"); + basicTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); }); }); describe("Array handling", () => { - 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"); - }); + interface ArrayTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" }; + expected: string; + } - 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"); - }); + 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 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"); + arrayTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); }); }); describe("Nested objects", () => { - 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", - ); - }); + 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 empty nested objects", () => { - const obj = { user: {} }; - expect(toQueryString(obj)).toBe(""); + nestedTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); }); }); describe("Encoding", () => { - 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"); - }); + interface EncodingTestCase { + description: string; + input: any; + options?: { encode?: boolean }; + expected: string; + } - 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"); - }); + 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 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"); + encodingTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); }); }); describe("Mixed scenarios", () => { - it("should handle complex nested structures", () => { - const obj = { - filters: { - status: ["active", "pending"], - category: { - type: "electronics", - subcategories: ["phones", "laptops"], + 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"], + }, }, + sort: { field: "name", direction: "asc" }, }, - 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"], + 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" }, }, - 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"); - }); + 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", + }, + ]; - 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"); + mixedTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); }); }); describe("Edge cases", () => { - 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"); - }); + 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 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"); + edgeCaseTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); }); }); describe("Options combinations", () => { - 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"); - }); + interface OptionsTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices"; encode?: boolean }; + expected: string; + } - 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"); - }); + 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 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"); + optionsTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); }); }); }); diff --git a/tests/wire/accounts.test.ts b/tests/wire/accounts.test.ts new file mode 100644 index 0000000..f4904c4 --- /dev/null +++ b/tests/wire/accounts.test.ts @@ -0,0 +1,453 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("AccountsClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [ + { + id: "id", + name: "name", + external_id: "external_id", + healthy: true, + dead: true, + app: { + name_slug: "name_slug", + name: "name", + img_src: "img_src", + categories: ["categories"], + featured_weight: 1.1, + }, + created_at: "2024-01-15T09:30:00Z", + updated_at: "2024-01-15T09:30:00Z", + credentials: { key: "value" }, + expires_at: "2024-01-15T09:30:00Z", + error: "error", + last_refreshed_at: "2024-01-15T09:30:00Z", + next_refresh_at: "2024-01-15T09:30:00Z", + }, + ], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/accounts") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + id: "id", + name: "name", + externalId: "external_id", + healthy: true, + dead: true, + app: { + nameSlug: "name_slug", + name: "name", + imgSrc: "img_src", + categories: ["categories"], + featuredWeight: 1.1, + }, + createdAt: new Date("2024-01-15T09:30:00.000Z"), + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + credentials: { + key: "value", + }, + expiresAt: new Date("2024-01-15T09:30:00.000Z"), + error: "error", + lastRefreshedAt: new Date("2024-01-15T09:30:00.000Z"), + nextRefreshAt: new Date("2024-01-15T09:30:00.000Z"), + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.accounts.list({ + externalUserId: "external_user_id", + oauthAppId: "oauth_app_id", + after: "after", + before: "before", + limit: 1, + app: "app", + includeCredentials: true, + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/accounts") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.accounts.list(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("create (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { app_slug: "app_slug", cfmap_json: "cfmap_json", connect_token: "connect_token" }; + const rawResponseBody = { + id: "id", + name: "name", + external_id: "external_id", + healthy: true, + dead: true, + app: { + id: "id", + name_slug: "name_slug", + name: "name", + auth_type: "keys", + description: "description", + img_src: "img_src", + custom_fields_json: "custom_fields_json", + categories: ["categories"], + featured_weight: 1.1, + }, + created_at: "2024-01-15T09:30:00Z", + updated_at: "2024-01-15T09:30:00Z", + credentials: { key: "value" }, + expires_at: "2024-01-15T09:30:00Z", + error: "error", + last_refreshed_at: "2024-01-15T09:30:00Z", + next_refresh_at: "2024-01-15T09:30:00Z", + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/accounts") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.accounts.create({ + externalUserId: "external_user_id", + oauthAppId: "oauth_app_id", + appSlug: "app_slug", + cfmapJson: "cfmap_json", + connectToken: "connect_token", + }); + expect(response).toEqual({ + id: "id", + name: "name", + externalId: "external_id", + healthy: true, + dead: true, + app: { + id: "id", + nameSlug: "name_slug", + name: "name", + authType: "keys", + description: "description", + imgSrc: "img_src", + customFieldsJson: "custom_fields_json", + categories: ["categories"], + featuredWeight: 1.1, + }, + createdAt: new Date("2024-01-15T09:30:00.000Z"), + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + credentials: { + key: "value", + }, + expiresAt: new Date("2024-01-15T09:30:00.000Z"), + error: "error", + lastRefreshedAt: new Date("2024-01-15T09:30:00.000Z"), + nextRefreshAt: new Date("2024-01-15T09:30:00.000Z"), + }); + }); + + test("create (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { app_slug: "app_slug", cfmap_json: "cfmap_json", connect_token: "connect_token" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/accounts") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.accounts.create({ + appSlug: "app_slug", + cfmapJson: "cfmap_json", + connectToken: "connect_token", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieve (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + id: "id", + name: "name", + external_id: "external_id", + healthy: true, + dead: true, + app: { + id: "id", + name_slug: "name_slug", + name: "name", + auth_type: "keys", + description: "description", + img_src: "img_src", + custom_fields_json: "custom_fields_json", + categories: ["categories"], + featured_weight: 1.1, + }, + created_at: "2024-01-15T09:30:00Z", + updated_at: "2024-01-15T09:30:00Z", + credentials: { key: "value" }, + expires_at: "2024-01-15T09:30:00Z", + error: "error", + last_refreshed_at: "2024-01-15T09:30:00Z", + next_refresh_at: "2024-01-15T09:30:00Z", + }; + server + .mockEndpoint() + .get("/v1/connect/project_id/accounts/account_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.accounts.retrieve("account_id", { + includeCredentials: true, + }); + expect(response).toEqual({ + id: "id", + name: "name", + externalId: "external_id", + healthy: true, + dead: true, + app: { + id: "id", + nameSlug: "name_slug", + name: "name", + authType: "keys", + description: "description", + imgSrc: "img_src", + customFieldsJson: "custom_fields_json", + categories: ["categories"], + featuredWeight: 1.1, + }, + createdAt: new Date("2024-01-15T09:30:00.000Z"), + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + credentials: { + key: "value", + }, + expiresAt: new Date("2024-01-15T09:30:00.000Z"), + error: "error", + lastRefreshedAt: new Date("2024-01-15T09:30:00.000Z"), + nextRefreshAt: new Date("2024-01-15T09:30:00.000Z"), + }); + }); + + test("retrieve (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/accounts/account_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.accounts.retrieve("account_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("delete (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + server + .mockEndpoint() + .delete("/v1/connect/project_id/accounts/account_id") + .respondWith() + .statusCode(200) + .build(); + + const response = await client.accounts.delete("account_id"); + expect(response).toEqual(undefined); + }); + + test("delete (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .delete("/v1/connect/project_id/accounts/account_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.accounts.delete("account_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("deleteByApp (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + server + .mockEndpoint() + .delete("/v1/connect/project_id/apps/app_id/accounts") + .respondWith() + .statusCode(200) + .build(); + + const response = await client.accounts.deleteByApp("app_id"); + expect(response).toEqual(undefined); + }); + + test("deleteByApp (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .delete("/v1/connect/project_id/apps/app_id/accounts") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.accounts.deleteByApp("app_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/actions.test.ts b/tests/wire/actions.test.ts new file mode 100644 index 0000000..a13428b --- /dev/null +++ b/tests/wire/actions.test.ts @@ -0,0 +1,481 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("ActionsClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [ + { + key: "key", + name: "name", + version: "version", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + description: "description", + component_type: "component_type", + stash: "optional", + }, + ], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/actions") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + key: "key", + name: "name", + version: "version", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + description: "description", + componentType: "component_type", + stash: "optional", + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.actions.list({ + after: "after", + before: "before", + limit: 1, + q: "q", + app: "app", + registry: "public", + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/actions") + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.actions.list(); + }).rejects.toThrow(Pipedream.BadRequestError); + }); + + test("list (3)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/actions") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.actions.list(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieve (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: { + key: "key", + name: "name", + version: "version", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + description: "description", + component_type: "component_type", + stash: "optional", + annotations: { + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + readOnlyHint: true, + title: "title", + }, + }, + }; + server + .mockEndpoint() + .get("/v1/connect/project_id/actions/component_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.actions.retrieve("component_id", { + version: "1.2.3", + }); + expect(response).toEqual({ + data: { + key: "key", + name: "name", + version: "version", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + description: "description", + componentType: "component_type", + stash: "optional", + annotations: { + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + readOnlyHint: true, + title: "title", + }, + }, + }); + }); + + test("retrieve (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/actions/component_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.actions.retrieve("component_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("configureProp (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id", prop_name: "prop_name" }; + const rawResponseBody = { + options: [{ label: "label", value: "value" }], + string_options: ["string_options"], + observations: [{ k: "k", msg: "msg", ts: 1.1 }], + context: { key: "value" }, + errors: ["errors"], + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/actions/configure") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.actions.configureProp({ + id: "id", + externalUserId: "external_user_id", + propName: "prop_name", + }); + expect(response).toEqual({ + options: [ + { + label: "label", + value: "value", + }, + ], + stringOptions: ["string_options"], + observations: [ + { + k: "k", + msg: "msg", + ts: 1.1, + }, + ], + context: { + key: "value", + }, + errors: ["errors"], + }); + }); + + test("configureProp (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id", prop_name: "prop_name" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/actions/configure") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.actions.configureProp({ + id: "id", + externalUserId: "external_user_id", + propName: "prop_name", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("reloadProps (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { + observations: [{ k: "k", msg: "msg", ts: 1.1 }], + errors: ["errors"], + dynamicProps: { id: "id", configurableProps: [{ type: "alert", name: "name", content: "content" }] }, + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/actions/props") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.actions.reloadProps({ + id: "id", + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + observations: [ + { + k: "k", + msg: "msg", + ts: 1.1, + }, + ], + errors: ["errors"], + dynamicProps: { + id: "id", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + }, + }); + }); + + test("reloadProps (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/actions/props") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.actions.reloadProps({ + id: "id", + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("run (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { + exports: { key: "value" }, + os: { key: "value" }, + ret: { key: "value" }, + stash_id: "stash_id", + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/actions/run") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.actions.run({ + id: "id", + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + exports: { + key: "value", + }, + os: { + key: "value", + }, + ret: { + key: "value", + }, + stashId: "stash_id", + }); + }); + + test("run (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/actions/run") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.actions.run({ + id: "id", + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/appCategories.test.ts b/tests/wire/appCategories.test.ts new file mode 100644 index 0000000..c7af23f --- /dev/null +++ b/tests/wire/appCategories.test.ts @@ -0,0 +1,69 @@ +// This file was auto-generated by Fern from our API Definition. + +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("AppCategoriesClient", () => { + test("list", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = [{ id: "id", name: "name", description: "description" }]; + server + .mockEndpoint() + .get("/v1/connect/app_categories") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.appCategories.list(); + expect(response).toEqual([ + { + id: "id", + name: "name", + description: "description", + }, + ]); + }); + + test("retrieve", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { id: "id", name: "name", description: "description" }; + server + .mockEndpoint() + .get("/v1/connect/app_categories/id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.appCategories.retrieve("id"); + expect(response).toEqual({ + id: "id", + name: "name", + description: "description", + }); + }); +}); diff --git a/tests/wire/apps.test.ts b/tests/wire/apps.test.ts new file mode 100644 index 0000000..39e7772 --- /dev/null +++ b/tests/wire/apps.test.ts @@ -0,0 +1,130 @@ +// This file was auto-generated by Fern from our API Definition. + +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("AppsClient", () => { + test("list", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [ + { + id: "id", + name_slug: "name_slug", + name: "name", + auth_type: "keys", + description: "description", + img_src: "img_src", + custom_fields_json: "custom_fields_json", + categories: ["categories"], + featured_weight: 1.1, + }, + ], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/apps") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + id: "id", + nameSlug: "name_slug", + name: "name", + authType: "keys", + description: "description", + imgSrc: "img_src", + customFieldsJson: "custom_fields_json", + categories: ["categories"], + featuredWeight: 1.1, + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.apps.list({ + after: "after", + before: "before", + limit: 1, + q: "q", + sortKey: "name", + sortDirection: "asc", + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("retrieve", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: { + id: "id", + name_slug: "name_slug", + name: "name", + auth_type: "keys", + description: "description", + img_src: "img_src", + custom_fields_json: "custom_fields_json", + categories: ["categories"], + featured_weight: 1.1, + }, + }; + server + .mockEndpoint() + .get("/v1/connect/apps/app_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.apps.retrieve("app_id"); + expect(response).toEqual({ + data: { + id: "id", + nameSlug: "name_slug", + name: "name", + authType: "keys", + description: "description", + imgSrc: "img_src", + customFieldsJson: "custom_fields_json", + categories: ["categories"], + featuredWeight: 1.1, + }, + }); + }); +}); diff --git a/tests/wire/components.test.ts b/tests/wire/components.test.ts new file mode 100644 index 0000000..9673e9e --- /dev/null +++ b/tests/wire/components.test.ts @@ -0,0 +1,405 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("ComponentsClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [ + { + key: "key", + name: "name", + version: "version", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + description: "description", + component_type: "component_type", + stash: "optional", + }, + ], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/components") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + key: "key", + name: "name", + version: "version", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + description: "description", + componentType: "component_type", + stash: "optional", + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.components.list({ + after: "after", + before: "before", + limit: 1, + q: "q", + app: "app", + registry: "public", + componentType: "trigger", + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/components") + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.components.list(); + }).rejects.toThrow(Pipedream.BadRequestError); + }); + + test("list (3)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/components") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.components.list(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieve (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: { + key: "key", + name: "name", + version: "version", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + description: "description", + component_type: "component_type", + stash: "optional", + annotations: { + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + readOnlyHint: true, + title: "title", + }, + }, + }; + server + .mockEndpoint() + .get("/v1/connect/project_id/components/component_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.components.retrieve("component_id", { + version: "1.2.3", + }); + expect(response).toEqual({ + data: { + key: "key", + name: "name", + version: "version", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + description: "description", + componentType: "component_type", + stash: "optional", + annotations: { + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + readOnlyHint: true, + title: "title", + }, + }, + }); + }); + + test("retrieve (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/components/component_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.components.retrieve("component_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("configureProp (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id", prop_name: "prop_name" }; + const rawResponseBody = { + options: [{ label: "label", value: "value" }], + string_options: ["string_options"], + observations: [{ k: "k", msg: "msg", ts: 1.1 }], + context: { key: "value" }, + errors: ["errors"], + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/components/configure") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.components.configureProp({ + id: "id", + externalUserId: "external_user_id", + propName: "prop_name", + }); + expect(response).toEqual({ + options: [ + { + label: "label", + value: "value", + }, + ], + stringOptions: ["string_options"], + observations: [ + { + k: "k", + msg: "msg", + ts: 1.1, + }, + ], + context: { + key: "value", + }, + errors: ["errors"], + }); + }); + + test("configureProp (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id", prop_name: "prop_name" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/components/configure") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.components.configureProp({ + id: "id", + externalUserId: "external_user_id", + propName: "prop_name", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("reloadProps (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { + observations: [{ k: "k", msg: "msg", ts: 1.1 }], + errors: ["errors"], + dynamicProps: { id: "id", configurableProps: [{ type: "alert", name: "name", content: "content" }] }, + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/components/props") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.components.reloadProps({ + id: "id", + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + observations: [ + { + k: "k", + msg: "msg", + ts: 1.1, + }, + ], + errors: ["errors"], + dynamicProps: { + id: "id", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + }, + }); + }); + + test("reloadProps (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/components/props") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.components.reloadProps({ + id: "id", + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/deployedTriggers.test.ts b/tests/wire/deployedTriggers.test.ts new file mode 100644 index 0000000..4bbb40f --- /dev/null +++ b/tests/wire/deployedTriggers.test.ts @@ -0,0 +1,704 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("DeployedTriggersClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [ + { + type: "DeployedComponent", + id: "id", + owner_id: "owner_id", + component_id: "component_id", + component_key: "component_key", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + configured_props: {}, + active: true, + created_at: 1, + updated_at: 1, + name: "name", + name_slug: "name_slug", + callback_observations: { key: "value" }, + emit_on_deploy: true, + }, + ], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/deployed-triggers") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + type: "DeployedComponent", + id: "id", + ownerId: "owner_id", + componentId: "component_id", + componentKey: "component_key", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + configuredProps: {}, + active: true, + createdAt: 1, + updatedAt: 1, + name: "name", + nameSlug: "name_slug", + callbackObservations: { + key: "value", + }, + emitOnDeploy: true, + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.deployedTriggers.list({ + after: "after", + before: "before", + limit: 1, + externalUserId: "external_user_id", + emitterType: "email", + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/deployed-triggers") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.list({ + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieve (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: { + type: "DeployedComponent", + id: "id", + owner_id: "owner_id", + component_id: "component_id", + component_key: "component_key", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + configured_props: { key: { key: "value" } }, + active: true, + created_at: 1, + updated_at: 1, + name: "name", + name_slug: "name_slug", + callback_observations: { key: "value" }, + emit_on_deploy: true, + }, + }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.retrieve("trigger_id", { + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + data: { + type: "DeployedComponent", + id: "id", + ownerId: "owner_id", + componentId: "component_id", + componentKey: "component_key", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + configuredProps: { + key: { + key: "value", + }, + }, + active: true, + createdAt: 1, + updatedAt: 1, + name: "name", + nameSlug: "name_slug", + callbackObservations: { + key: "value", + }, + emitOnDeploy: true, + }, + }); + }); + + test("retrieve (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.retrieve("trigger_id", { + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("update (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = {}; + const rawResponseBody = { + data: { + type: "DeployedComponent", + id: "id", + owner_id: "owner_id", + component_id: "component_id", + component_key: "component_key", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + configured_props: { key: { key: "value" } }, + active: true, + created_at: 1, + updated_at: 1, + name: "name", + name_slug: "name_slug", + callback_observations: { key: "value" }, + emit_on_deploy: true, + }, + }; + server + .mockEndpoint() + .put("/v1/connect/project_id/deployed-triggers/trigger_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.update("trigger_id", { + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + data: { + type: "DeployedComponent", + id: "id", + ownerId: "owner_id", + componentId: "component_id", + componentKey: "component_key", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + configuredProps: { + key: { + key: "value", + }, + }, + active: true, + createdAt: 1, + updatedAt: 1, + name: "name", + nameSlug: "name_slug", + callbackObservations: { + key: "value", + }, + emitOnDeploy: true, + }, + }); + }); + + test("update (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = {}; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .put("/v1/connect/project_id/deployed-triggers/trigger_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.update("trigger_id", { + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("delete (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + server + .mockEndpoint() + .delete("/v1/connect/project_id/deployed-triggers/trigger_id") + .respondWith() + .statusCode(200) + .build(); + + const response = await client.deployedTriggers.delete("trigger_id", { + externalUserId: "external_user_id", + ignoreHookErrors: true, + }); + expect(response).toEqual(undefined); + }); + + test("delete (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .delete("/v1/connect/project_id/deployed-triggers/trigger_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.delete("trigger_id", { + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("listEvents (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { data: [{ e: { key: "value" }, k: "k", ts: 1, id: "id" }] }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id/events") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.listEvents("trigger_id", { + externalUserId: "external_user_id", + n: 1, + }); + expect(response).toEqual({ + data: [ + { + e: { + key: "value", + }, + k: "k", + ts: 1, + id: "id", + }, + ], + }); + }); + + test("listEvents (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id/events") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.listEvents("trigger_id", { + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("listWorkflows (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { workflow_ids: ["workflow_ids"] }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id/pipelines") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.listWorkflows("trigger_id", { + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + workflowIds: ["workflow_ids"], + }); + }); + + test("listWorkflows (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id/pipelines") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.listWorkflows("trigger_id", { + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("updateWorkflows (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { workflow_ids: ["workflow_ids"] }; + const rawResponseBody = { workflow_ids: ["workflow_ids"] }; + server + .mockEndpoint() + .put("/v1/connect/project_id/deployed-triggers/trigger_id/pipelines") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.updateWorkflows("trigger_id", { + externalUserId: "external_user_id", + workflowIds: ["workflow_ids"], + }); + expect(response).toEqual({ + workflowIds: ["workflow_ids"], + }); + }); + + test("updateWorkflows (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { workflow_ids: ["workflow_ids", "workflow_ids"] }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .put("/v1/connect/project_id/deployed-triggers/trigger_id/pipelines") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.updateWorkflows("trigger_id", { + externalUserId: "external_user_id", + workflowIds: ["workflow_ids", "workflow_ids"], + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("listWebhooks (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { webhook_urls: ["webhook_urls"] }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id/webhooks") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.listWebhooks("trigger_id", { + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + webhookUrls: ["webhook_urls"], + }); + }); + + test("listWebhooks (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/deployed-triggers/trigger_id/webhooks") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.listWebhooks("trigger_id", { + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("updateWebhooks (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { webhook_urls: ["webhook_urls"] }; + const rawResponseBody = { webhook_urls: ["webhook_urls"] }; + server + .mockEndpoint() + .put("/v1/connect/project_id/deployed-triggers/trigger_id/webhooks") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.deployedTriggers.updateWebhooks("trigger_id", { + externalUserId: "external_user_id", + webhookUrls: ["webhook_urls"], + }); + expect(response).toEqual({ + webhookUrls: ["webhook_urls"], + }); + }); + + test("updateWebhooks (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { webhook_urls: ["webhook_urls", "webhook_urls"] }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .put("/v1/connect/project_id/deployed-triggers/trigger_id/webhooks") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.deployedTriggers.updateWebhooks("trigger_id", { + externalUserId: "external_user_id", + webhookUrls: ["webhook_urls", "webhook_urls"], + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/mockAuth.ts b/tests/wire/mockAuth.ts new file mode 100644 index 0000000..207d885 --- /dev/null +++ b/tests/wire/mockAuth.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { MockServer } from "../mock-server/MockServer"; + +export function mockOAuth(server: MockServer): void { + const rawRequestBody = { grant_type: "client_credentials", client_id: "client_id", client_secret: "client_secret" }; + const rawResponseBody = { access_token: "access_token", token_type: "token_type", expires_in: 1 }; + server + .mockEndpoint() + .post("/v1/oauth/token") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); +} diff --git a/tests/wire/oauthTokens.test.ts b/tests/wire/oauthTokens.test.ts new file mode 100644 index 0000000..6d386b3 --- /dev/null +++ b/tests/wire/oauthTokens.test.ts @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("OauthTokensClient", () => { + test("create", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { + grant_type: "client_credentials", + client_id: "client_id", + client_secret: "client_secret", + }; + const rawResponseBody = { access_token: "access_token", token_type: "token_type", expires_in: 1 }; + server + .mockEndpoint() + .post("/v1/oauth/token") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.oauthTokens.create({ + clientId: "client_id", + clientSecret: "client_secret", + }); + expect(response).toEqual({ + accessToken: "access_token", + tokenType: "token_type", + expiresIn: 1, + }); + }); +}); diff --git a/tests/wire/projects.test.ts b/tests/wire/projects.test.ts new file mode 100644 index 0000000..95a7d4b --- /dev/null +++ b/tests/wire/projects.test.ts @@ -0,0 +1,492 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("ProjectsClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [{ id: "id", name: "name", app_name: "app_name", support_email: "support_email" }], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/projects") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + id: "id", + name: "name", + appName: "app_name", + supportEmail: "support_email", + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.projects.list({ + after: "after", + before: "before", + limit: 1, + q: "q", + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/projects") + .respondWith() + .statusCode(404) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.list(); + }).rejects.toThrow(Pipedream.NotFoundError); + }); + + test("list (3)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/projects") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.list(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("create (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { name: "name" }; + const rawResponseBody = { id: "id", name: "name", app_name: "app_name", support_email: "support_email" }; + server + .mockEndpoint() + .post("/v1/connect/projects") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.projects.create({ + name: "name", + }); + expect(response).toEqual({ + id: "id", + name: "name", + appName: "app_name", + supportEmail: "support_email", + }); + }); + + test("create (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { name: "name" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/projects") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.create({ + name: "name", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieve (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { id: "id", name: "name", app_name: "app_name", support_email: "support_email" }; + server + .mockEndpoint() + .get("/v1/connect/projects/project_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.projects.retrieve(); + expect(response).toEqual({ + id: "id", + name: "name", + appName: "app_name", + supportEmail: "support_email", + }); + }); + + test("retrieve (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/projects/project_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.retrieve(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("delete (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + server.mockEndpoint().delete("/v1/connect/projects/project_id").respondWith().statusCode(200).build(); + + const response = await client.projects.delete(); + expect(response).toEqual(undefined); + }); + + test("delete (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .delete("/v1/connect/projects/project_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.delete(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("update (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = {}; + const rawResponseBody = { id: "id", name: "name", app_name: "app_name", support_email: "support_email" }; + server + .mockEndpoint() + .patch("/v1/connect/projects/project_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.projects.update(); + expect(response).toEqual({ + id: "id", + name: "name", + appName: "app_name", + supportEmail: "support_email", + }); + }); + + test("update (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = {}; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .patch("/v1/connect/projects/project_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.update(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("updateLogo (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { logo: "data:image/png;base64,AAAAAA..." }; + + server + .mockEndpoint() + .post("/v1/connect/projects/project_id/logo") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .build(); + + const response = await client.projects.updateLogo({ + logo: "data:image/png;base64,AAAAAA...", + }); + expect(response).toEqual(undefined); + }); + + test("updateLogo (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { logo: "logo" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/projects/project_id/logo") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.updateLogo({ + logo: "logo", + }); + }).rejects.toThrow(Pipedream.BadRequestError); + }); + + test("updateLogo (3)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { logo: "logo" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/projects/project_id/logo") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.updateLogo({ + logo: "logo", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieveInfo (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { apps: [{ id: "id", name_slug: "name_slug" }] }; + server + .mockEndpoint() + .get("/v1/connect/project_id/projects/info") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.projects.retrieveInfo(); + expect(response).toEqual({ + apps: [ + { + id: "id", + nameSlug: "name_slug", + }, + ], + }); + }); + + test("retrieveInfo (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/projects/info") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.projects.retrieveInfo(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/tokens.test.ts b/tests/wire/tokens.test.ts new file mode 100644 index 0000000..778386d --- /dev/null +++ b/tests/wire/tokens.test.ts @@ -0,0 +1,175 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("TokensClient", () => { + test("create (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { external_user_id: "external_user_id" }; + const rawResponseBody = { + connect_link_url: "connect_link_url", + expires_at: "2024-01-15T09:30:00Z", + token: "token", + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/tokens") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.tokens.create({ + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + connectLinkUrl: "connect_link_url", + expiresAt: new Date("2024-01-15T09:30:00.000Z"), + token: "token", + }); + }); + + test("create (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { external_user_id: "external_user_id" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/tokens") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.tokens.create({ + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("validate (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + app: { + id: "id", + name_slug: "name_slug", + name: "name", + auth_type: "keys", + description: "description", + img_src: "img_src", + custom_fields_json: "custom_fields_json", + categories: ["categories"], + featured_weight: 1.1, + }, + error: "error", + error_redirect_uri: "error_redirect_uri", + oauth_app_id: "oauth_app_id", + project_app_name: "project_app_name", + project_environment: "project_environment", + project_id: "project_id", + project_support_email: "project_support_email", + success: true, + success_redirect_uri: "success_redirect_uri", + }; + server + .mockEndpoint() + .get("/v1/connect/tokens/ctok/validate") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.tokens.validate("ctok", { + appId: "app_id", + oauthAppId: "oauth_app_id", + }); + expect(response).toEqual({ + app: { + id: "id", + nameSlug: "name_slug", + name: "name", + authType: "keys", + description: "description", + imgSrc: "img_src", + customFieldsJson: "custom_fields_json", + categories: ["categories"], + featuredWeight: 1.1, + }, + error: "error", + errorRedirectUri: "error_redirect_uri", + oauthAppId: "oauth_app_id", + projectAppName: "project_app_name", + projectEnvironment: "project_environment", + projectId: "project_id", + projectSupportEmail: "project_support_email", + success: true, + successRedirectUri: "success_redirect_uri", + }); + }); + + test("validate (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "projectId", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/tokens/ctok/validate") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.tokens.validate("ctok", { + appId: "app_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/triggers.test.ts b/tests/wire/triggers.test.ts new file mode 100644 index 0000000..dc3760b --- /dev/null +++ b/tests/wire/triggers.test.ts @@ -0,0 +1,511 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("TriggersClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: [ + { + key: "key", + name: "name", + version: "version", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + description: "description", + component_type: "component_type", + stash: "optional", + }, + ], + page_info: { count: 1, total_count: 1, start_cursor: "start_cursor", end_cursor: "end_cursor" }, + }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/triggers") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + key: "key", + name: "name", + version: "version", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + description: "description", + componentType: "component_type", + stash: "optional", + }, + ], + pageInfo: { + count: 1, + totalCount: 1, + startCursor: "start_cursor", + endCursor: "end_cursor", + }, + }; + const page = await client.triggers.list({ + after: "after", + before: "before", + limit: 1, + q: "q", + app: "app", + registry: "public", + }); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/triggers") + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.triggers.list(); + }).rejects.toThrow(Pipedream.BadRequestError); + }); + + test("list (3)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint({ once: false }) + .get("/v1/connect/project_id/triggers") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.triggers.list(); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("retrieve (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { + data: { + key: "key", + name: "name", + version: "version", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + description: "description", + component_type: "component_type", + stash: "optional", + annotations: { + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + readOnlyHint: true, + title: "title", + }, + }, + }; + server + .mockEndpoint() + .get("/v1/connect/project_id/triggers/component_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.triggers.retrieve("component_id", { + version: "1.2.3", + }); + expect(response).toEqual({ + data: { + key: "key", + name: "name", + version: "version", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + description: "description", + componentType: "component_type", + stash: "optional", + annotations: { + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + readOnlyHint: true, + title: "title", + }, + }, + }); + }); + + test("retrieve (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .get("/v1/connect/project_id/triggers/component_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.triggers.retrieve("component_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("configureProp (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id", prop_name: "prop_name" }; + const rawResponseBody = { + options: [{ label: "label", value: "value" }], + string_options: ["string_options"], + observations: [{ k: "k", msg: "msg", ts: 1.1 }], + context: { key: "value" }, + errors: ["errors"], + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/triggers/configure") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.triggers.configureProp({ + id: "id", + externalUserId: "external_user_id", + propName: "prop_name", + }); + expect(response).toEqual({ + options: [ + { + label: "label", + value: "value", + }, + ], + stringOptions: ["string_options"], + observations: [ + { + k: "k", + msg: "msg", + ts: 1.1, + }, + ], + context: { + key: "value", + }, + errors: ["errors"], + }); + }); + + test("configureProp (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id", prop_name: "prop_name" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/triggers/configure") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.triggers.configureProp({ + id: "id", + externalUserId: "external_user_id", + propName: "prop_name", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("reloadProps (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { + observations: [{ k: "k", msg: "msg", ts: 1.1 }], + errors: ["errors"], + dynamicProps: { id: "id", configurableProps: [{ type: "alert", name: "name", content: "content" }] }, + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/triggers/props") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.triggers.reloadProps({ + id: "id", + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + observations: [ + { + k: "k", + msg: "msg", + ts: 1.1, + }, + ], + errors: ["errors"], + dynamicProps: { + id: "id", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + }, + }); + }); + + test("reloadProps (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/triggers/props") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.triggers.reloadProps({ + id: "id", + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); + + test("deploy (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { + data: { + type: "DeployedComponent", + id: "id", + owner_id: "owner_id", + component_id: "component_id", + component_key: "component_key", + configurable_props: [{ type: "alert", name: "name", content: "content" }], + configured_props: { key: { key: "value" } }, + active: true, + created_at: 1, + updated_at: 1, + name: "name", + name_slug: "name_slug", + callback_observations: { key: "value" }, + emit_on_deploy: true, + }, + }; + server + .mockEndpoint() + .post("/v1/connect/project_id/triggers/deploy") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.triggers.deploy({ + id: "id", + externalUserId: "external_user_id", + }); + expect(response).toEqual({ + data: { + type: "DeployedComponent", + id: "id", + ownerId: "owner_id", + componentId: "component_id", + componentKey: "component_key", + configurableProps: [ + { + type: "alert", + name: "name", + content: "content", + }, + ], + configuredProps: { + key: { + key: "value", + }, + }, + active: true, + createdAt: 1, + updatedAt: 1, + name: "name", + nameSlug: "name_slug", + callbackObservations: { + key: "value", + }, + emitOnDeploy: true, + }, + }); + }); + + test("deploy (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + const rawRequestBody = { id: "id", external_user_id: "external_user_id" }; + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .post("/v1/connect/project_id/triggers/deploy") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.triggers.deploy({ + id: "id", + externalUserId: "external_user_id", + }); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/tests/wire/users.test.ts b/tests/wire/users.test.ts new file mode 100644 index 0000000..d66d8c5 --- /dev/null +++ b/tests/wire/users.test.ts @@ -0,0 +1,59 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as Pipedream from "../../src/api/index"; +import { PipedreamClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; +import { mockOAuth } from "./mockAuth"; + +describe("UsersClient", () => { + test("deleteExternalUser (1)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + server + .mockEndpoint() + .delete("/v1/connect/project_id/users/external_user_id") + .respondWith() + .statusCode(200) + .build(); + + const response = await client.users.deleteExternalUser("external_user_id"); + expect(response).toEqual(undefined); + }); + + test("deleteExternalUser (2)", async () => { + const server = mockServerPool.createServer(); + mockOAuth(server); + + const client = new PipedreamClient({ + maxRetries: 0, + projectId: "project_id", + clientId: "client_id", + clientSecret: "client_secret", + projectEnvironment: "test", + environment: server.baseUrl, + }); + + const rawResponseBody = { key: "value" }; + server + .mockEndpoint() + .delete("/v1/connect/project_id/users/external_user_id") + .respondWith() + .statusCode(429) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.users.deleteExternalUser("external_user_id"); + }).rejects.toThrow(Pipedream.TooManyRequestsError); + }); +}); diff --git a/vitest.config.ts b/vitest.config.mts similarity index 85% rename from vitest.config.ts rename to vitest.config.mts index 677c585..ba2ec4f 100644 --- a/vitest.config.ts +++ b/vitest.config.mts @@ -10,6 +10,7 @@ export default defineConfig({ root: "./tests", include: ["**/*.test.{js,ts,jsx,tsx}"], exclude: ["wire/**"], + setupFiles: ["./setup.ts"], }, }, { @@ -18,7 +19,7 @@ export default defineConfig({ name: "wire", environment: "node", root: "./tests/wire", - setupFiles: ["../mock-server/setup.ts"], + setupFiles: ["../setup.ts", "../mock-server/setup.ts"], }, }, ],