From 3eaee7de908bd37cb089f25747dc25f66829baa3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:24:58 +0000 Subject: [PATCH 01/14] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index a8b69ff..2e315f5 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From c92502e8ce716259136efe8dbb3edcb3e7c2899b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:20:00 +0000 Subject: [PATCH 02/14] chore(internal): codegen related update --- scripts/utils/postprocess-files.cjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/utils/postprocess-files.cjs b/scripts/utils/postprocess-files.cjs index deae575..a8cdeb7 100644 --- a/scripts/utils/postprocess-files.cjs +++ b/scripts/utils/postprocess-files.cjs @@ -23,12 +23,19 @@ async function postprocess() { // strip out lib="dom", types="node", and types="react" references; these // are needed at build time, but would pollute the user's TS environment - const transformed = code.replace( + let transformed = code.replace( /^ *\/\/\/ * ' '.repeat(match.length - 1) + '\n', ); + // TypeScript's declaration emitter collapses /** @ts-ignore */ onto the same + // line as the type declaration, which doesn't work. So we convert to // @ts-ignore + // on its own line to properly suppresses errors. + if (file.endsWith('.d.ts') || file.endsWith('.d.mts') || file.endsWith('.d.cts')) { + transformed = transformed.replace(/\/\*\* @ts-ignore\b[^*]*\*\/ /gm, '// @ts-ignore\n'); + } + if (transformed !== code) { console.error(`wrote ${path.relative(process.cwd(), file)}`); await fs.promises.writeFile(file, transformed, 'utf8'); From 305fe467afbe744697c18b8d9a505d7e0952fabb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:21:10 +0000 Subject: [PATCH 03/14] feat: support setting headers via env --- src/client.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/client.ts b/src/client.ts index c4c1cab..551037d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -204,6 +204,18 @@ export class Parallel { this.fetch = options.fetch ?? Shims.getDefaultFetch(); this.#encoder = Opts.FallbackEncoder; + const customHeadersEnv = readEnv('PARALLEL_CUSTOM_HEADERS'); + if (customHeadersEnv) { + const parsed: Record = {}; + for (const line of customHeadersEnv.split('\n')) { + const colon = line.indexOf(':'); + if (colon >= 0) { + parsed[line.substring(0, colon).trim()] = line.substring(colon + 1).trim(); + } + } + options.defaultHeaders = { ...parsed, ...options.defaultHeaders }; + } + this._options = options; this.apiKey = apiKey; From 4ac089c6b95d0b96927c841cf9587a0d395b8a91 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 04:47:34 +0000 Subject: [PATCH 04/14] chore(format): run eslint and prettier separately --- .github/workflows/release-doctor.yml | 1 - eslint.config.mjs | 3 --- package.json | 1 - scripts/fast-format | 9 +++----- scripts/format | 3 +-- scripts/lint | 3 +++ src/internal/types.ts | 14 ++++++------ yarn.lock | 32 ---------------------------- 8 files changed, 13 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 2f3cc1d..c38131b 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -19,4 +19,3 @@ jobs: bash ./bin/check-release-environment env: NPM_TOKEN: ${{ secrets.PARALLEL_NPM_TOKEN || secrets.NPM_TOKEN }} - diff --git a/eslint.config.mjs b/eslint.config.mjs index 6d90fee..ff4d179 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,6 @@ // @ts-check import tseslint from 'typescript-eslint'; import unusedImports from 'eslint-plugin-unused-imports'; -import prettier from 'eslint-plugin-prettier'; export default tseslint.config( { @@ -14,11 +13,9 @@ export default tseslint.config( plugins: { '@typescript-eslint': tseslint.plugin, 'unused-imports': unusedImports, - prettier, }, rules: { 'no-unused-vars': 'off', - 'prettier/prettier': 'error', 'unused-imports/no-unused-imports': 'error', 'no-restricted-imports': [ 'error', diff --git a/package.json b/package.json index b9e1560..f58b0bb 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", "eslint": "^9.39.1", - "eslint-plugin-prettier": "^5.4.1", "eslint-plugin-unused-imports": "^4.1.4", "iconv-lite": "^0.6.3", "jest": "^29.4.0", diff --git a/scripts/fast-format b/scripts/fast-format index 53721ac..f1873ae 100755 --- a/scripts/fast-format +++ b/scripts/fast-format @@ -31,10 +31,7 @@ if ! [ -z "$ESLINT_FILES" ]; then fi echo "==> Running prettier --write" -# format things eslint didn't -PRETTIER_FILES="$(grep '\.\(js\|json\)$' "$FILE_LIST" || true)" -if ! [ -z "$PRETTIER_FILES" ]; then - echo "$PRETTIER_FILES" | xargs ./node_modules/.bin/prettier \ - --write --cache --cache-strategy metadata --no-error-on-unmatched-pattern \ - '!**/dist' '!**/*.ts' '!**/*.mts' '!**/*.cts' '!**/*.js' '!**/*.mjs' '!**/*.cjs' +if ! [ -z "$FILE_LIST" ]; then + cat "$FILE_LIST" | xargs ./node_modules/.bin/prettier \ + --write --cache --cache-strategy metadata --no-error-on-unmatched-pattern --ignore-unknown fi diff --git a/scripts/format b/scripts/format index 7a75640..b1b2c17 100755 --- a/scripts/format +++ b/scripts/format @@ -8,5 +8,4 @@ echo "==> Running eslint --fix" ./node_modules/.bin/eslint --fix . echo "==> Running prettier --write" -# format things eslint didn't -./node_modules/.bin/prettier --write --cache --cache-strategy metadata . '!**/dist' '!**/*.ts' '!**/*.mts' '!**/*.cts' '!**/*.js' '!**/*.mjs' '!**/*.cjs' +./node_modules/.bin/prettier --write --cache --cache-strategy metadata . diff --git a/scripts/lint b/scripts/lint index 3ffb78a..1f53254 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,6 +4,9 @@ set -e cd "$(dirname "$0")/.." +echo "==> Running prettier --check" +./node_modules/.bin/prettier --check . + echo "==> Running eslint" ./node_modules/.bin/eslint . diff --git a/src/internal/types.ts b/src/internal/types.ts index b668dfc..a050513 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -40,7 +40,6 @@ type OverloadedParameters = : T extends (...args: infer A) => unknown ? A : never; -/* eslint-disable */ /** * These imports attempt to get types from a parent package's dependencies. * Unresolved bare specifiers can trigger [automatic type acquisition][1] in some projects, which @@ -63,19 +62,18 @@ type OverloadedParameters = * * [1]: https://www.typescriptlang.org/tsconfig/#typeAcquisition */ -/** @ts-ignore For users with \@types/node */ +/** @ts-ignore For users with \@types/node */ /* prettier-ignore */ type UndiciTypesRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; -/** @ts-ignore For users with undici */ +/** @ts-ignore For users with undici */ /* prettier-ignore */ type UndiciRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; -/** @ts-ignore For users with \@types/bun */ +/** @ts-ignore For users with \@types/bun */ /* prettier-ignore */ type BunRequestInit = globalThis.FetchRequestInit; -/** @ts-ignore For users with node-fetch@2 */ +/** @ts-ignore For users with node-fetch@2 */ /* prettier-ignore */ type NodeFetch2RequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; -/** @ts-ignore For users with node-fetch@3, doesn't need file extension because types are at ./@types/index.d.ts */ +/** @ts-ignore For users with node-fetch@3, doesn't need file extension because types are at ./@types/index.d.ts */ /* prettier-ignore */ type NodeFetch3RequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; -/** @ts-ignore For users who use Deno */ +/** @ts-ignore For users who use Deno */ /* prettier-ignore */ type FetchRequestInit = NonNullable[1]>; -/* eslint-enable */ type RequestInits = | NotAny diff --git a/yarn.lock b/yarn.lock index f6eae3c..18e7cbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -709,11 +709,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pkgr/core@^0.2.4": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" - integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== - "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -1515,14 +1510,6 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-prettier@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz#99b55d7dd70047886b2222fdd853665f180b36af" - integrity sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg== - dependencies: - prettier-linter-helpers "^1.0.0" - synckit "^0.11.7" - eslint-plugin-unused-imports@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz#62ddc7446ccbf9aa7b6f1f0b00a980423cda2738" @@ -1674,11 +1661,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@^1.1.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" - integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== - fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -2841,13 +2823,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - prettier@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" @@ -3144,13 +3119,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@^0.11.7: - version "0.11.8" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457" - integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== - dependencies: - "@pkgr/core" "^0.2.4" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" From 0ca7138dc69549cb8419f67963e529ceae674da2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:23:35 +0000 Subject: [PATCH 05/14] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 778c22b..2ea2dd0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web%2Fparallel-sdk-66ee13c3475d2c76f0956f258f0469903155b83ef02e839641be94cdc2014cf3.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-66ee13c3475d2c76f0956f258f0469903155b83ef02e839641be94cdc2014cf3.yml openapi_spec_hash: 88af7b88725bead1f8ccdcaeb436fadb config_hash: e17d82e9cb35004e5f9a9d3c4cf51aeb From b939d5e663c39640ce2f0033fc3590d886f2b571 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:05:36 +0000 Subject: [PATCH 06/14] feat(api): Task Groups v1 added to SDK --- .stats.yml | 8 +- api.md | 18 ++ scripts/detect-breaking-changes | 1 + src/client.ts | 37 ++- src/resources/beta/api.md | 6 +- src/resources/beta/beta.ts | 4 +- src/resources/beta/index.ts | 4 +- src/resources/beta/task-group.ts | 115 ++-------- src/resources/beta/task-run.ts | 2 +- src/resources/index.ts | 11 + src/resources/task-group.ts | 298 +++++++++++++++++++++++++ src/resources/task-run.ts | 2 +- tests/api-resources/task-group.test.ts | 150 +++++++++++++ 13 files changed, 541 insertions(+), 115 deletions(-) create mode 100644 src/resources/task-group.ts create mode 100644 tests/api-resources/task-group.test.ts diff --git a/.stats.yml b/.stats.yml index 2ea2dd0..cbce637 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-66ee13c3475d2c76f0956f258f0469903155b83ef02e839641be94cdc2014cf3.yml -openapi_spec_hash: 88af7b88725bead1f8ccdcaeb436fadb -config_hash: e17d82e9cb35004e5f9a9d3c4cf51aeb +configured_endpoints: 29 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-50b96a0c200c4be7554264406265a45af334f5e61ec30554fd9ef0fe0d63de92.yml +openapi_spec_hash: a46fe5664f6e27846f060276e765fddc +config_hash: 456111b9d3664d8dbb99018e7dc88488 diff --git a/api.md b/api.md index 482f31b..0fc6907 100644 --- a/api.md +++ b/api.md @@ -56,4 +56,22 @@ Methods: - client.taskRun.events(runID) -> TaskRunEventsResponse - client.taskRun.result(runID, { ...params }) -> TaskRunResult +# TaskGroup + +Types: + +- TaskGroup +- TaskGroupRunResponse +- TaskGroupStatus +- TaskGroupEventsResponse +- TaskGroupGetRunsResponse + +Methods: + +- client.taskGroup.create({ ...params }) -> TaskGroup +- client.taskGroup.retrieve(taskGroupID) -> TaskGroup +- client.taskGroup.addRuns(taskGroupID, { ...params }) -> TaskGroupRunResponse +- client.taskGroup.events(taskGroupID, { ...params }) -> TaskGroupEventsResponse +- client.taskGroup.getRuns(taskGroupID, { ...params }) -> TaskGroupGetRunsResponse + # [Beta](src/resources/beta/api.md) diff --git a/scripts/detect-breaking-changes b/scripts/detect-breaking-changes index dca3fbb..79b8b2e 100755 --- a/scripts/detect-breaking-changes +++ b/scripts/detect-breaking-changes @@ -9,6 +9,7 @@ echo "==> Detecting breaking changes" TEST_PATHS=( tests/api-resources/top-level.test.ts tests/api-resources/task-run.test.ts + tests/api-resources/task-group.test.ts tests/api-resources/beta/beta.test.ts tests/api-resources/beta/task-run.test.ts tests/api-resources/beta/task-group.test.ts diff --git a/src/client.ts b/src/client.ts index 551037d..945e5f4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -32,6 +32,17 @@ import { WebSearchResult, } from './resources/top-level'; import { APIPromise } from './core/api-promise'; +import { + TaskGroup, + TaskGroupAddRunsParams, + TaskGroupCreateParams, + TaskGroupEventsParams, + TaskGroupEventsResponse, + TaskGroupGetRunsParams, + TaskGroupGetRunsResponse, + TaskGroupRunResponse, + TaskGroupStatus, +} from './resources/task-group'; import { AutoSchema, Citation, @@ -812,13 +823,25 @@ export class Parallel { * - Output metadata: citations, excerpts, reasoning, and confidence per field * * Task Groups enable batch execution of many independent Task runs with group-level monitoring and failure handling. - * - Submit hundreds or thousands of Tasks as a single group + * - Submit hundreds or thousands of Tasks as a single group * - Observe group progress and receive results as they complete * - Real-time updates via Server-Sent Events (SSE) * - Add tasks to an existing group while it is running * - Group-level retry and error aggregation */ taskRun: API.TaskRun = new API.TaskRun(this); + /** + * The Task API executes web research and extraction tasks. Clients submit a natural-language objective with an optional input schema; the service plans retrieval, fetches relevant URLs, and returns outputs that conform to a provided or inferred JSON schema. Supports deep research style queries and can return rich structured JSON outputs. Processors trade-off between cost, latency, and quality. Each processor supports calibrated confidences. + * - Output metadata: citations, excerpts, reasoning, and confidence per field + * + * Task Groups enable batch execution of many independent Task runs with group-level monitoring and failure handling. + * - Submit hundreds or thousands of Tasks as a single group + * - Observe group progress and receive results as they complete + * - Real-time updates via Server-Sent Events (SSE) + * - Add tasks to an existing group while it is running + * - Group-level retry and error aggregation + */ + taskGroup: API.TaskGroup = new API.TaskGroup(this); beta: API.Beta = new API.Beta(this); } @@ -864,6 +887,18 @@ export declare namespace Parallel { type TaskRunResultParams as TaskRunResultParams, }; + export { + type TaskGroup as TaskGroup, + type TaskGroupRunResponse as TaskGroupRunResponse, + type TaskGroupStatus as TaskGroupStatus, + type TaskGroupEventsResponse as TaskGroupEventsResponse, + type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, + type TaskGroupCreateParams as TaskGroupCreateParams, + type TaskGroupAddRunsParams as TaskGroupAddRunsParams, + type TaskGroupEventsParams as TaskGroupEventsParams, + type TaskGroupGetRunsParams as TaskGroupGetRunsParams, + }; + export { Beta as Beta }; export type ErrorObject = API.ErrorObject; diff --git a/src/resources/beta/api.md b/src/resources/beta/api.md index 24adbf2..0dca07f 100644 --- a/src/resources/beta/api.md +++ b/src/resources/beta/api.md @@ -40,11 +40,11 @@ Methods: Types: -- TaskGroup -- TaskGroupRunResponse -- TaskGroupStatus - TaskGroupEventsResponse - TaskGroupGetRunsResponse +- TaskGroupStatus +- TaskGroup +- TaskGroupRunResponse Methods: diff --git a/src/resources/beta/beta.ts b/src/resources/beta/beta.ts index b889a8e..dc5bae4 100644 --- a/src/resources/beta/beta.ts +++ b/src/resources/beta/beta.ts @@ -425,10 +425,10 @@ export declare namespace Beta { export { type TaskGroup as TaskGroup, - type TaskGroupRunResponse as TaskGroupRunResponse, - type TaskGroupStatus as TaskGroupStatus, type TaskGroupEventsResponse as TaskGroupEventsResponse, type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, + type TaskGroupStatus as TaskGroupStatus, + type TaskGroupRunResponse as TaskGroupRunResponse, type TaskGroupCreateParams as TaskGroupCreateParams, type TaskGroupAddRunsParams as TaskGroupAddRunsParams, type TaskGroupEventsParams as TaskGroupEventsParams, diff --git a/src/resources/beta/index.ts b/src/resources/beta/index.ts index 9681d32..6367d0a 100644 --- a/src/resources/beta/index.ts +++ b/src/resources/beta/index.ts @@ -52,10 +52,10 @@ export { } from './findall'; export { TaskGroup, - type TaskGroupRunResponse, - type TaskGroupStatus, type TaskGroupEventsResponse, type TaskGroupGetRunsResponse, + type TaskGroupStatus, + type TaskGroupRunResponse, type TaskGroupCreateParams, type TaskGroupAddRunsParams, type TaskGroupEventsParams, diff --git a/src/resources/beta/task-group.ts b/src/resources/beta/task-group.ts index 7c642c3..ff64ba7 100644 --- a/src/resources/beta/task-group.ts +++ b/src/resources/beta/task-group.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../core/resource'; -import * as TaskGroupAPI from './task-group'; +import * as TaskGroupAPI from '../task-group'; import * as TaskRunAPI from '../task-run'; import * as BetaTaskRunAPI from './task-run'; import { APIPromise } from '../../core/api-promise'; @@ -11,15 +11,7 @@ import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; /** - * The Task API executes web research and extraction tasks. Clients submit a natural-language objective with an optional input schema; the service plans retrieval, fetches relevant URLs, and returns outputs that conform to a provided or inferred JSON schema. Supports deep research style queries and can return rich structured JSON outputs. Processors trade-off between cost, latency, and quality. Each processor supports calibrated confidences. - * - Output metadata: citations, excerpts, reasoning, and confidence per field - * - * Task Groups enable batch execution of many independent Task runs with group-level monitoring and failure handling. - * - Submit hundreds or thousands of Tasks as a single group - * - Observe group progress and receive results as they complete - * - Real-time updates via Server-Sent Events (SSE) - * - Add tasks to an existing group while it is running - * - Group-level retry and error aggregation + * Tasks (Beta) */ export class TaskGroup extends APIResource { /** @@ -30,7 +22,7 @@ export class TaskGroup extends APIResource { * const taskGroup = await client.beta.taskGroup.create(); * ``` */ - create(body: TaskGroupCreateParams, options?: RequestOptions): APIPromise { + create(body: TaskGroupCreateParams, options?: RequestOptions): APIPromise { return this._client.post('/v1beta/tasks/groups', { body, ...options, @@ -48,7 +40,7 @@ export class TaskGroup extends APIResource { * ); * ``` */ - retrieve(taskGroupID: string, options?: RequestOptions): APIPromise { + retrieve(taskGroupID: string, options?: RequestOptions): APIPromise { return this._client.get(path`/v1beta/tasks/groups/${taskGroupID}`, { ...options, headers: buildHeaders([{ 'parallel-beta': 'search-extract-2025-10-10' }, options?.headers]), @@ -75,7 +67,7 @@ export class TaskGroup extends APIResource { taskGroupID: string, params: TaskGroupAddRunsParams, options?: RequestOptions, - ): APIPromise { + ): APIPromise { const { refresh_status, betas, ...body } = params; return this._client.post(path`/v1beta/tasks/groups/${taskGroupID}/runs`, { query: { refresh_status }, @@ -153,91 +145,6 @@ export class TaskGroup extends APIResource { } } -/** - * Response object for a task group, including its status and metadata. - */ -export interface TaskGroup { - /** - * Timestamp of the creation of the group, as an RFC 3339 string. - */ - created_at: string | null; - - /** - * Status of the group. - */ - status: TaskGroupStatus; - - /** - * ID of the group. - */ - taskgroup_id: string; - - /** - * User-provided metadata stored with the group. - */ - metadata?: { [key: string]: string | number | boolean } | null; -} - -/** - * Response from adding new task runs to a task group. - */ -export interface TaskGroupRunResponse { - /** - * Cursor for these runs in the event stream at - * taskgroup/events?last_event_id=. Empty for the first runs in the - * group. - */ - event_cursor: string | null; - - /** - * Cursor for these runs in the run stream at - * taskgroup/runs?last_event_id=. Empty for the first runs in the - * group. - */ - run_cursor: string | null; - - /** - * IDs of the newly created runs. - */ - run_ids: Array; - - /** - * Status of the group. - */ - status: TaskGroupStatus; -} - -/** - * Status of a task group. - */ -export interface TaskGroupStatus { - /** - * True if at least one run in the group is currently active, i.e. status is one of - * {'cancelling', 'queued', 'running'}. - */ - is_active: boolean; - - /** - * Timestamp of the last status update to the group, as an RFC 3339 string. - */ - modified_at: string | null; - - /** - * Number of task runs in the group. - */ - num_task_runs: number; - - /** - * Human-readable status message for the group. - */ - status_message: string | null; - - /** - * Number of task runs with each status. - */ - task_run_status_counts: { [key: string]: number }; -} - /** * Event indicating an update to group status. */ @@ -275,6 +182,12 @@ export namespace TaskGroupEventsResponse { */ export type TaskGroupGetRunsResponse = TaskRunAPI.TaskRunEvent | TaskRunAPI.ErrorEvent; +export type TaskGroupStatus = TaskGroupAPI.TaskGroupStatus; + +export type TaskGroup = TaskGroupAPI.TaskGroup; + +export type TaskGroupRunResponse = TaskGroupAPI.TaskGroupRunResponse; + export interface TaskGroupCreateParams { /** * User-provided metadata stored with the task group. @@ -337,11 +250,11 @@ export interface TaskGroupGetRunsParams { export declare namespace TaskGroup { export { - type TaskGroup as TaskGroup, - type TaskGroupRunResponse as TaskGroupRunResponse, - type TaskGroupStatus as TaskGroupStatus, type TaskGroupEventsResponse as TaskGroupEventsResponse, type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, + type TaskGroupStatus as TaskGroupStatus, + type TaskGroup as TaskGroup, + type TaskGroupRunResponse as TaskGroupRunResponse, type TaskGroupCreateParams as TaskGroupCreateParams, type TaskGroupAddRunsParams as TaskGroupAddRunsParams, type TaskGroupEventsParams as TaskGroupEventsParams, diff --git a/src/resources/beta/task-run.ts b/src/resources/beta/task-run.ts index 4b83fe1..258c7ae 100644 --- a/src/resources/beta/task-run.ts +++ b/src/resources/beta/task-run.ts @@ -14,7 +14,7 @@ import { path } from '../../internal/utils/path'; * - Output metadata: citations, excerpts, reasoning, and confidence per field * * Task Groups enable batch execution of many independent Task runs with group-level monitoring and failure handling. - * - Submit hundreds or thousands of Tasks as a single group + * - Submit hundreds or thousands of Tasks as a single group * - Observe group progress and receive results as they complete * - Real-time updates via Server-Sent Events (SSE) * - Add tasks to an existing group while it is running diff --git a/src/resources/index.ts b/src/resources/index.ts index c7e6560..1a44a3b 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -2,6 +2,17 @@ export * from './shared'; export { Beta } from './beta/beta'; +export { + TaskGroup, + type TaskGroupRunResponse, + type TaskGroupStatus, + type TaskGroupEventsResponse, + type TaskGroupGetRunsResponse, + type TaskGroupCreateParams, + type TaskGroupAddRunsParams, + type TaskGroupEventsParams, + type TaskGroupGetRunsParams, +} from './task-group'; export { TaskRun, type AutoSchema, diff --git a/src/resources/task-group.ts b/src/resources/task-group.ts new file mode 100644 index 0000000..78ad0f7 --- /dev/null +++ b/src/resources/task-group.ts @@ -0,0 +1,298 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../core/resource'; +import * as TaskGroupAPI from './task-group'; +import * as TaskRunAPI from './task-run'; +import * as BetaTaskRunAPI from './beta/task-run'; +import { APIPromise } from '../core/api-promise'; +import { Stream } from '../core/streaming'; +import { buildHeaders } from '../internal/headers'; +import { RequestOptions } from '../internal/request-options'; +import { path } from '../internal/utils/path'; + +/** + * The Task API executes web research and extraction tasks. Clients submit a natural-language objective with an optional input schema; the service plans retrieval, fetches relevant URLs, and returns outputs that conform to a provided or inferred JSON schema. Supports deep research style queries and can return rich structured JSON outputs. Processors trade-off between cost, latency, and quality. Each processor supports calibrated confidences. + * - Output metadata: citations, excerpts, reasoning, and confidence per field + * + * Task Groups enable batch execution of many independent Task runs with group-level monitoring and failure handling. + * - Submit hundreds or thousands of Tasks as a single group + * - Observe group progress and receive results as they complete + * - Real-time updates via Server-Sent Events (SSE) + * - Add tasks to an existing group while it is running + * - Group-level retry and error aggregation + */ +export class TaskGroup extends APIResource { + /** + * Initiates a TaskGroup to group and track multiple runs. + */ + create(body: TaskGroupCreateParams, options?: RequestOptions): APIPromise { + return this._client.post('/v1/tasks/groups', { body, ...options }); + } + + /** + * Retrieves aggregated status across runs in a TaskGroup. + */ + retrieve(taskGroupID: string, options?: RequestOptions): APIPromise { + return this._client.get(path`/v1/tasks/groups/${taskGroupID}`, options); + } + + /** + * Initiates multiple task runs within a TaskGroup. + */ + addRuns( + taskGroupID: string, + params: TaskGroupAddRunsParams, + options?: RequestOptions, + ): APIPromise { + const { refresh_status, betas, ...body } = params; + return this._client.post(path`/v1/tasks/groups/${taskGroupID}/runs`, { + query: { refresh_status }, + body, + ...options, + headers: buildHeaders([ + { ...(betas?.toString() != null ? { 'parallel-beta': betas?.toString() } : undefined) }, + options?.headers, + ]), + }); + } + + /** + * Streams events from a TaskGroup: status updates and run completions. + * + * The connection will remain open for up to an hour as long as at least one run in + * the group is still active. + */ + events( + taskGroupID: string, + query: TaskGroupEventsParams | undefined = {}, + options?: RequestOptions, + ): APIPromise> { + return this._client.get(path`/v1/tasks/groups/${taskGroupID}/events`, { + query, + ...options, + headers: buildHeaders([{ Accept: 'text/event-stream' }, options?.headers]), + stream: true, + }) as APIPromise>; + } + + /** + * Retrieves task runs in a TaskGroup and optionally their inputs and outputs. + * + * All runs within a TaskGroup are returned as a stream. To get the inputs and/or + * outputs back in the stream, set the corresponding `include_input` and + * `include_output` parameters to `true`. + * + * The stream is resumable using the `event_id` as the cursor. To resume a stream, + * specify the `last_event_id` parameter with the `event_id` of the last event in + * the stream. The stream will resume from the next event after the + * `last_event_id`. + */ + getRuns( + taskGroupID: string, + query: TaskGroupGetRunsParams | undefined = {}, + options?: RequestOptions, + ): APIPromise> { + return this._client.get(path`/v1/tasks/groups/${taskGroupID}/runs`, { + query, + ...options, + headers: buildHeaders([{ Accept: 'text/event-stream' }, options?.headers]), + stream: true, + }) as APIPromise>; + } +} + +/** + * Response object for a task group, including its status and metadata. + */ +export interface TaskGroup { + /** + * Timestamp of the creation of the group, as an RFC 3339 string. + */ + created_at: string | null; + + /** + * Status of the group. + */ + status: TaskGroupStatus; + + /** + * ID of the group. + */ + taskgroup_id: string; + + /** + * User-provided metadata stored with the group. + */ + metadata?: { [key: string]: string | number | boolean } | null; +} + +/** + * Response from adding new task runs to a task group. + */ +export interface TaskGroupRunResponse { + /** + * Cursor for these runs in the event stream at + * taskgroup/events?last_event_id=. Empty for the first runs in the + * group. + */ + event_cursor: string | null; + + /** + * Cursor for these runs in the run stream at + * taskgroup/runs?last_event_id=. Empty for the first runs in the + * group. + */ + run_cursor: string | null; + + /** + * IDs of the newly created runs. + */ + run_ids: Array; + + /** + * Status of the group. + */ + status: TaskGroupStatus; +} + +/** + * Status of a task group. + */ +export interface TaskGroupStatus { + /** + * True if at least one run in the group is currently active, i.e. status is one of + * {'cancelling', 'queued', 'running'}. + */ + is_active: boolean; + + /** + * Timestamp of the last status update to the group, as an RFC 3339 string. + */ + modified_at: string | null; + + /** + * Number of task runs in the group. + */ + num_task_runs: number; + + /** + * Human-readable status message for the group. + */ + status_message: string | null; + + /** + * Number of task runs with each status. + */ + task_run_status_counts: { [key: string]: number }; +} + +/** + * Event indicating an update to group status. + */ +export type TaskGroupEventsResponse = + | TaskGroupEventsResponse.TaskGroupStatusEvent + | TaskRunAPI.TaskRunEvent + | TaskRunAPI.ErrorEvent; + +export namespace TaskGroupEventsResponse { + /** + * Event indicating an update to group status. + */ + export interface TaskGroupStatusEvent { + /** + * Cursor to resume the event stream. + */ + event_id: string; + + /** + * Task group status object. + */ + status: TaskGroupAPI.TaskGroupStatus; + + /** + * Event type; always 'task_group_status'. + */ + type: 'task_group_status'; + } +} + +/** + * Event when a task run transitions to a non-active status. + * + * May indicate completion, cancellation, or failure. + */ +export type TaskGroupGetRunsResponse = TaskRunAPI.TaskRunEvent | TaskRunAPI.ErrorEvent; + +export interface TaskGroupCreateParams { + /** + * User-provided metadata stored with the task group. + */ + metadata?: { [key: string]: string | number | boolean } | null; +} + +export interface TaskGroupAddRunsParams { + /** + * Body param: List of task runs to execute. Up to 1,000 runs can be specified per + * request. If you'd like to add more runs, split them across multiple TaskGroup + * POST requests. + */ + inputs: Array; + + /** + * Query param + */ + refresh_status?: boolean; + + /** + * Body param: Specification for a task. + * + * Auto output schemas can be specified by setting `output_schema={"type":"auto"}`. + * Not specifying a TaskSpec is the same as setting an auto output schema. + * + * For convenience bare strings are also accepted as input or output schemas. + */ + default_task_spec?: TaskRunAPI.TaskSpec | null; + + /** + * Header param: Optional header to specify the beta version(s) to enable. + */ + betas?: Array; +} + +export interface TaskGroupEventsParams { + last_event_id?: string | null; + + timeout?: number | null; +} + +export interface TaskGroupGetRunsParams { + include_input?: boolean; + + include_output?: boolean; + + last_event_id?: string | null; + + status?: + | 'queued' + | 'action_required' + | 'running' + | 'completed' + | 'failed' + | 'cancelling' + | 'cancelled' + | null; +} + +export declare namespace TaskGroup { + export { + type TaskGroup as TaskGroup, + type TaskGroupRunResponse as TaskGroupRunResponse, + type TaskGroupStatus as TaskGroupStatus, + type TaskGroupEventsResponse as TaskGroupEventsResponse, + type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, + type TaskGroupCreateParams as TaskGroupCreateParams, + type TaskGroupAddRunsParams as TaskGroupAddRunsParams, + type TaskGroupEventsParams as TaskGroupEventsParams, + type TaskGroupGetRunsParams as TaskGroupGetRunsParams, + }; +} diff --git a/src/resources/task-run.ts b/src/resources/task-run.ts index 9399fc4..b13cfd2 100644 --- a/src/resources/task-run.ts +++ b/src/resources/task-run.ts @@ -14,7 +14,7 @@ import { path } from '../internal/utils/path'; * - Output metadata: citations, excerpts, reasoning, and confidence per field * * Task Groups enable batch execution of many independent Task runs with group-level monitoring and failure handling. - * - Submit hundreds or thousands of Tasks as a single group + * - Submit hundreds or thousands of Tasks as a single group * - Observe group progress and receive results as they complete * - Real-time updates via Server-Sent Events (SSE) * - Add tasks to an existing group while it is running diff --git a/tests/api-resources/task-group.test.ts b/tests/api-resources/task-group.test.ts new file mode 100644 index 0000000..b0ad4d9 --- /dev/null +++ b/tests/api-resources/task-group.test.ts @@ -0,0 +1,150 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import Parallel from 'parallel-web'; + +const client = new Parallel({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource taskGroup', () => { + test('create', async () => { + const responsePromise = client.taskGroup.create({}); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('retrieve', async () => { + const responsePromise = client.taskGroup.retrieve('taskgroup_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('addRuns: only required params', async () => { + const responsePromise = client.taskGroup.addRuns('taskgroup_id', { + inputs: [{ input: 'What was the GDP of France in 2023?', processor: 'base' }], + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('addRuns: required and optional params', async () => { + const response = await client.taskGroup.addRuns('taskgroup_id', { + inputs: [ + { + input: 'What was the GDP of France in 2023?', + processor: 'base', + advanced_settings: { location: 'us' }, + enable_events: true, + mcp_servers: [ + { + name: 'name', + url: 'url', + allowed_tools: ['string'], + headers: { foo: 'string' }, + type: 'url', + }, + ], + metadata: { foo: 'string' }, + previous_interaction_id: 'previous_interaction_id', + source_policy: { + after_date: '2024-01-01', + exclude_domains: ['reddit.com', 'x.com', '.ai'], + include_domains: ['wikipedia.org', 'usa.gov', '.edu'], + }, + task_spec: { + output_schema: { + json_schema: { + additionalProperties: 'bar', + properties: 'bar', + required: 'bar', + type: 'bar', + }, + type: 'json', + }, + input_schema: 'string', + }, + webhook: { url: 'url', event_types: ['task_run.status'] }, + }, + ], + refresh_status: true, + default_task_spec: { + output_schema: { + json_schema: { + additionalProperties: 'bar', + properties: 'bar', + required: 'bar', + type: 'bar', + }, + type: 'json', + }, + input_schema: 'string', + }, + betas: ['mcp-server-2025-07-17'], + }); + }); + + test('events', async () => { + const responsePromise = client.taskGroup.events('taskgroup_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('events: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.taskGroup.events( + 'taskgroup_id', + { last_event_id: 'last_event_id', timeout: 0 }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(Parallel.NotFoundError); + }); + + test('getRuns', async () => { + const responsePromise = client.taskGroup.getRuns('taskgroup_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('getRuns: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.taskGroup.getRuns( + 'taskgroup_id', + { + include_input: true, + include_output: true, + last_event_id: 'last_event_id', + status: 'queued', + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(Parallel.NotFoundError); + }); +}); From 9402893db2583d6d60dbbe61f110db3f7422cd59 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 04:09:26 +0000 Subject: [PATCH 07/14] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cbce637..6863893 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-50b96a0c200c4be7554264406265a45af334f5e61ec30554fd9ef0fe0d63de92.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-f886afe5dd04f4e33caf809b02945ca92fd22a7d297f7815444a41404b0b545f.yml openapi_spec_hash: a46fe5664f6e27846f060276e765fddc config_hash: 456111b9d3664d8dbb99018e7dc88488 From 98f915c996abb163d8a79698fa57259bae3b0584 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 03:15:08 +0000 Subject: [PATCH 08/14] feat(api): manual updates --- .stats.yml | 8 +- api.md | 40 + scripts/detect-breaking-changes | 1 + src/client.ts | 75 +- src/internal/qs/LICENSE.md | 13 + src/internal/qs/README.md | 3 + src/internal/qs/formats.ts | 10 + src/internal/qs/index.ts | 13 + src/internal/qs/stringify.ts | 385 ++++ src/internal/qs/types.ts | 71 + src/internal/qs/utils.ts | 265 +++ src/internal/utils/query.ts | 20 +- src/resources/beta/api.md | 7 +- src/resources/beta/beta.ts | 32 +- src/resources/beta/findall.ts | 301 ++-- src/resources/beta/index.ts | 5 + src/resources/beta/task-group.ts | 28 +- src/resources/beta/task-run.ts | 88 +- src/resources/index.ts | 31 + src/resources/monitor.ts | 752 ++++++++ src/resources/task-group.ts | 63 +- src/resources/task-run.ts | 188 +- src/resources/top-level.ts | 29 +- tests/api-resources/monitor.test.ts | 153 ++ tests/api-resources/task-group.test.ts | 15 + tests/api-resources/task-run.test.ts | 11 + tests/qs/empty-keys-cases.ts | 271 +++ tests/qs/stringify.test.ts | 2232 ++++++++++++++++++++++++ tests/qs/utils.test.ts | 169 ++ tests/stringifyQuery.test.ts | 6 - 30 files changed, 4803 insertions(+), 482 deletions(-) create mode 100644 src/internal/qs/LICENSE.md create mode 100644 src/internal/qs/README.md create mode 100644 src/internal/qs/formats.ts create mode 100644 src/internal/qs/index.ts create mode 100644 src/internal/qs/stringify.ts create mode 100644 src/internal/qs/types.ts create mode 100644 src/internal/qs/utils.ts create mode 100644 src/resources/monitor.ts create mode 100644 tests/api-resources/monitor.test.ts create mode 100644 tests/qs/empty-keys-cases.ts create mode 100644 tests/qs/stringify.test.ts create mode 100644 tests/qs/utils.test.ts diff --git a/.stats.yml b/.stats.yml index 6863893..be05b09 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-f886afe5dd04f4e33caf809b02945ca92fd22a7d297f7815444a41404b0b545f.yml -openapi_spec_hash: a46fe5664f6e27846f060276e765fddc -config_hash: 456111b9d3664d8dbb99018e7dc88488 +configured_endpoints: 38 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-6b946abf6bef7f85a11824bfb1298c2887979f309873eb3ab94eee76dd6668ca.yml +openapi_spec_hash: 688facba9b1f3e19785c0ca88953e1c9 +config_hash: 0b93697d62ec2afc2b9ed854621dffe7 diff --git a/api.md b/api.md index 0fc6907..22538c0 100644 --- a/api.md +++ b/api.md @@ -9,6 +9,7 @@ Types: - ExtractResponse - ExtractResult - FetchPolicy +- FullContentSettings - SearchResult - UsageItem - WebSearchResult @@ -39,10 +40,14 @@ Types: - McpServer - McpToolCall - RunInput +- TaskAdvancedSettings - TaskRun - TaskRunEvent - TaskRunJsonOutput +- TaskRunProgressMessageEvent +- TaskRunProgressStatsEvent - TaskRunResult +- TaskRunSourceStats - TaskRunTextOutput - TaskSpec - TextSchema @@ -55,6 +60,7 @@ Methods: - client.taskRun.retrieve(runID) -> TaskRun - client.taskRun.events(runID) -> TaskRunEventsResponse - client.taskRun.result(runID, { ...params }) -> TaskRunResult +- client.taskRun.retrieveInput(runID) -> RunInput # TaskGroup @@ -63,6 +69,7 @@ Types: - TaskGroup - TaskGroupRunResponse - TaskGroupStatus +- TaskGroupStatusEvent - TaskGroupEventsResponse - TaskGroupGetRunsResponse @@ -73,5 +80,38 @@ Methods: - client.taskGroup.addRuns(taskGroupID, { ...params }) -> TaskGroupRunResponse - client.taskGroup.events(taskGroupID, { ...params }) -> TaskGroupEventsResponse - client.taskGroup.getRuns(taskGroupID, { ...params }) -> TaskGroupGetRunsResponse +- client.taskGroup.retrieveRun(runID, { ...params }) -> TaskRun + +# Monitor + +Types: + +- AdvancedMonitorSettings +- CreateMonitorRequest +- Monitor +- MonitorCompletionEvent +- MonitorErrorEvent +- MonitorEventStreamEvent +- MonitorEventStreamResponseSettings +- MonitorEventStreamSettings +- MonitorSnapshotEvent +- MonitorSnapshotOutput +- MonitorSnapshotResponseSettings +- MonitorSnapshotSettings +- MonitorWebhook +- PaginatedMonitorEvents +- PaginatedMonitorResponse +- UpdateMonitorEventStreamSettings +- UpdateMonitorRequest + +Methods: + +- client.monitor.create({ ...params }) -> Monitor +- client.monitor.retrieve(monitorID) -> Monitor +- client.monitor.update(monitorID, { ...params }) -> Monitor +- client.monitor.list({ ...params }) -> PaginatedMonitorResponse +- client.monitor.cancel(monitorID) -> Monitor +- client.monitor.events(monitorID, { ...params }) -> PaginatedMonitorEvents +- client.monitor.trigger(monitorID) -> void # [Beta](src/resources/beta/api.md) diff --git a/scripts/detect-breaking-changes b/scripts/detect-breaking-changes index 79b8b2e..0eb1c56 100755 --- a/scripts/detect-breaking-changes +++ b/scripts/detect-breaking-changes @@ -10,6 +10,7 @@ TEST_PATHS=( tests/api-resources/top-level.test.ts tests/api-resources/task-run.test.ts tests/api-resources/task-group.test.ts + tests/api-resources/monitor.test.ts tests/api-resources/beta/beta.test.ts tests/api-resources/beta/task-run.test.ts tests/api-resources/beta/task-group.test.ts diff --git a/src/client.ts b/src/client.ts index 945e5f4..b216d9a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -26,12 +26,37 @@ import { ExtractResponse, ExtractResult, FetchPolicy, + FullContentSettings, SearchParams, SearchResult, UsageItem, WebSearchResult, } from './resources/top-level'; import { APIPromise } from './core/api-promise'; +import { + AdvancedMonitorSettings, + CreateMonitorRequest, + Monitor, + MonitorCompletionEvent, + MonitorCreateParams, + MonitorErrorEvent, + MonitorEventStreamEvent, + MonitorEventStreamResponseSettings, + MonitorEventStreamSettings, + MonitorEventsParams, + MonitorListParams, + MonitorResource, + MonitorSnapshotEvent, + MonitorSnapshotOutput, + MonitorSnapshotResponseSettings, + MonitorSnapshotSettings, + MonitorUpdateParams, + MonitorWebhook, + PaginatedMonitorEvents, + PaginatedMonitorResponse, + UpdateMonitorEventStreamSettings, + UpdateMonitorRequest, +} from './resources/monitor'; import { TaskGroup, TaskGroupAddRunsParams, @@ -40,8 +65,10 @@ import { TaskGroupEventsResponse, TaskGroupGetRunsParams, TaskGroupGetRunsResponse, + TaskGroupRetrieveRunParams, TaskGroupRunResponse, TaskGroupStatus, + TaskGroupStatusEvent, } from './resources/task-group'; import { AutoSchema, @@ -52,13 +79,17 @@ import { McpServer, McpToolCall, RunInput, + TaskAdvancedSettings, TaskRun, TaskRunCreateParams, TaskRunEvent, TaskRunEventsResponse, TaskRunJsonOutput, + TaskRunProgressMessageEvent, + TaskRunProgressStatsEvent, TaskRunResult, TaskRunResultParams, + TaskRunSourceStats, TaskRunTextOutput, TaskSpec, TextSchema, @@ -311,9 +342,6 @@ export class Parallel { return buildHeaders([{ 'x-api-key': this.apiKey }]); } - /** - * Basic re-implementation of `qs.stringify` for primitive types. - */ protected stringifyQuery(query: object | Record): string { return stringifyQuery(query); } @@ -842,9 +870,18 @@ export class Parallel { * - Group-level retry and error aggregation */ taskGroup: API.TaskGroup = new API.TaskGroup(this); + /** + * The Monitor API watches the web for material changes on a fixed frequency. Each monitor runs once on creation and then on its configured schedule, emitting events when meaningful changes are detected. + * - `event_stream` monitors track a search query and emit an event for each new material change. + * - `snapshot` monitors track a specific task run's output and emit an event when the output changes. + * + * Results can be polled via the events endpoint or delivered via webhooks. + */ + monitor: API.MonitorResource = new API.MonitorResource(this); beta: API.Beta = new API.Beta(this); } +Parallel.MonitorResource = MonitorResource; Parallel.Beta = Beta; export declare namespace Parallel { @@ -858,6 +895,7 @@ export declare namespace Parallel { type ExtractResponse as ExtractResponse, type ExtractResult as ExtractResult, type FetchPolicy as FetchPolicy, + type FullContentSettings as FullContentSettings, type SearchResult as SearchResult, type UsageItem as UsageItem, type WebSearchResult as WebSearchResult, @@ -875,9 +913,13 @@ export declare namespace Parallel { type McpServer as McpServer, type McpToolCall as McpToolCall, type RunInput as RunInput, + type TaskAdvancedSettings as TaskAdvancedSettings, type TaskRunEvent as TaskRunEvent, type TaskRunJsonOutput as TaskRunJsonOutput, + type TaskRunProgressMessageEvent as TaskRunProgressMessageEvent, + type TaskRunProgressStatsEvent as TaskRunProgressStatsEvent, type TaskRunResult as TaskRunResult, + type TaskRunSourceStats as TaskRunSourceStats, type TaskRunTextOutput as TaskRunTextOutput, type TaskSpec as TaskSpec, type TextSchema as TextSchema, @@ -891,12 +933,39 @@ export declare namespace Parallel { type TaskGroup as TaskGroup, type TaskGroupRunResponse as TaskGroupRunResponse, type TaskGroupStatus as TaskGroupStatus, + type TaskGroupStatusEvent as TaskGroupStatusEvent, type TaskGroupEventsResponse as TaskGroupEventsResponse, type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, type TaskGroupCreateParams as TaskGroupCreateParams, type TaskGroupAddRunsParams as TaskGroupAddRunsParams, type TaskGroupEventsParams as TaskGroupEventsParams, type TaskGroupGetRunsParams as TaskGroupGetRunsParams, + type TaskGroupRetrieveRunParams as TaskGroupRetrieveRunParams, + }; + + export { + MonitorResource as MonitorResource, + type AdvancedMonitorSettings as AdvancedMonitorSettings, + type CreateMonitorRequest as CreateMonitorRequest, + type Monitor as Monitor, + type MonitorCompletionEvent as MonitorCompletionEvent, + type MonitorErrorEvent as MonitorErrorEvent, + type MonitorEventStreamEvent as MonitorEventStreamEvent, + type MonitorEventStreamResponseSettings as MonitorEventStreamResponseSettings, + type MonitorEventStreamSettings as MonitorEventStreamSettings, + type MonitorSnapshotEvent as MonitorSnapshotEvent, + type MonitorSnapshotOutput as MonitorSnapshotOutput, + type MonitorSnapshotResponseSettings as MonitorSnapshotResponseSettings, + type MonitorSnapshotSettings as MonitorSnapshotSettings, + type MonitorWebhook as MonitorWebhook, + type PaginatedMonitorEvents as PaginatedMonitorEvents, + type PaginatedMonitorResponse as PaginatedMonitorResponse, + type UpdateMonitorEventStreamSettings as UpdateMonitorEventStreamSettings, + type UpdateMonitorRequest as UpdateMonitorRequest, + type MonitorCreateParams as MonitorCreateParams, + type MonitorUpdateParams as MonitorUpdateParams, + type MonitorListParams as MonitorListParams, + type MonitorEventsParams as MonitorEventsParams, }; export { Beta as Beta }; diff --git a/src/internal/qs/LICENSE.md b/src/internal/qs/LICENSE.md new file mode 100644 index 0000000..3fda157 --- /dev/null +++ b/src/internal/qs/LICENSE.md @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/puruvj/neoqs/graphs/contributors) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/internal/qs/README.md b/src/internal/qs/README.md new file mode 100644 index 0000000..67ae04e --- /dev/null +++ b/src/internal/qs/README.md @@ -0,0 +1,3 @@ +# qs + +This is a vendored version of [neoqs](https://github.com/PuruVJ/neoqs) which is a TypeScript rewrite of [qs](https://github.com/ljharb/qs), a query string library. diff --git a/src/internal/qs/formats.ts b/src/internal/qs/formats.ts new file mode 100644 index 0000000..e76a742 --- /dev/null +++ b/src/internal/qs/formats.ts @@ -0,0 +1,10 @@ +import type { Format } from './types'; + +export const default_format: Format = 'RFC3986'; +export const default_formatter = (v: PropertyKey) => String(v); +export const formatters: Record string> = { + RFC1738: (v: PropertyKey) => String(v).replace(/%20/g, '+'), + RFC3986: default_formatter, +}; +export const RFC1738 = 'RFC1738'; +export const RFC3986 = 'RFC3986'; diff --git a/src/internal/qs/index.ts b/src/internal/qs/index.ts new file mode 100644 index 0000000..c3a3620 --- /dev/null +++ b/src/internal/qs/index.ts @@ -0,0 +1,13 @@ +import { default_format, formatters, RFC1738, RFC3986 } from './formats'; + +const formats = { + formatters, + RFC1738, + RFC3986, + default: default_format, +}; + +export { stringify } from './stringify'; +export { formats }; + +export type { DefaultDecoder, DefaultEncoder, Format, ParseOptions, StringifyOptions } from './types'; diff --git a/src/internal/qs/stringify.ts b/src/internal/qs/stringify.ts new file mode 100644 index 0000000..7e71387 --- /dev/null +++ b/src/internal/qs/stringify.ts @@ -0,0 +1,385 @@ +import { encode, is_buffer, maybe_map, has } from './utils'; +import { default_format, default_formatter, formatters } from './formats'; +import type { NonNullableProperties, StringifyOptions } from './types'; +import { isArray } from '../utils/values'; + +const array_prefix_generators = { + brackets(prefix: PropertyKey) { + return String(prefix) + '[]'; + }, + comma: 'comma', + indices(prefix: PropertyKey, key: string) { + return String(prefix) + '[' + key + ']'; + }, + repeat(prefix: PropertyKey) { + return String(prefix); + }, +}; + +const push_to_array = function (arr: any[], value_or_array: any) { + Array.prototype.push.apply(arr, isArray(value_or_array) ? value_or_array : [value_or_array]); +}; + +let toISOString; + +const defaults = { + addQueryPrefix: false, + allowDots: false, + allowEmptyArrays: false, + arrayFormat: 'indices', + charset: 'utf-8', + charsetSentinel: false, + delimiter: '&', + encode: true, + encodeDotInKeys: false, + encoder: encode, + encodeValuesOnly: false, + format: default_format, + formatter: default_formatter, + /** @deprecated */ + indices: false, + serializeDate(date) { + return (toISOString ??= Function.prototype.call.bind(Date.prototype.toISOString))(date); + }, + skipNulls: false, + strictNullHandling: false, +} as NonNullableProperties; + +function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint { + return ( + typeof v === 'string' || + typeof v === 'number' || + typeof v === 'boolean' || + typeof v === 'symbol' || + typeof v === 'bigint' + ); +} + +const sentinel = {}; + +function inner_stringify( + object: any, + prefix: PropertyKey, + generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string), + commaRoundTrip: boolean, + allowEmptyArrays: boolean, + strictNullHandling: boolean, + skipNulls: boolean, + encodeDotInKeys: boolean, + encoder: StringifyOptions['encoder'], + filter: StringifyOptions['filter'], + sort: StringifyOptions['sort'], + allowDots: StringifyOptions['allowDots'], + serializeDate: StringifyOptions['serializeDate'], + format: StringifyOptions['format'], + formatter: StringifyOptions['formatter'], + encodeValuesOnly: boolean, + charset: StringifyOptions['charset'], + sideChannel: WeakMap, +) { + let obj = object; + + let tmp_sc = sideChannel; + let step = 0; + let find_flag = false; + while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) { + // Where object last appeared in the ref tree + const pos = tmp_sc.get(object); + step += 1; + if (typeof pos !== 'undefined') { + if (pos === step) { + throw new RangeError('Cyclic object value'); + } else { + find_flag = true; // Break while + } + } + if (typeof tmp_sc.get(sentinel) === 'undefined') { + step = 0; + } + } + + if (typeof filter === 'function') { + obj = filter(prefix, obj); + } else if (obj instanceof Date) { + obj = serializeDate?.(obj); + } else if (generateArrayPrefix === 'comma' && isArray(obj)) { + obj = maybe_map(obj, function (value) { + if (value instanceof Date) { + return serializeDate?.(value); + } + return value; + }); + } + + if (obj === null) { + if (strictNullHandling) { + return encoder && !encodeValuesOnly ? + // @ts-expect-error + encoder(prefix, defaults.encoder, charset, 'key', format) + : prefix; + } + + obj = ''; + } + + if (is_non_nullish_primitive(obj) || is_buffer(obj)) { + if (encoder) { + const key_value = + encodeValuesOnly ? prefix + // @ts-expect-error + : encoder(prefix, defaults.encoder, charset, 'key', format); + return [ + formatter?.(key_value) + + '=' + + // @ts-expect-error + formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)), + ]; + } + return [formatter?.(prefix) + '=' + formatter?.(String(obj))]; + } + + const values: string[] = []; + + if (typeof obj === 'undefined') { + return values; + } + + let obj_keys; + if (generateArrayPrefix === 'comma' && isArray(obj)) { + // we need to join elements in + if (encodeValuesOnly && encoder) { + // @ts-expect-error values only + obj = maybe_map(obj, encoder); + } + obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }]; + } else if (isArray(filter)) { + obj_keys = filter; + } else { + const keys = Object.keys(obj); + obj_keys = sort ? keys.sort(sort) : keys; + } + + const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix); + + const adjusted_prefix = + commaRoundTrip && isArray(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix; + + if (allowEmptyArrays && isArray(obj) && obj.length === 0) { + return adjusted_prefix + '[]'; + } + + for (let j = 0; j < obj_keys.length; ++j) { + const key = obj_keys[j]; + const value = + // @ts-ignore + typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any]; + + if (skipNulls && value === null) { + continue; + } + + // @ts-ignore + const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key; + const key_prefix = + isArray(obj) ? + typeof generateArrayPrefix === 'function' ? + generateArrayPrefix(adjusted_prefix, encoded_key) + : adjusted_prefix + : adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']'); + + sideChannel.set(object, step); + const valueSideChannel = new WeakMap(); + valueSideChannel.set(sentinel, sideChannel); + push_to_array( + values, + inner_stringify( + value, + key_prefix, + generateArrayPrefix, + commaRoundTrip, + allowEmptyArrays, + strictNullHandling, + skipNulls, + encodeDotInKeys, + // @ts-ignore + generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder, + filter, + sort, + allowDots, + serializeDate, + format, + formatter, + encodeValuesOnly, + charset, + valueSideChannel, + ), + ); + } + + return values; +} + +function normalize_stringify_options( + opts: StringifyOptions = defaults, +): NonNullableProperties> & { indices?: boolean } { + if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { + throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); + } + + if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') { + throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided'); + } + + if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { + throw new TypeError('Encoder has to be a function.'); + } + + const charset = opts.charset || defaults.charset; + if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { + throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + + let format = default_format; + if (typeof opts.format !== 'undefined') { + if (!has(formatters, opts.format)) { + throw new TypeError('Unknown format option provided.'); + } + format = opts.format; + } + const formatter = formatters[format]; + + let filter = defaults.filter; + if (typeof opts.filter === 'function' || isArray(opts.filter)) { + filter = opts.filter; + } + + let arrayFormat: StringifyOptions['arrayFormat']; + if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) { + arrayFormat = opts.arrayFormat; + } else if ('indices' in opts) { + arrayFormat = opts.indices ? 'indices' : 'repeat'; + } else { + arrayFormat = defaults.arrayFormat; + } + + if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') { + throw new TypeError('`commaRoundTrip` must be a boolean, or absent'); + } + + const allowDots = + typeof opts.allowDots === 'undefined' ? + !!opts.encodeDotInKeys === true ? + true + : defaults.allowDots + : !!opts.allowDots; + + return { + addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, + // @ts-ignore + allowDots: allowDots, + allowEmptyArrays: + typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, + arrayFormat: arrayFormat, + charset: charset, + charsetSentinel: + typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, + commaRoundTrip: !!opts.commaRoundTrip, + delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, + encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, + encodeDotInKeys: + typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys, + encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, + encodeValuesOnly: + typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, + filter: filter, + format: format, + formatter: formatter, + serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, + skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, + // @ts-ignore + sort: typeof opts.sort === 'function' ? opts.sort : null, + strictNullHandling: + typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling, + }; +} + +export function stringify(object: any, opts: StringifyOptions = {}) { + let obj = object; + const options = normalize_stringify_options(opts); + + let obj_keys: PropertyKey[] | undefined; + let filter; + + if (typeof options.filter === 'function') { + filter = options.filter; + obj = filter('', obj); + } else if (isArray(options.filter)) { + filter = options.filter; + obj_keys = filter; + } + + const keys: string[] = []; + + if (typeof obj !== 'object' || obj === null) { + return ''; + } + + const generateArrayPrefix = array_prefix_generators[options.arrayFormat]; + const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip; + + if (!obj_keys) { + obj_keys = Object.keys(obj); + } + + if (options.sort) { + obj_keys.sort(options.sort); + } + + const sideChannel = new WeakMap(); + for (let i = 0; i < obj_keys.length; ++i) { + const key = obj_keys[i]!; + + if (options.skipNulls && obj[key] === null) { + continue; + } + push_to_array( + keys, + inner_stringify( + obj[key], + key, + // @ts-expect-error + generateArrayPrefix, + commaRoundTrip, + options.allowEmptyArrays, + options.strictNullHandling, + options.skipNulls, + options.encodeDotInKeys, + options.encode ? options.encoder : null, + options.filter, + options.sort, + options.allowDots, + options.serializeDate, + options.format, + options.formatter, + options.encodeValuesOnly, + options.charset, + sideChannel, + ), + ); + } + + const joined = keys.join(options.delimiter); + let prefix = options.addQueryPrefix === true ? '?' : ''; + + if (options.charsetSentinel) { + if (options.charset === 'iso-8859-1') { + // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark + prefix += 'utf8=%26%2310003%3B&'; + } else { + // encodeURIComponent('✓') + prefix += 'utf8=%E2%9C%93&'; + } + } + + return joined.length > 0 ? prefix + joined : ''; +} diff --git a/src/internal/qs/types.ts b/src/internal/qs/types.ts new file mode 100644 index 0000000..7c28dbb --- /dev/null +++ b/src/internal/qs/types.ts @@ -0,0 +1,71 @@ +export type Format = 'RFC1738' | 'RFC3986'; + +export type DefaultEncoder = (str: any, defaultEncoder?: any, charset?: string) => string; +export type DefaultDecoder = (str: string, decoder?: any, charset?: string) => string; + +export type BooleanOptional = boolean | undefined; + +export type StringifyBaseOptions = { + delimiter?: string; + allowDots?: boolean; + encodeDotInKeys?: boolean; + strictNullHandling?: boolean; + skipNulls?: boolean; + encode?: boolean; + encoder?: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format?: Format, + ) => string; + filter?: Array | ((prefix: PropertyKey, value: any) => any); + arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma'; + indices?: boolean; + sort?: ((a: PropertyKey, b: PropertyKey) => number) | null; + serializeDate?: (d: Date) => string; + format?: 'RFC1738' | 'RFC3986'; + formatter?: (str: PropertyKey) => string; + encodeValuesOnly?: boolean; + addQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + allowEmptyArrays?: boolean; + commaRoundTrip?: boolean; +}; + +export type StringifyOptions = StringifyBaseOptions; + +export type ParseBaseOptions = { + comma?: boolean; + delimiter?: string | RegExp; + depth?: number | false; + decoder?: (str: string, defaultDecoder: DefaultDecoder, charset: string, type: 'key' | 'value') => any; + arrayLimit?: number; + parseArrays?: boolean; + plainObjects?: boolean; + allowPrototypes?: boolean; + allowSparse?: boolean; + parameterLimit?: number; + strictDepth?: boolean; + strictNullHandling?: boolean; + ignoreQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + interpretNumericEntities?: boolean; + allowEmptyArrays?: boolean; + duplicates?: 'combine' | 'first' | 'last'; + allowDots?: boolean; + decodeDotInKeys?: boolean; +}; + +export type ParseOptions = ParseBaseOptions; + +export type ParsedQs = { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +}; + +// Type to remove null or undefined union from each property +export type NonNullableProperties = { + [K in keyof T]-?: Exclude; +}; diff --git a/src/internal/qs/utils.ts b/src/internal/qs/utils.ts new file mode 100644 index 0000000..4cd5657 --- /dev/null +++ b/src/internal/qs/utils.ts @@ -0,0 +1,265 @@ +import { RFC1738 } from './formats'; +import type { DefaultEncoder, Format } from './types'; +import { isArray } from '../utils/values'; + +export let has = (obj: object, key: PropertyKey): boolean => ( + (has = (Object as any).hasOwn ?? Function.prototype.call.bind(Object.prototype.hasOwnProperty)), + has(obj, key) +); + +const hex_table = /* @__PURE__ */ (() => { + const array = []; + for (let i = 0; i < 256; ++i) { + array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); + } + + return array; +})(); + +function compact_queue>(queue: Array<{ obj: T; prop: string }>) { + while (queue.length > 1) { + const item = queue.pop(); + if (!item) continue; + + const obj = item.obj[item.prop]; + + if (isArray(obj)) { + const compacted: unknown[] = []; + + for (let j = 0; j < obj.length; ++j) { + if (typeof obj[j] !== 'undefined') { + compacted.push(obj[j]); + } + } + + // @ts-ignore + item.obj[item.prop] = compacted; + } + } +} + +function array_to_object(source: any[], options: { plainObjects: boolean }) { + const obj = options && options.plainObjects ? Object.create(null) : {}; + for (let i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +} + +export function merge( + target: any, + source: any, + options: { plainObjects?: boolean; allowPrototypes?: boolean } = {}, +) { + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (isArray(target)) { + target.push(source); + } else if (target && typeof target === 'object') { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has(Object.prototype, source)) { + target[source] = true; + } + } else { + return [target, source]; + } + + return target; + } + + if (!target || typeof target !== 'object') { + return [target].concat(source); + } + + let mergeTarget = target; + if (isArray(target) && !isArray(source)) { + // @ts-ignore + mergeTarget = array_to_object(target, options); + } + + if (isArray(target) && isArray(source)) { + source.forEach(function (item, i) { + if (has(target, i)) { + const targetItem = target[i]; + if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { + target[i] = merge(targetItem, item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; + } + + return Object.keys(source).reduce(function (acc, key) { + const value = source[key]; + + if (has(acc, key)) { + acc[key] = merge(acc[key], value, options); + } else { + acc[key] = value; + } + return acc; + }, mergeTarget); +} + +export function assign_single_source(target: any, source: any) { + return Object.keys(source).reduce(function (acc, key) { + acc[key] = source[key]; + return acc; + }, target); +} + +export function decode(str: string, _: any, charset: string) { + const strWithoutPlus = str.replace(/\+/g, ' '); + if (charset === 'iso-8859-1') { + // unescape never throws, no try...catch needed: + return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); + } + // utf-8 + try { + return decodeURIComponent(strWithoutPlus); + } catch (e) { + return strWithoutPlus; + } +} + +const limit = 1024; + +export const encode: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format: Format, +) => string = (str, _defaultEncoder, charset, _kind, format: Format) => { + // This code was originally written by Brian White for the io.js core querystring library. + // It has been adapted here for stricter adherence to RFC 3986 + if (str.length === 0) { + return str; + } + + let string = str; + if (typeof str === 'symbol') { + string = Symbol.prototype.toString.call(str); + } else if (typeof str !== 'string') { + string = String(str); + } + + if (charset === 'iso-8859-1') { + return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { + return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; + }); + } + + let out = ''; + for (let j = 0; j < string.length; j += limit) { + const segment = string.length >= limit ? string.slice(j, j + limit) : string; + const arr = []; + + for (let i = 0; i < segment.length; ++i) { + let c = segment.charCodeAt(i); + if ( + c === 0x2d || // - + c === 0x2e || // . + c === 0x5f || // _ + c === 0x7e || // ~ + (c >= 0x30 && c <= 0x39) || // 0-9 + (c >= 0x41 && c <= 0x5a) || // a-z + (c >= 0x61 && c <= 0x7a) || // A-Z + (format === RFC1738 && (c === 0x28 || c === 0x29)) // ( ) + ) { + arr[arr.length] = segment.charAt(i); + continue; + } + + if (c < 0x80) { + arr[arr.length] = hex_table[c]; + continue; + } + + if (c < 0x800) { + arr[arr.length] = hex_table[0xc0 | (c >> 6)]! + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + if (c < 0xd800 || c >= 0xe000) { + arr[arr.length] = + hex_table[0xe0 | (c >> 12)]! + hex_table[0x80 | ((c >> 6) & 0x3f)] + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + i += 1; + c = 0x10000 + (((c & 0x3ff) << 10) | (segment.charCodeAt(i) & 0x3ff)); + + arr[arr.length] = + hex_table[0xf0 | (c >> 18)]! + + hex_table[0x80 | ((c >> 12) & 0x3f)] + + hex_table[0x80 | ((c >> 6) & 0x3f)] + + hex_table[0x80 | (c & 0x3f)]; + } + + out += arr.join(''); + } + + return out; +}; + +export function compact(value: any) { + const queue = [{ obj: { o: value }, prop: 'o' }]; + const refs = []; + + for (let i = 0; i < queue.length; ++i) { + const item = queue[i]; + // @ts-ignore + const obj = item.obj[item.prop]; + + const keys = Object.keys(obj); + for (let j = 0; j < keys.length; ++j) { + const key = keys[j]!; + const val = obj[key]; + if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { + queue.push({ obj: obj, prop: key }); + refs.push(val); + } + } + } + + compact_queue(queue); + + return value; +} + +export function is_regexp(obj: any) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +export function is_buffer(obj: any) { + if (!obj || typeof obj !== 'object') { + return false; + } + + return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); +} + +export function combine(a: any, b: any) { + return [].concat(a, b); +} + +export function maybe_map(val: T[], fn: (v: T) => T) { + if (isArray(val)) { + const mapped = []; + for (let i = 0; i < val.length; i += 1) { + mapped.push(fn(val[i]!)); + } + return mapped; + } + return fn(val); +} diff --git a/src/internal/utils/query.ts b/src/internal/utils/query.ts index 2194aaa..0139cac 100644 --- a/src/internal/utils/query.ts +++ b/src/internal/utils/query.ts @@ -1,23 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { ParallelError } from '../../core/error'; +import * as qs from '../qs/stringify'; -/** - * Basic re-implementation of `qs.stringify` for primitive types. - */ export function stringifyQuery(query: object | Record) { - return Object.entries(query) - .filter(([_, value]) => typeof value !== 'undefined') - .map(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } - if (value === null) { - return `${encodeURIComponent(key)}=`; - } - throw new ParallelError( - `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, - ); - }) - .join('&'); + return qs.stringify(query, { arrayFormat: 'comma' }); } diff --git a/src/resources/beta/api.md b/src/resources/beta/api.md index 0dca07f..e94d1ed 100644 --- a/src/resources/beta/api.md +++ b/src/resources/beta/api.md @@ -9,6 +9,7 @@ Types: - WebSearchResult - ExtractError - FetchPolicy +- FullContentSettings - UsageItem Methods: @@ -43,7 +44,7 @@ Types: - TaskGroupEventsResponse - TaskGroupGetRunsResponse - TaskGroupStatus -- TaskGroup +- TaskGroupStatusEvent - TaskGroupRunResponse Methods: @@ -58,7 +59,9 @@ Methods: Types: +- FindAllCandidate - FindAllCandidateMatchStatusEvent +- FindAllCandidateMetrics - FindAllCandidatesRequest - FindAllCandidatesResponse - FindAllEnrichInput @@ -66,10 +69,12 @@ Types: - FindAllRun - FindAllRunInput - FindAllRunResult +- FindAllRunStatus - FindAllRunStatusEvent - FindAllSchema - FindAllSchemaUpdatedEvent - IngestInput +- MatchCondition - FindAllCancelResponse - FindAllEventsResponse diff --git a/src/resources/beta/beta.ts b/src/resources/beta/beta.ts index dc5bae4..b53276f 100644 --- a/src/resources/beta/beta.ts +++ b/src/resources/beta/beta.ts @@ -8,7 +8,9 @@ import { FindAll, FindAllCancelParams, FindAllCancelResponse, + FindAllCandidate, FindAllCandidateMatchStatusEvent, + FindAllCandidateMetrics, FindAllCandidatesParams, FindAllCandidatesRequest, FindAllCandidatesResponse, @@ -25,11 +27,13 @@ import { FindAllRun, FindAllRunInput, FindAllRunResult, + FindAllRunStatus, FindAllRunStatusEvent, FindAllSchema, FindAllSchemaParams, FindAllSchemaUpdatedEvent, IngestInput, + MatchCondition, } from './findall'; import * as TaskGroupAPI from './task-group'; import { @@ -42,6 +46,7 @@ import { TaskGroupGetRunsResponse, TaskGroupRunResponse, TaskGroupStatus, + TaskGroupStatusEvent, } from './task-group'; import * as TaskRunAPI from './task-run'; import { @@ -236,6 +241,8 @@ export type ExtractError = TopLevelAPI.ExtractError; export type FetchPolicy = TopLevelAPI.FetchPolicy; +export type FullContentSettings = TopLevelAPI.FullContentSettings; + export type UsageItem = TopLevelAPI.UsageItem; export interface BetaExtractParams { @@ -266,7 +273,7 @@ export interface BetaExtractParams { * Body param: Include full content from each URL. Note that if neither objective * nor search_queries is provided, excerpts are redundant with full content. */ - full_content?: boolean | BetaExtractParams.FullContentSettings; + full_content?: boolean | TopLevelAPI.FullContentSettings; /** * Body param: If provided, focuses extracted content on the specified search @@ -293,20 +300,6 @@ export interface BetaExtractParams { betas?: Array; } -export namespace BetaExtractParams { - /** - * Optional settings for returning full content. - */ - export interface FullContentSettings { - /** - * Optional limit on the number of characters to include in the full content for - * each url. Full content always starts at the beginning of the page and is - * truncated at the limit if necessary. - */ - max_chars_per_result?: number | null; - } -} - export interface BetaSearchParams { /** * Body param: The model generating this request and consuming the results. Enables @@ -392,6 +385,7 @@ export interface BetaSearchParams { } Beta.TaskRun = TaskRun; +Beta.TaskGroup = TaskGroup; Beta.FindAll = FindAll; export declare namespace Beta { @@ -403,6 +397,7 @@ export declare namespace Beta { type WebSearchResult as WebSearchResult, type ExtractError as ExtractError, type FetchPolicy as FetchPolicy, + type FullContentSettings as FullContentSettings, type UsageItem as UsageItem, type BetaExtractParams as BetaExtractParams, type BetaSearchParams as BetaSearchParams, @@ -424,10 +419,11 @@ export declare namespace Beta { }; export { - type TaskGroup as TaskGroup, + TaskGroup as TaskGroup, type TaskGroupEventsResponse as TaskGroupEventsResponse, type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, type TaskGroupStatus as TaskGroupStatus, + type TaskGroupStatusEvent as TaskGroupStatusEvent, type TaskGroupRunResponse as TaskGroupRunResponse, type TaskGroupCreateParams as TaskGroupCreateParams, type TaskGroupAddRunsParams as TaskGroupAddRunsParams, @@ -437,7 +433,9 @@ export declare namespace Beta { export { FindAll as FindAll, + type FindAllCandidate as FindAllCandidate, type FindAllCandidateMatchStatusEvent as FindAllCandidateMatchStatusEvent, + type FindAllCandidateMetrics as FindAllCandidateMetrics, type FindAllCandidatesRequest as FindAllCandidatesRequest, type FindAllCandidatesResponse as FindAllCandidatesResponse, type FindAllEnrichInput as FindAllEnrichInput, @@ -445,10 +443,12 @@ export declare namespace Beta { type FindAllRun as FindAllRun, type FindAllRunInput as FindAllRunInput, type FindAllRunResult as FindAllRunResult, + type FindAllRunStatus as FindAllRunStatus, type FindAllRunStatusEvent as FindAllRunStatusEvent, type FindAllSchema as FindAllSchema, type FindAllSchemaUpdatedEvent as FindAllSchemaUpdatedEvent, type IngestInput as IngestInput, + type MatchCondition as MatchCondition, type FindAllCancelResponse as FindAllCancelResponse, type FindAllEventsResponse as FindAllEventsResponse, type FindAllCreateParams as FindAllCreateParams, diff --git a/src/resources/beta/findall.ts b/src/resources/beta/findall.ts index a054365..f649d6b 100644 --- a/src/resources/beta/findall.ts +++ b/src/resources/beta/findall.ts @@ -304,6 +304,52 @@ export class FindAll extends APIResource { } } +/** + * Candidate for a find all run that may end up as a match. + * + * Contains all the candidate's metadata and the output of the match conditions. A + * candidate is a match if all match conditions are satisfied. + */ +export interface FindAllCandidate { + /** + * ID of the candidate. + */ + candidate_id: string; + + /** + * Status of the candidate. One of generated, matched, unmatched, discarded. + */ + match_status: 'generated' | 'matched' | 'unmatched' | 'discarded'; + + /** + * Name of the candidate. + */ + name: string; + + /** + * URL that provides context or details of the entity for disambiguation. + */ + url: string; + + /** + * List of FieldBasis objects supporting the output. + */ + basis?: Array | null; + + /** + * Brief description of the entity that can help answer whether entity satisfies + * the query. + */ + description?: string | null; + + /** + * Results of the match condition evaluations for this candidate. This object + * contains the structured output that determines whether the candidate matches the + * overall FindAll objective. + */ + output?: { [key: string]: unknown } | null; +} + /** * Event containing a candidate whose match status has changed. */ @@ -311,7 +357,7 @@ export interface FindAllCandidateMatchStatusEvent { /** * The candidate whose match status has been updated. */ - data: FindAllCandidateMatchStatusEvent.Data; + data: FindAllCandidate; /** * Unique event identifier for the event. @@ -336,49 +382,19 @@ export interface FindAllCandidateMatchStatusEvent { | 'findall.candidate.enriched'; } -export namespace FindAllCandidateMatchStatusEvent { +/** + * Metrics object for FindAll run. + */ +export interface FindAllCandidateMetrics { /** - * The candidate whose match status has been updated. + * Number of candidates that were selected. */ - export interface Data { - /** - * ID of the candidate. - */ - candidate_id: string; - - /** - * Status of the candidate. One of generated, matched, unmatched, discarded. - */ - match_status: 'generated' | 'matched' | 'unmatched' | 'discarded'; - - /** - * Name of the candidate. - */ - name: string; + generated_candidates_count?: number; - /** - * URL that provides context or details of the entity for disambiguation. - */ - url: string; - - /** - * List of FieldBasis objects supporting the output. - */ - basis?: Array | null; - - /** - * Brief description of the entity that can help answer whether entity satisfies - * the query. - */ - description?: string | null; - - /** - * Results of the match condition evaluations for this candidate. This object - * contains the structured output that determines whether the candidate matches the - * overall FindAll objective. - */ - output?: { [key: string]: unknown } | null; - } + /** + * Number of candidates that evaluated to matched. + */ + matched_candidates_count?: number; } export interface FindAllCandidatesRequest { @@ -480,7 +496,7 @@ export interface FindAllRun { /** * Status object for the FindAll run. */ - status: FindAllRun.Status; + status: FindAllRunStatus; /** * Timestamp of the creation of the run, in RFC 3339 format. @@ -499,58 +515,6 @@ export interface FindAllRun { modified_at?: string | null; } -export namespace FindAllRun { - /** - * Status object for the FindAll run. - */ - export interface Status { - /** - * Whether the FindAll run is active - */ - is_active: boolean; - - /** - * Candidate metrics for the FindAll run. - */ - metrics: Status.Metrics; - - /** - * Status of the FindAll run. - */ - status: 'queued' | 'action_required' | 'running' | 'completed' | 'failed' | 'cancelling' | 'cancelled'; - - /** - * Reason for termination when FindAll run is in terminal status. - */ - termination_reason?: - | 'low_match_rate' - | 'match_limit_met' - | 'candidates_exhausted' - | 'user_cancelled' - | 'error_occurred' - | 'timeout' - | 'insufficient_funds' - | null; - } - - export namespace Status { - /** - * Candidate metrics for the FindAll run. - */ - export interface Metrics { - /** - * Number of candidates that were selected. - */ - generated_candidates_count?: number; - - /** - * Number of candidates that evaluated to matched. - */ - matched_candidates_count?: number; - } - } -} - /** * Input model for FindAll run. */ @@ -568,7 +532,7 @@ export interface FindAllRunInput { /** * List of match conditions for the FindAll run. */ - match_conditions: Array; + match_conditions: Array; /** * Maximum number of matches to find for this FindAll run. Must be between 5 and @@ -598,23 +562,6 @@ export interface FindAllRunInput { } export namespace FindAllRunInput { - /** - * Match condition model for FindAll ingest. - */ - export interface MatchCondition { - /** - * Detailed description of the match condition. Include as much specific - * information as possible to help improve the quality and accuracy of Find All run - * results. - */ - description: string; - - /** - * Name of the match condition. - */ - name: string; - } - /** * Exclude candidate input model for FindAll run. */ @@ -642,7 +589,7 @@ export interface FindAllRunResult { /** * All evaluated candidates at the time of the snapshot. */ - candidates: Array; + candidates: Array; /** * FindAll run object. @@ -656,52 +603,37 @@ export interface FindAllRunResult { last_event_id?: string | null; } -export namespace FindAllRunResult { +/** + * Status object for FindAll run. + */ +export interface FindAllRunStatus { /** - * Candidate for a find all run that may end up as a match. - * - * Contains all the candidate's metadata and the output of the match conditions. A - * candidate is a match if all match conditions are satisfied. + * Whether the FindAll run is active */ - export interface Candidate { - /** - * ID of the candidate. - */ - candidate_id: string; + is_active: boolean; - /** - * Status of the candidate. One of generated, matched, unmatched, discarded. - */ - match_status: 'generated' | 'matched' | 'unmatched' | 'discarded'; - - /** - * Name of the candidate. - */ - name: string; - - /** - * URL that provides context or details of the entity for disambiguation. - */ - url: string; - - /** - * List of FieldBasis objects supporting the output. - */ - basis?: Array | null; + /** + * Candidate metrics for the FindAll run. + */ + metrics: FindAllCandidateMetrics; - /** - * Brief description of the entity that can help answer whether entity satisfies - * the query. - */ - description?: string | null; + /** + * Status of the FindAll run. + */ + status: 'queued' | 'action_required' | 'running' | 'completed' | 'failed' | 'cancelling' | 'cancelled'; - /** - * Results of the match condition evaluations for this candidate. This object - * contains the structured output that determines whether the candidate matches the - * overall FindAll objective. - */ - output?: { [key: string]: unknown } | null; - } + /** + * Reason for termination when FindAll run is in terminal status. + */ + termination_reason?: + | 'low_match_rate' + | 'match_limit_met' + | 'candidates_exhausted' + | 'user_cancelled' + | 'error_occurred' + | 'timeout' + | 'insufficient_funds' + | null; } /** @@ -741,7 +673,7 @@ export interface FindAllSchema { /** * List of match conditions for the FindAll run. */ - match_conditions: Array; + match_conditions: Array; /** * Natural language objective of the FindAll run. @@ -764,25 +696,6 @@ export interface FindAllSchema { match_limit?: number | null; } -export namespace FindAllSchema { - /** - * Match condition model for FindAll ingest. - */ - export interface MatchCondition { - /** - * Detailed description of the match condition. Include as much specific - * information as possible to help improve the quality and accuracy of Find All run - * results. - */ - description: string; - - /** - * Name of the match condition. - */ - name: string; - } -} - /** * Event containing full snapshot of FindAll run state. */ @@ -818,6 +731,23 @@ export interface IngestInput { objective: string; } +/** + * Match condition model for FindAll ingest. + */ +export interface MatchCondition { + /** + * Detailed description of the match condition. Include as much specific + * information as possible to help improve the quality and accuracy of Find All run + * results. + */ + description: string; + + /** + * Name of the match condition. + */ + name: string; +} + export type FindAllCancelResponse = unknown; /** @@ -843,7 +773,7 @@ export interface FindAllCreateParams { /** * Body param: List of match conditions for the FindAll run. */ - match_conditions: Array; + match_conditions: Array; /** * Body param: Maximum number of matches to find for this FindAll run. Must be @@ -878,23 +808,6 @@ export interface FindAllCreateParams { } export namespace FindAllCreateParams { - /** - * Match condition model for FindAll ingest. - */ - export interface MatchCondition { - /** - * Detailed description of the match condition. Include as much specific - * information as possible to help improve the quality and accuracy of Find All run - * results. - */ - description: string; - - /** - * Name of the match condition. - */ - name: string; - } - /** * Exclude candidate input model for FindAll run. */ @@ -1024,7 +937,9 @@ export interface FindAllSchemaParams { export declare namespace FindAll { export { + type FindAllCandidate as FindAllCandidate, type FindAllCandidateMatchStatusEvent as FindAllCandidateMatchStatusEvent, + type FindAllCandidateMetrics as FindAllCandidateMetrics, type FindAllCandidatesRequest as FindAllCandidatesRequest, type FindAllCandidatesResponse as FindAllCandidatesResponse, type FindAllEnrichInput as FindAllEnrichInput, @@ -1032,10 +947,12 @@ export declare namespace FindAll { type FindAllRun as FindAllRun, type FindAllRunInput as FindAllRunInput, type FindAllRunResult as FindAllRunResult, + type FindAllRunStatus as FindAllRunStatus, type FindAllRunStatusEvent as FindAllRunStatusEvent, type FindAllSchema as FindAllSchema, type FindAllSchemaUpdatedEvent as FindAllSchemaUpdatedEvent, type IngestInput as IngestInput, + type MatchCondition as MatchCondition, type FindAllCancelResponse as FindAllCancelResponse, type FindAllEventsResponse as FindAllEventsResponse, type FindAllCreateParams as FindAllCreateParams, diff --git a/src/resources/beta/index.ts b/src/resources/beta/index.ts index 6367d0a..944e12d 100644 --- a/src/resources/beta/index.ts +++ b/src/resources/beta/index.ts @@ -4,7 +4,9 @@ export { Beta } from './beta'; export { FindAll, Findall, + type FindAllCandidate, type FindAllCandidateMatchStatusEvent, + type FindAllCandidateMetrics, type FindAllCandidatesRequest, type FindAllCandidatesResponse, type FindAllEnrichInput, @@ -12,10 +14,12 @@ export { type FindAllRun, type FindAllRunInput, type FindAllRunResult, + type FindAllRunStatus, type FindAllRunStatusEvent, type FindAllSchema, type FindAllSchemaUpdatedEvent, type IngestInput, + type MatchCondition, type FindAllCancelResponse, type FindAllEventsResponse, type FindAllCreateParams, @@ -55,6 +59,7 @@ export { type TaskGroupEventsResponse, type TaskGroupGetRunsResponse, type TaskGroupStatus, + type TaskGroupStatusEvent, type TaskGroupRunResponse, type TaskGroupCreateParams, type TaskGroupAddRunsParams, diff --git a/src/resources/beta/task-group.ts b/src/resources/beta/task-group.ts index ff64ba7..9f684e2 100644 --- a/src/resources/beta/task-group.ts +++ b/src/resources/beta/task-group.ts @@ -149,32 +149,10 @@ export class TaskGroup extends APIResource { * Event indicating an update to group status. */ export type TaskGroupEventsResponse = - | TaskGroupEventsResponse.TaskGroupStatusEvent + | TaskGroupAPI.TaskGroupStatusEvent | TaskRunAPI.TaskRunEvent | TaskRunAPI.ErrorEvent; -export namespace TaskGroupEventsResponse { - /** - * Event indicating an update to group status. - */ - export interface TaskGroupStatusEvent { - /** - * Cursor to resume the event stream. - */ - event_id: string; - - /** - * Task group status object. - */ - status: TaskGroupAPI.TaskGroupStatus; - - /** - * Event type; always 'task_group_status'. - */ - type: 'task_group_status'; - } -} - /** * Event when a task run transitions to a non-active status. * @@ -184,7 +162,7 @@ export type TaskGroupGetRunsResponse = TaskRunAPI.TaskRunEvent | TaskRunAPI.Erro export type TaskGroupStatus = TaskGroupAPI.TaskGroupStatus; -export type TaskGroup = TaskGroupAPI.TaskGroup; +export type TaskGroupStatusEvent = TaskGroupAPI.TaskGroupStatusEvent; export type TaskGroupRunResponse = TaskGroupAPI.TaskGroupRunResponse; @@ -253,7 +231,7 @@ export declare namespace TaskGroup { type TaskGroupEventsResponse as TaskGroupEventsResponse, type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, type TaskGroupStatus as TaskGroupStatus, - type TaskGroup as TaskGroup, + type TaskGroupStatusEvent as TaskGroupStatusEvent, type TaskGroupRunResponse as TaskGroupRunResponse, type TaskGroupCreateParams as TaskGroupCreateParams, type TaskGroupAddRunsParams as TaskGroupAddRunsParams, diff --git a/src/resources/beta/task-run.ts b/src/resources/beta/task-run.ts index 258c7ae..74b0e91 100644 --- a/src/resources/beta/task-run.ts +++ b/src/resources/beta/task-run.ts @@ -104,81 +104,11 @@ export type ParallelBeta = * A progress update for a task run. */ export type TaskRunEventsResponse = - | TaskRunEventsResponse.TaskRunProgressStatsEvent - | TaskRunEventsResponse.TaskRunProgressMessageEvent + | TaskRunAPI.TaskRunProgressStatsEvent + | TaskRunAPI.TaskRunProgressMessageEvent | TaskRunAPI.TaskRunEvent | TaskRunAPI.ErrorEvent; -export namespace TaskRunEventsResponse { - /** - * A progress update for a task run. - */ - export interface TaskRunProgressStatsEvent { - /** - * Completion percentage of the task run. Ranges from 0 to 100 where 0 indicates no - * progress and 100 indicates completion. - */ - progress_meter: number; - - /** - * Source stats describing progress so far. - */ - source_stats: TaskRunProgressStatsEvent.SourceStats; - - /** - * Event type; always 'task_run.progress_stats'. - */ - type: 'task_run.progress_stats'; - } - - export namespace TaskRunProgressStatsEvent { - /** - * Source stats describing progress so far. - */ - export interface SourceStats { - /** - * Number of sources considered in processing the task. - */ - num_sources_considered: number | null; - - /** - * Number of sources read in processing the task. - */ - num_sources_read: number | null; - - /** - * A sample of URLs of sources read in processing the task. - */ - sources_read_sample: Array | null; - } - } - - /** - * A message for a task run progress update. - */ - export interface TaskRunProgressMessageEvent { - /** - * Progress update message. - */ - message: string; - - /** - * Timestamp of the message. - */ - timestamp: string | null; - - /** - * Event type; always starts with 'task_run.progress_msg'. - */ - type: - | 'task_run.progress_msg.plan' - | 'task_run.progress_msg.search' - | 'task_run.progress_msg.result' - | 'task_run.progress_msg.tool_call' - | 'task_run.progress_msg.exec_status'; - } -} - /** * @deprecated Use parallel.types.task_run.TaskRunInput instead */ @@ -239,7 +169,7 @@ export interface TaskRunCreateParams { /** * Body param: Advanced search configuration for a task run. */ - advanced_settings?: TaskRunCreateParams.AdvancedSettings | null; + advanced_settings?: TaskRunAPI.TaskAdvancedSettings | null; /** * Body param: Controls tracking of task run execution progress. When set to true, @@ -295,18 +225,6 @@ export interface TaskRunCreateParams { betas?: Array; } -export namespace TaskRunCreateParams { - /** - * Advanced search configuration for a task run. - */ - export interface AdvancedSettings { - /** - * ISO 3166-1 alpha-2 country code for geo-targeted search results. - */ - location?: string | null; - } -} - export interface TaskRunResultParams { /** * Query param diff --git a/src/resources/index.ts b/src/resources/index.ts index 1a44a3b..6fce058 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -2,16 +2,42 @@ export * from './shared'; export { Beta } from './beta/beta'; +export { + MonitorResource, + type AdvancedMonitorSettings, + type CreateMonitorRequest, + type Monitor, + type MonitorCompletionEvent, + type MonitorErrorEvent, + type MonitorEventStreamEvent, + type MonitorEventStreamResponseSettings, + type MonitorEventStreamSettings, + type MonitorSnapshotEvent, + type MonitorSnapshotOutput, + type MonitorSnapshotResponseSettings, + type MonitorSnapshotSettings, + type MonitorWebhook, + type PaginatedMonitorEvents, + type PaginatedMonitorResponse, + type UpdateMonitorEventStreamSettings, + type UpdateMonitorRequest, + type MonitorCreateParams, + type MonitorUpdateParams, + type MonitorListParams, + type MonitorEventsParams, +} from './monitor'; export { TaskGroup, type TaskGroupRunResponse, type TaskGroupStatus, + type TaskGroupStatusEvent, type TaskGroupEventsResponse, type TaskGroupGetRunsResponse, type TaskGroupCreateParams, type TaskGroupAddRunsParams, type TaskGroupEventsParams, type TaskGroupGetRunsParams, + type TaskGroupRetrieveRunParams, } from './task-group'; export { TaskRun, @@ -23,9 +49,13 @@ export { type McpServer, type McpToolCall, type RunInput, + type TaskAdvancedSettings, type TaskRunEvent, type TaskRunJsonOutput, + type TaskRunProgressMessageEvent, + type TaskRunProgressStatsEvent, type TaskRunResult, + type TaskRunSourceStats, type TaskRunTextOutput, type TaskSpec, type TextSchema, @@ -42,6 +72,7 @@ export { type ExtractResponse, type ExtractResult, type FetchPolicy, + type FullContentSettings, type SearchResult, type UsageItem, type WebSearchResult, diff --git a/src/resources/monitor.ts b/src/resources/monitor.ts new file mode 100644 index 0000000..3aa55d7 --- /dev/null +++ b/src/resources/monitor.ts @@ -0,0 +1,752 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../core/resource'; +import * as Shared from './shared'; +import * as TaskRunAPI from './task-run'; +import { APIPromise } from '../core/api-promise'; +import { buildHeaders } from '../internal/headers'; +import { RequestOptions } from '../internal/request-options'; +import { path } from '../internal/utils/path'; + +/** + * The Monitor API watches the web for material changes on a fixed frequency. Each monitor runs once on creation and then on its configured schedule, emitting events when meaningful changes are detected. + * - `event_stream` monitors track a search query and emit an event for each new material change. + * - `snapshot` monitors track a specific task run's output and emit an event when the output changes. + * + * Results can be polled via the events endpoint or delivered via webhooks. + */ +export class MonitorResource extends APIResource { + /** + * Create a monitor. + * + * Monitors run on a fixed frequency to detect material changes in web content. Set + * `type=event_stream` to monitor a search query, or `type=snapshot` to monitor a + * specific task run's output. The monitor runs once immediately at creation, then + * continues on the configured schedule. + * + * @example + * ```ts + * const monitor = await client.monitor.create({ + * frequency: '1h', + * settings: { query: 'Extract recent news about AI' }, + * type: 'event_stream', + * }); + * ``` + */ + create(body: MonitorCreateParams, options?: RequestOptions): APIPromise { + return this._client.post('/v1/monitors', { body, ...options }); + } + + /** + * Retrieve a monitor. + * + * Retrieves a specific monitor by `monitor_id`. Returns the monitor configuration + * including status, frequency, query, and webhook settings. + * + * @example + * ```ts + * const monitor = await client.monitor.retrieve('monitor_id'); + * ``` + */ + retrieve(monitorID: string, options?: RequestOptions): APIPromise { + return this._client.get(path`/v1/monitors/${monitorID}`, options); + } + + /** + * Update a monitor. + * + * Only fields explicitly included in the request body are changed. Pass `null` for + * `webhook` or `metadata` to clear those fields. Pass `type` and `settings` to + * update type-specific settings on an `event_stream` monitor. At least one field + * must be provided. Cancelled monitors cannot be updated. + * + * @example + * ```ts + * const monitor = await client.monitor.update('monitor_id'); + * ``` + */ + update(monitorID: string, body: MonitorUpdateParams, options?: RequestOptions): APIPromise { + return this._client.post(path`/v1/monitors/${monitorID}/update`, { body, ...options }); + } + + /** + * List monitors ordered by creation time, newest first. + * + * Monitors are sorted by `created_at` descending. `limit` defaults to 100. Use + * `next_cursor` from the response and pass it as `cursor` to fetch the next page. + * Pagination ends when `next_cursor` is absent. + * + * By default only `active` monitors are returned. Pass `status=cancelled` or both + * values to include cancelled monitors. + * + * The legacy Monitor API (`/v1alpha/monitors` endpoints) is documented under the + * `Monitor (Alpha)` tag. + * + * @example + * ```ts + * const paginatedMonitorResponse = + * await client.monitor.list(); + * ``` + */ + list( + query: MonitorListParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.get('/v1/monitors', { query, ...options }); + } + + /** + * Cancel a monitor. + * + * Permanently stops the monitor from running. Cancellation is irreversible — + * create a new monitor to resume monitoring. Cancelling an already-cancelled + * monitor is a no-op. + * + * @example + * ```ts + * const monitor = await client.monitor.cancel('monitor_id'); + * ``` + */ + cancel(monitorID: string, options?: RequestOptions): APIPromise { + return this._client.post(path`/v1/monitors/${monitorID}/cancel`, options); + } + + /** + * List events for a monitor, newest first. + * + * Pass `event_group_id` to narrow results to a single execution. Otherwise returns + * all executions newest-first; use `next_cursor` to paginate. Set + * `include_completions=true` to also include no-change executions. + * + * @example + * ```ts + * const paginatedMonitorEvents = await client.monitor.events( + * 'monitor_id', + * ); + * ``` + */ + events( + monitorID: string, + query: MonitorEventsParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.get(path`/v1/monitors/${monitorID}/events`, { query, ...options }); + } + + /** + * Trigger an immediate monitor run. + * + * Enqueues a one-off execution of the monitor outside its normal schedule. The + * monitor's regular schedule is not affected. An event is only emitted if the + * execution detects a material change. Cancelled monitors cannot be triggered. + * + * @example + * ```ts + * await client.monitor.trigger('monitor_id'); + * ``` + */ + trigger(monitorID: string, options?: RequestOptions): APIPromise { + return this._client.post(path`/v1/monitors/${monitorID}/trigger`, { + ...options, + headers: buildHeaders([{ Accept: '*/*' }, options?.headers]), + }); + } +} + +/** + * Advanced monitor configuration. + */ +export interface AdvancedMonitorSettings { + /** + * ISO 3166-1 alpha-2 country code for geo-targeted monitor results. + */ + location?: string | null; + + /** + * Source policy for web search results. + * + * This policy governs which sources are allowed/disallowed in results. + */ + source_policy?: Shared.SourcePolicy | null; +} + +/** + * Request body to create a monitor. + * + * The `type` field at the root determines the expected shape of `settings`: + * `event_stream` requires `MonitorEventStreamSettings`, and `snapshot` requires + * `MonitorSnapshotSettings`. + */ +export interface CreateMonitorRequest { + /** + * Frequency of the monitor. Format: '' where unit is 'h' (hours), + * 'd' (days), or 'w' (weeks). Must be between 1h and 30d (inclusive). + */ + frequency: string; + + /** + * Type-specific settings for the monitor. The expected shape is determined by the + * root `type` field: pass `MonitorEventStreamSettings` when `type` is + * `event_stream`, and `MonitorSnapshotSettings` when `type` is `snapshot`. + */ + settings: MonitorEventStreamSettings | MonitorSnapshotSettings; + + /** + * Type of monitor to create. `event_stream` monitors a search query for material + * changes; `snapshot` monitors a specific task run's output. Determines the + * expected shape of `settings`. + */ + type: 'event_stream' | 'snapshot'; + + /** + * User-provided metadata stored with the monitor and echoed back in webhook + * notifications and GET responses, so you can map events to objects in your + * application. Keys: max 16 chars; values: max 512 chars. + */ + metadata?: { [key: string]: string } | null; + + /** + * Processor to use for the monitor. `lite` is faster and cheaper; `base` performs + * more thorough analysis at higher cost and latency. Defaults to `lite`. + */ + processor?: 'lite' | 'base'; + + /** + * Webhook configuration for a monitor. + */ + webhook?: MonitorWebhook | null; +} + +/** + * Response object for a monitor. + * + * The `type` field at the root determines the concrete shape of `settings`: + * `event_stream` uses `MonitorEventStreamResponseSettings`, and `snapshot` uses + * `MonitorSnapshotResponseSettings`. Snapshot monitors also carry an `output` + * field (`MonitorSnapshotOutput`) with the latest computed state. + */ +export interface Monitor { + /** + * Timestamp of the creation of the monitor, as an RFC 3339 string. + */ + created_at: string; + + /** + * Frequency of the monitor. Format: '' where unit is 'h' (hours), + * 'd' (days), or 'w' (weeks). Must be between 1h and 30d (inclusive). + */ + frequency: string; + + /** + * ID of the monitor. + */ + monitor_id: string; + + /** + * Processor to use for the monitor. `lite` is faster and cheaper; `base` performs + * more thorough analysis at higher cost and latency. Defaults to `lite`. + */ + processor: 'lite' | 'base'; + + /** + * Type-specific configuration. Shape is determined by `type`: + * `MonitorEventStreamResponseSettings` for `event_stream`, + * `MonitorSnapshotResponseSettings` for `snapshot`. + */ + settings: MonitorEventStreamResponseSettings | MonitorSnapshotResponseSettings; + + /** + * Status of the monitor. + */ + status: 'active' | 'cancelled'; + + /** + * The type of monitor. + */ + type: 'event_stream' | 'snapshot'; + + /** + * Timestamp of the last run for the monitor, as an RFC 3339 string. + */ + last_run_at?: string | null; + + /** + * User-provided metadata stored with the monitor and echoed back in webhook + * notifications and GET responses, so you can map events to objects in your + * application. Keys: max 16 chars; values: max 512 chars. + */ + metadata?: { [key: string]: string } | null; + + /** + * Runtime output state for a `snapshot` monitor. + */ + output?: MonitorSnapshotOutput | null; + + /** + * Webhook configuration for a monitor. + */ + webhook?: MonitorWebhook | null; +} + +/** + * Emitted when a monitor execution ran but detected no material changes. + * + * Only returned when `include_completions=true` is passed to the list events + * endpoint. Useful for auditing execution history alongside content events. + */ +export interface MonitorCompletionEvent { + /** + * Timestamp of when the monitor execution completed, as an RFC 3339 string. + */ + timestamp: string; + + /** + * Discriminant for the completion event variant. + */ + event_type?: 'completion'; +} + +/** + * Emitted when a monitor execution failed (e.g. payment or quota error). + * + * Always included in the events list regardless of `include_completions`. + */ +export interface MonitorErrorEvent { + /** + * Human-readable description of the failure. + */ + error_message: string; + + /** + * Timestamp of when the monitor execution failed, as an RFC 3339 string. + */ + timestamp: string; + + /** + * Discriminant for the error event variant. + */ + event_type?: 'error'; +} + +/** + * Append-only event from an event_stream monitor. + * + * Each event represents a distinct material change detected since the previous + * execution. Events are net-new relative to the cursor; clients should treat them + * as an append-only log. + */ +export interface MonitorEventStreamEvent { + /** + * Date when this event was produced. ISO 8601 date (YYYY-MM-DD) or partial + * (YYYY-MM or YYYY). + */ + event_date: string | null; + + /** + * ID of the event group that owns this event. + */ + event_group_id: string; + + /** + * Stable identifier for this event. Safe to use for client-side deduplication + * across pagination and retries. + */ + event_id: string; + + /** + * Text or JSON output describing the detected change. + */ + output: TaskRunAPI.TaskRunTextOutput | TaskRunAPI.TaskRunJsonOutput; + + /** + * Discriminant for the event_stream event variant. + */ + event_type?: 'event_stream'; +} + +/** + * Type-specific response fields for an `event_stream` monitor. + */ +export interface MonitorEventStreamResponseSettings { + /** + * The search query being monitored. + */ + query: string; + + /** + * Advanced monitor configuration. + */ + advanced_settings?: AdvancedMonitorSettings | null; + + /** + * If true, the first execution returns a sample of recent historical events + * matching the query (preview only — not exhaustive). If false or omitted, only + * events from the monitor's creation date onward are returned. Subsequent + * executions are always incremental. + */ + include_backfill?: boolean | null; + + /** + * JSON schema for a task input or output. + */ + output_schema?: TaskRunAPI.JsonSchema | null; +} + +/** + * Type-specific settings for an `event_stream` monitor. + */ +export interface MonitorEventStreamSettings { + /** + * Search query to monitor for material changes. + */ + query: string; + + /** + * Advanced monitor configuration. + */ + advanced_settings?: AdvancedMonitorSettings | null; + + /** + * If true, the first execution returns a sample of recent historical events + * matching the query (preview only — not exhaustive). If false or omitted, only + * events from the monitor's creation date onward are returned. Subsequent + * executions are always incremental. + */ + include_backfill?: boolean | null; + + /** + * JSON schema for a task input or output. + */ + output_schema?: TaskRunAPI.JsonSchema | null; +} + +/** + * Snapshot diff event emitted when a monitored task run's output changes. + * + * `changed_output` contains only the fields that changed since the previous + * execution, along with their `basis` (reasoning + citations). `previous_output` + * holds the complete output from the prior run for comparison. + */ +export interface MonitorSnapshotEvent { + /** + * Partial output containing only the fields that changed since the previous + * execution, each with its `basis` (reasoning and citations). + */ + changed_output: TaskRunAPI.TaskRunTextOutput | TaskRunAPI.TaskRunJsonOutput; + + /** + * Date when this event was produced. ISO 8601 date (YYYY-MM-DD) or partial + * (YYYY-MM or YYYY). + */ + event_date: string | null; + + /** + * ID of the event group that owns this event. + */ + event_group_id: string; + + /** + * Stable identifier for this event. Safe to use for client-side deduplication + * across pagination and retries. + */ + event_id: string; + + /** + * The full output from the prior run, including all fields and basis. + */ + previous_output: TaskRunAPI.TaskRunTextOutput | TaskRunAPI.TaskRunJsonOutput; + + /** + * Discriminant for the snapshot event variant. + */ + event_type?: 'snapshot'; +} + +/** + * Runtime output state for a `snapshot` monitor. + */ +export interface MonitorSnapshotOutput { + /** + * Task run output from the most recent completed execution of this snapshot + * monitor — same structure as the output of the original task run the monitor was + * created from. `null` until the first run completes. + */ + latest_snapshot?: TaskRunAPI.TaskRunTextOutput | TaskRunAPI.TaskRunJsonOutput | null; +} + +/** + * Configuration settings for a `snapshot` monitor. + */ +export interface MonitorSnapshotResponseSettings { + /** + * The original task input from the baseline task run that this monitor tracks. + */ + query: string; + + /** + * ID of the task run used as the monitoring baseline. + */ + task_run_id: string; + + /** + * JSON schema for a task input or output. + */ + output_schema?: TaskRunAPI.JsonSchema | null; +} + +/** + * Type-specific settings for a `snapshot` monitor. + */ +export interface MonitorSnapshotSettings { + /** + * Task run ID whose output becomes the data and schema for the monitor. + */ + task_run_id: string; +} + +/** + * Webhook configuration for a monitor. + */ +export interface MonitorWebhook { + /** + * URL for the webhook. + */ + url: string; + + /** + * Event types to send the webhook notifications for. + */ + event_types?: Array<'monitor.event.detected' | 'monitor.execution.completed' | 'monitor.execution.failed'>; +} + +/** + * Paginated list of monitor events, newest first. + */ +export interface PaginatedMonitorEvents { + /** + * Monitor events returned by this request, ordered newest first. + */ + events: Array; + + /** + * Pass as `cursor` to retrieve more events. Absent when there are no more events. + */ + next_cursor?: string | null; + + /** + * Execution caveats for this page of events, e.g. compute limits. + */ + warnings?: Array | null; +} + +/** + * Paginated list of monitors. + */ +export interface PaginatedMonitorResponse { + /** + * List of monitors for the current page. + */ + monitors: Array; + + /** + * Opaque pagination token. Pass as `cursor` to retrieve the next page. Absent when + * there are no more pages. + */ + next_cursor?: string | null; +} + +/** + * Type-specific update settings for an `event_stream` monitor. + */ +export interface UpdateMonitorEventStreamSettings { + /** + * Advanced monitor configuration. + */ + advanced_settings?: AdvancedMonitorSettings | null; +} + +/** + * Request body to update a monitor. + * + * Only fields that are explicitly included in the request body are updated. Pass + * `null` for `webhook` or `metadata` to clear those fields. To update + * type-specific settings on an `event_stream` monitor, include `type` and + * `settings`; pass `null` for `settings.advanced_settings` to clear it. At least + * one non-`type` field must be provided. + */ +export interface UpdateMonitorRequest { + /** + * Frequency of the monitor. Format: '' where unit is 'h' (hours), + * 'd' (days), or 'w' (weeks). Must be between 1h and 30d (inclusive). + */ + frequency?: string | null; + + /** + * User-provided metadata stored with the monitor and echoed back in webhook + * notifications and GET responses, so you can map events to objects in your + * application. Keys: max 16 chars; values: max 512 chars. + */ + metadata?: { [key: string]: string } | null; + + /** + * Type-specific update settings for an `event_stream` monitor. + */ + settings?: UpdateMonitorEventStreamSettings | null; + + /** + * Type of the monitor being updated. Required when `settings` is provided; must be + * `event_stream` (snapshot monitors have no updatable type-specific settings). + */ + type?: 'event_stream' | 'snapshot' | null; + + /** + * Webhook configuration for a monitor. + */ + webhook?: MonitorWebhook | null; +} + +export interface MonitorCreateParams { + /** + * Frequency of the monitor. Format: '' where unit is 'h' (hours), + * 'd' (days), or 'w' (weeks). Must be between 1h and 30d (inclusive). + */ + frequency: string; + + /** + * Type-specific settings for the monitor. The expected shape is determined by the + * root `type` field: pass `MonitorEventStreamSettings` when `type` is + * `event_stream`, and `MonitorSnapshotSettings` when `type` is `snapshot`. + */ + settings: MonitorEventStreamSettings | MonitorSnapshotSettings; + + /** + * Type of monitor to create. `event_stream` monitors a search query for material + * changes; `snapshot` monitors a specific task run's output. Determines the + * expected shape of `settings`. + */ + type: 'event_stream' | 'snapshot'; + + /** + * User-provided metadata stored with the monitor and echoed back in webhook + * notifications and GET responses, so you can map events to objects in your + * application. Keys: max 16 chars; values: max 512 chars. + */ + metadata?: { [key: string]: string } | null; + + /** + * Processor to use for the monitor. `lite` is faster and cheaper; `base` performs + * more thorough analysis at higher cost and latency. Defaults to `lite`. + */ + processor?: 'lite' | 'base'; + + /** + * Webhook configuration for a monitor. + */ + webhook?: MonitorWebhook | null; +} + +export interface MonitorUpdateParams { + /** + * Frequency of the monitor. Format: '' where unit is 'h' (hours), + * 'd' (days), or 'w' (weeks). Must be between 1h and 30d (inclusive). + */ + frequency?: string | null; + + /** + * User-provided metadata stored with the monitor and echoed back in webhook + * notifications and GET responses, so you can map events to objects in your + * application. Keys: max 16 chars; values: max 512 chars. + */ + metadata?: { [key: string]: string } | null; + + /** + * Type-specific update settings for an `event_stream` monitor. + */ + settings?: UpdateMonitorEventStreamSettings | null; + + /** + * Type of the monitor being updated. Required when `settings` is provided; must be + * `event_stream` (snapshot monitors have no updatable type-specific settings). + */ + type?: 'event_stream' | 'snapshot' | null; + + /** + * Webhook configuration for a monitor. + */ + webhook?: MonitorWebhook | null; +} + +export interface MonitorListParams { + /** + * Pagination token from `next_cursor` in a previous response. Omit to start from + * the most recently created monitor. + */ + cursor?: string | null; + + /** + * Maximum number of monitors to return. Defaults to 100. Between 1 and 10000. + */ + limit?: number | null; + + /** + * Filter by monitor status. Pass multiple times to filter by multiple values. + * Defaults to `active` only. + */ + status?: Array<'active' | 'cancelled'> | null; + + /** + * Filter by monitor type. Pass multiple times to filter by multiple values. Omit + * to return all types. + */ + type?: Array<'event_stream' | 'snapshot'> | null; +} + +export interface MonitorEventsParams { + /** + * Pass `next_cursor` from a previous response to retrieve more events. + */ + cursor?: string | null; + + /** + * Filter to a single execution. Values come from `event_group_id` in webhook + * events and listed events. Pagination params are ignored when set. + */ + event_group_id?: string | null; + + /** + * When true, include completion events for executions that ran but detected no + * material changes. Useful for auditing execution history. + */ + include_completions?: boolean; + + /** + * Maximum number of events to return. Defaults to 20. Between 1 and 100. + */ + limit?: number | null; +} + +export declare namespace MonitorResource { + export { + type AdvancedMonitorSettings as AdvancedMonitorSettings, + type CreateMonitorRequest as CreateMonitorRequest, + type Monitor as Monitor, + type MonitorCompletionEvent as MonitorCompletionEvent, + type MonitorErrorEvent as MonitorErrorEvent, + type MonitorEventStreamEvent as MonitorEventStreamEvent, + type MonitorEventStreamResponseSettings as MonitorEventStreamResponseSettings, + type MonitorEventStreamSettings as MonitorEventStreamSettings, + type MonitorSnapshotEvent as MonitorSnapshotEvent, + type MonitorSnapshotOutput as MonitorSnapshotOutput, + type MonitorSnapshotResponseSettings as MonitorSnapshotResponseSettings, + type MonitorSnapshotSettings as MonitorSnapshotSettings, + type MonitorWebhook as MonitorWebhook, + type PaginatedMonitorEvents as PaginatedMonitorEvents, + type PaginatedMonitorResponse as PaginatedMonitorResponse, + type UpdateMonitorEventStreamSettings as UpdateMonitorEventStreamSettings, + type UpdateMonitorRequest as UpdateMonitorRequest, + type MonitorCreateParams as MonitorCreateParams, + type MonitorUpdateParams as MonitorUpdateParams, + type MonitorListParams as MonitorListParams, + type MonitorEventsParams as MonitorEventsParams, + }; +} diff --git a/src/resources/task-group.ts b/src/resources/task-group.ts index 78ad0f7..5f80745 100644 --- a/src/resources/task-group.ts +++ b/src/resources/task-group.ts @@ -1,7 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../core/resource'; -import * as TaskGroupAPI from './task-group'; import * as TaskRunAPI from './task-run'; import * as BetaTaskRunAPI from './beta/task-run'; import { APIPromise } from '../core/api-promise'; @@ -99,6 +98,23 @@ export class TaskGroup extends APIResource { stream: true, }) as APIPromise>; } + + /** + * Retrieves run status by run_id. + * + * This endpoint is equivalent to fetching run status directly using the + * `retrieve()` method or the `tasks/runs` GET endpoint. + * + * The run result is available from the `/result` endpoint. + */ + retrieveRun( + runID: string, + params: TaskGroupRetrieveRunParams, + options?: RequestOptions, + ): APIPromise { + const { taskgroup_id } = params; + return this._client.get(path`/v1/tasks/groups/${taskgroup_id}/runs/${runID}`, options); + } } /** @@ -189,33 +205,28 @@ export interface TaskGroupStatus { /** * Event indicating an update to group status. */ -export type TaskGroupEventsResponse = - | TaskGroupEventsResponse.TaskGroupStatusEvent - | TaskRunAPI.TaskRunEvent - | TaskRunAPI.ErrorEvent; +export interface TaskGroupStatusEvent { + /** + * Cursor to resume the event stream. + */ + event_id: string; -export namespace TaskGroupEventsResponse { /** - * Event indicating an update to group status. + * Task group status object. */ - export interface TaskGroupStatusEvent { - /** - * Cursor to resume the event stream. - */ - event_id: string; - - /** - * Task group status object. - */ - status: TaskGroupAPI.TaskGroupStatus; - - /** - * Event type; always 'task_group_status'. - */ - type: 'task_group_status'; - } + status: TaskGroupStatus; + + /** + * Event type; always 'task_group_status'. + */ + type: 'task_group_status'; } +/** + * Event indicating an update to group status. + */ +export type TaskGroupEventsResponse = TaskGroupStatusEvent | TaskRunAPI.TaskRunEvent | TaskRunAPI.ErrorEvent; + /** * Event when a task run transitions to a non-active status. * @@ -283,16 +294,22 @@ export interface TaskGroupGetRunsParams { | null; } +export interface TaskGroupRetrieveRunParams { + taskgroup_id: string; +} + export declare namespace TaskGroup { export { type TaskGroup as TaskGroup, type TaskGroupRunResponse as TaskGroupRunResponse, type TaskGroupStatus as TaskGroupStatus, + type TaskGroupStatusEvent as TaskGroupStatusEvent, type TaskGroupEventsResponse as TaskGroupEventsResponse, type TaskGroupGetRunsResponse as TaskGroupGetRunsResponse, type TaskGroupCreateParams as TaskGroupCreateParams, type TaskGroupAddRunsParams as TaskGroupAddRunsParams, type TaskGroupEventsParams as TaskGroupEventsParams, type TaskGroupGetRunsParams as TaskGroupGetRunsParams, + type TaskGroupRetrieveRunParams as TaskGroupRetrieveRunParams, }; } diff --git a/src/resources/task-run.ts b/src/resources/task-run.ts index b13cfd2..c5c3410 100644 --- a/src/resources/task-run.ts +++ b/src/resources/task-run.ts @@ -107,6 +107,20 @@ export class TaskRun extends APIResource { ]), }); } + + /** + * Retrieves the input of a run by run_id. + * + * @example + * ```ts + * const runInput = await client.taskRun.retrieveInput( + * 'run_id', + * ); + * ``` + */ + retrieveInput(runID: string, options?: RequestOptions): APIPromise { + return this._client.get(path`/v1/tasks/runs/${runID}/input`, options); + } } /** @@ -278,7 +292,7 @@ export interface RunInput { /** * Advanced search configuration for a task run. */ - advanced_settings?: RunInput.AdvancedSettings | null; + advanced_settings?: TaskAdvancedSettings | null; /** * Controls tracking of task run execution progress. When set to true, progress @@ -329,16 +343,14 @@ export interface RunInput { webhook?: Webhook | null; } -export namespace RunInput { +/** + * Advanced search configuration for a task run. + */ +export interface TaskAdvancedSettings { /** - * Advanced search configuration for a task run. + * ISO 3166-1 alpha-2 country code for geo-targeted search results. */ - export interface AdvancedSettings { - /** - * ISO 3166-1 alpha-2 country code for geo-targeted search results. - */ - location?: string | null; - } + location?: string | null; } /** @@ -476,6 +488,52 @@ export interface TaskRunJsonOutput { output_schema?: { [key: string]: unknown } | null; } +/** + * A message for a task run progress update. + */ +export interface TaskRunProgressMessageEvent { + /** + * Progress update message. + */ + message: string; + + /** + * Timestamp of the message. + */ + timestamp: string | null; + + /** + * Event type; always starts with 'task_run.progress_msg'. + */ + type: + | 'task_run.progress_msg.plan' + | 'task_run.progress_msg.search' + | 'task_run.progress_msg.result' + | 'task_run.progress_msg.tool_call' + | 'task_run.progress_msg.exec_status'; +} + +/** + * A progress update for a task run. + */ +export interface TaskRunProgressStatsEvent { + /** + * Completion percentage of the task run. Ranges from 0 to 100 where 0 indicates no + * progress and 100 indicates completion. + */ + progress_meter: number; + + /** + * Source stats describing progress so far. + */ + source_stats: TaskRunSourceStats; + + /** + * Event type; always 'task_run.progress_stats'. + */ + type: 'task_run.progress_stats'; +} + /** * Result of a task run. */ @@ -491,6 +549,26 @@ export interface TaskRunResult { run: TaskRun; } +/** + * Source stats for a task run. + */ +export interface TaskRunSourceStats { + /** + * Number of sources considered in processing the task. + */ + num_sources_considered: number | null; + + /** + * Number of sources read in processing the task. + */ + num_sources_read: number | null; + + /** + * A sample of URLs of sources read in processing the task. + */ + sources_read_sample: Array | null; +} + /** * Output from a task that returns text. */ @@ -581,81 +659,11 @@ export interface Webhook { * A progress update for a task run. */ export type TaskRunEventsResponse = - | TaskRunEventsResponse.TaskRunProgressStatsEvent - | TaskRunEventsResponse.TaskRunProgressMessageEvent + | TaskRunProgressStatsEvent + | TaskRunProgressMessageEvent | TaskRunEvent | ErrorEvent; -export namespace TaskRunEventsResponse { - /** - * A progress update for a task run. - */ - export interface TaskRunProgressStatsEvent { - /** - * Completion percentage of the task run. Ranges from 0 to 100 where 0 indicates no - * progress and 100 indicates completion. - */ - progress_meter: number; - - /** - * Source stats describing progress so far. - */ - source_stats: TaskRunProgressStatsEvent.SourceStats; - - /** - * Event type; always 'task_run.progress_stats'. - */ - type: 'task_run.progress_stats'; - } - - export namespace TaskRunProgressStatsEvent { - /** - * Source stats describing progress so far. - */ - export interface SourceStats { - /** - * Number of sources considered in processing the task. - */ - num_sources_considered: number | null; - - /** - * Number of sources read in processing the task. - */ - num_sources_read: number | null; - - /** - * A sample of URLs of sources read in processing the task. - */ - sources_read_sample: Array | null; - } - } - - /** - * A message for a task run progress update. - */ - export interface TaskRunProgressMessageEvent { - /** - * Progress update message. - */ - message: string; - - /** - * Timestamp of the message. - */ - timestamp: string | null; - - /** - * Event type; always starts with 'task_run.progress_msg'. - */ - type: - | 'task_run.progress_msg.plan' - | 'task_run.progress_msg.search' - | 'task_run.progress_msg.result' - | 'task_run.progress_msg.tool_call' - | 'task_run.progress_msg.exec_status'; - } -} - export interface TaskRunCreateParams { /** * Body param: Input to the task, either text or a JSON object. @@ -670,7 +678,7 @@ export interface TaskRunCreateParams { /** * Body param: Advanced search configuration for a task run. */ - advanced_settings?: TaskRunCreateParams.AdvancedSettings | null; + advanced_settings?: TaskAdvancedSettings | null; /** * Body param: Controls tracking of task run execution progress. When set to true, @@ -726,18 +734,6 @@ export interface TaskRunCreateParams { betas?: Array; } -export namespace TaskRunCreateParams { - /** - * Advanced search configuration for a task run. - */ - export interface AdvancedSettings { - /** - * ISO 3166-1 alpha-2 country code for geo-targeted search results. - */ - location?: string | null; - } -} - export interface TaskRunResultParams { /** * Query param @@ -760,10 +756,14 @@ export declare namespace TaskRun { type McpServer as McpServer, type McpToolCall as McpToolCall, type RunInput as RunInput, + type TaskAdvancedSettings as TaskAdvancedSettings, type TaskRun as TaskRun, type TaskRunEvent as TaskRunEvent, type TaskRunJsonOutput as TaskRunJsonOutput, + type TaskRunProgressMessageEvent as TaskRunProgressMessageEvent, + type TaskRunProgressStatsEvent as TaskRunProgressStatsEvent, type TaskRunResult as TaskRunResult, + type TaskRunSourceStats as TaskRunSourceStats, type TaskRunTextOutput as TaskRunTextOutput, type TaskSpec as TaskSpec, type TextSchema as TextSchema, diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts index 8171ddd..5ae45cd 100644 --- a/src/resources/top-level.ts +++ b/src/resources/top-level.ts @@ -23,21 +23,7 @@ export interface AdvancedExtractSettings { * Controls full content extraction. Set to true to enable with defaults, false to * disable, or provide FullContentSettings for fine-grained control. */ - full_content?: AdvancedExtractSettings.FullContentSettings | boolean; -} - -export namespace AdvancedExtractSettings { - /** - * Optional settings for returning full content. - */ - export interface FullContentSettings { - /** - * Optional limit on the number of characters to include in the full content for - * each url. Full content always starts at the beginning of the page and is - * truncated at the limit if necessary. - */ - max_chars_per_result?: number | null; - } + full_content?: FullContentSettings | boolean; } /** @@ -198,6 +184,18 @@ export interface FetchPolicy { timeout_seconds?: number | null; } +/** + * Optional settings for returning full content. + */ +export interface FullContentSettings { + /** + * Optional limit on the number of characters to include in the full content for + * each url. Full content always starts at the beginning of the page and is + * truncated at the limit if necessary. + */ + max_chars_per_result?: number | null; +} + /** * Search response. */ @@ -376,6 +374,7 @@ export declare namespace TopLevel { type ExtractResponse as ExtractResponse, type ExtractResult as ExtractResult, type FetchPolicy as FetchPolicy, + type FullContentSettings as FullContentSettings, type SearchResult as SearchResult, type UsageItem as UsageItem, type WebSearchResult as WebSearchResult, diff --git a/tests/api-resources/monitor.test.ts b/tests/api-resources/monitor.test.ts new file mode 100644 index 0000000..2bcd474 --- /dev/null +++ b/tests/api-resources/monitor.test.ts @@ -0,0 +1,153 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import Parallel from 'parallel-web'; + +const client = new Parallel({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource monitor', () => { + test('create: only required params', async () => { + const responsePromise = client.monitor.create({ + frequency: '1h', + settings: { query: 'Extract recent news about AI' }, + type: 'event_stream', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('create: required and optional params', async () => { + const response = await client.monitor.create({ + frequency: '1h', + settings: { + query: 'Extract recent news about AI', + advanced_settings: { + location: 'us', + source_policy: { + after_date: '2024-01-01', + exclude_domains: ['reddit.com', 'x.com', '.ai'], + include_domains: ['wikipedia.org', 'usa.gov', '.edu'], + }, + }, + include_backfill: true, + output_schema: { + json_schema: { + additionalProperties: 'bar', + properties: 'bar', + required: 'bar', + type: 'bar', + }, + type: 'json', + }, + }, + type: 'event_stream', + metadata: { slack_thread_id: '1234567890.123456', user_id: 'U123ABC' }, + processor: 'lite', + webhook: { url: 'https://example.com/webhook', event_types: ['monitor.event.detected'] }, + }); + }); + + test('retrieve', async () => { + const responsePromise = client.monitor.retrieve('monitor_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('update', async () => { + const responsePromise = client.monitor.update('monitor_id', {}); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('list', async () => { + const responsePromise = client.monitor.list(); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('list: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.monitor.list( + { + cursor: 'cursor', + limit: 1, + status: ['active'], + type: ['event_stream'], + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(Parallel.NotFoundError); + }); + + test('cancel', async () => { + const responsePromise = client.monitor.cancel('monitor_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('events', async () => { + const responsePromise = client.monitor.events('monitor_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('events: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.monitor.events( + 'monitor_id', + { + cursor: 'cursor', + event_group_id: 'event_group_id', + include_completions: true, + limit: 1, + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(Parallel.NotFoundError); + }); + + test('trigger', async () => { + const responsePromise = client.monitor.trigger('monitor_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); +}); diff --git a/tests/api-resources/task-group.test.ts b/tests/api-resources/task-group.test.ts index b0ad4d9..a46d7fc 100644 --- a/tests/api-resources/task-group.test.ts +++ b/tests/api-resources/task-group.test.ts @@ -147,4 +147,19 @@ describe('resource taskGroup', () => { ), ).rejects.toThrow(Parallel.NotFoundError); }); + + test('retrieveRun: only required params', async () => { + const responsePromise = client.taskGroup.retrieveRun('run_id', { taskgroup_id: 'taskgroup_id' }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('retrieveRun: required and optional params', async () => { + const response = await client.taskGroup.retrieveRun('run_id', { taskgroup_id: 'taskgroup_id' }); + }); }); diff --git a/tests/api-resources/task-run.test.ts b/tests/api-resources/task-run.test.ts index 78f6fb6..c6c1ae3 100644 --- a/tests/api-resources/task-run.test.ts +++ b/tests/api-resources/task-run.test.ts @@ -104,4 +104,15 @@ describe('resource taskRun', () => { ), ).rejects.toThrow(Parallel.NotFoundError); }); + + test('retrieveInput', async () => { + const responsePromise = client.taskRun.retrieveInput('run_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); }); diff --git a/tests/qs/empty-keys-cases.ts b/tests/qs/empty-keys-cases.ts new file mode 100644 index 0000000..ea2c1b0 --- /dev/null +++ b/tests/qs/empty-keys-cases.ts @@ -0,0 +1,271 @@ +export const empty_test_cases = [ + { + input: '&', + with_empty_keys: {}, + stringify_output: { + brackets: '', + indices: '', + repeat: '', + }, + no_empty_keys: {}, + }, + { + input: '&&', + with_empty_keys: {}, + stringify_output: { + brackets: '', + indices: '', + repeat: '', + }, + no_empty_keys: {}, + }, + { + input: '&=', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '&=&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '&=&=', + with_empty_keys: { '': ['', ''] }, + stringify_output: { + brackets: '[]=&[]=', + indices: '[0]=&[1]=', + repeat: '=&=', + }, + no_empty_keys: {}, + }, + { + input: '&=&=&', + with_empty_keys: { '': ['', ''] }, + stringify_output: { + brackets: '[]=&[]=', + indices: '[0]=&[1]=', + repeat: '=&=', + }, + no_empty_keys: {}, + }, + { + input: '=', + with_empty_keys: { '': '' }, + no_empty_keys: {}, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + }, + { + input: '=&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '=&&&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '=&=&=&', + with_empty_keys: { '': ['', '', ''] }, + stringify_output: { + brackets: '[]=&[]=&[]=', + indices: '[0]=&[1]=&[2]=', + repeat: '=&=&=', + }, + no_empty_keys: {}, + }, + { + input: '=&a[]=b&a[1]=c', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: '=a', + with_empty_keys: { '': 'a' }, + no_empty_keys: {}, + stringify_output: { + brackets: '=a', + indices: '=a', + repeat: '=a', + }, + }, + { + input: 'a==a', + with_empty_keys: { a: '=a' }, + no_empty_keys: { a: '=a' }, + stringify_output: { + brackets: 'a==a', + indices: 'a==a', + repeat: 'a==a', + }, + }, + { + input: '=&a[]=b', + with_empty_keys: { '': '', a: ['b'] }, + stringify_output: { + brackets: '=&a[]=b', + indices: '=&a[0]=b', + repeat: '=&a=b', + }, + no_empty_keys: { a: ['b'] }, + }, + { + input: '=&a[]=b&a[]=c&a[2]=d', + with_empty_keys: { '': '', a: ['b', 'c', 'd'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c&a[]=d', + indices: '=&a[0]=b&a[1]=c&a[2]=d', + repeat: '=&a=b&a=c&a=d', + }, + no_empty_keys: { a: ['b', 'c', 'd'] }, + }, + { + input: '=a&=b', + with_empty_keys: { '': ['a', 'b'] }, + stringify_output: { + brackets: '[]=a&[]=b', + indices: '[0]=a&[1]=b', + repeat: '=a&=b', + }, + no_empty_keys: {}, + }, + { + input: '=a&foo=b', + with_empty_keys: { '': 'a', foo: 'b' }, + no_empty_keys: { foo: 'b' }, + stringify_output: { + brackets: '=a&foo=b', + indices: '=a&foo=b', + repeat: '=a&foo=b', + }, + }, + { + input: 'a[]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a[]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a[0]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a=b&a[]=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a=b&a[0]=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: '[]=a&[]=b& []=1', + with_empty_keys: { '': ['a', 'b'], ' ': ['1'] }, + stringify_output: { + brackets: '[]=a&[]=b& []=1', + indices: '[0]=a&[1]=b& [0]=1', + repeat: '=a&=b& =1', + }, + no_empty_keys: { 0: 'a', 1: 'b', ' ': ['1'] }, + }, + { + input: '[0]=a&[1]=b&a[0]=1&a[1]=2', + with_empty_keys: { '': ['a', 'b'], a: ['1', '2'] }, + no_empty_keys: { 0: 'a', 1: 'b', a: ['1', '2'] }, + stringify_output: { + brackets: '[]=a&[]=b&a[]=1&a[]=2', + indices: '[0]=a&[1]=b&a[0]=1&a[1]=2', + repeat: '=a&=b&a=1&a=2', + }, + }, + { + input: '[deep]=a&[deep]=2', + with_empty_keys: { '': { deep: ['a', '2'] } }, + stringify_output: { + brackets: '[deep][]=a&[deep][]=2', + indices: '[deep][0]=a&[deep][1]=2', + repeat: '[deep]=a&[deep]=2', + }, + no_empty_keys: { deep: ['a', '2'] }, + }, + { + input: '%5B0%5D=a&%5B1%5D=b', + with_empty_keys: { '': ['a', 'b'] }, + stringify_output: { + brackets: '[]=a&[]=b', + indices: '[0]=a&[1]=b', + repeat: '=a&=b', + }, + no_empty_keys: { 0: 'a', 1: 'b' }, + }, +] satisfies { + input: string; + with_empty_keys: Record; + stringify_output: { + brackets: string; + indices: string; + repeat: string; + }; + no_empty_keys: Record; +}[]; diff --git a/tests/qs/stringify.test.ts b/tests/qs/stringify.test.ts new file mode 100644 index 0000000..1c66332 --- /dev/null +++ b/tests/qs/stringify.test.ts @@ -0,0 +1,2232 @@ +import iconv from 'iconv-lite'; +import { stringify } from 'parallel-web/internal/qs'; +import { encode } from 'parallel-web/internal/qs/utils'; +import { StringifyOptions } from 'parallel-web/internal/qs/types'; +import { empty_test_cases } from './empty-keys-cases'; +import assert from 'assert'; + +describe('stringify()', function () { + test('stringifies a querystring object', function () { + expect(stringify({ a: 'b' })).toBe('a=b'); + expect(stringify({ a: 1 })).toBe('a=1'); + expect(stringify({ a: 1, b: 2 })).toBe('a=1&b=2'); + expect(stringify({ a: 'A_Z' })).toBe('a=A_Z'); + expect(stringify({ a: '€' })).toBe('a=%E2%82%AC'); + expect(stringify({ a: '' })).toBe('a=%EE%80%80'); + expect(stringify({ a: 'א' })).toBe('a=%D7%90'); + expect(stringify({ a: '𐐷' })).toBe('a=%F0%90%90%B7'); + }); + + test('stringifies falsy values', function () { + expect(stringify(undefined)).toBe(''); + expect(stringify(null)).toBe(''); + expect(stringify(null, { strictNullHandling: true })).toBe(''); + expect(stringify(false)).toBe(''); + expect(stringify(0)).toBe(''); + }); + + test('stringifies symbols', function () { + expect(stringify(Symbol.iterator)).toBe(''); + expect(stringify([Symbol.iterator])).toBe('0=Symbol%28Symbol.iterator%29'); + expect(stringify({ a: Symbol.iterator })).toBe('a=Symbol%28Symbol.iterator%29'); + expect(stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=Symbol%28Symbol.iterator%29', + ); + }); + + test('stringifies bigints', function () { + var three = BigInt(3); + // @ts-expect-error + var encodeWithN = function (value, defaultEncoder, charset) { + var result = defaultEncoder(value, defaultEncoder, charset); + return typeof value === 'bigint' ? result + 'n' : result; + }; + + expect(stringify(three)).toBe(''); + expect(stringify([three])).toBe('0=3'); + expect(stringify([three], { encoder: encodeWithN })).toBe('0=3n'); + expect(stringify({ a: three })).toBe('a=3'); + expect(stringify({ a: three }, { encoder: encodeWithN })).toBe('a=3n'); + expect(stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=3'); + expect( + stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }), + ).toBe('a[]=3n'); + }); + + test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function () { + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: false }), + ).toBe('name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: false }), + ).toBe('name.obj.first=John&name.obj.last=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: true }), + ).toBe('name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true }), + ).toBe('name%252Eobj.first=John&name%252Eobj.last=Doe'); + + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: false, encodeDotInKeys: false }, + // ), + // 'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe', + // 'with allowDots false and encodeDotInKeys false', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: false }, + // ), + // 'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe', + // 'with allowDots false and encodeDotInKeys false', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: false, encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe', + // 'with allowDots false and encodeDotInKeys true', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', + // 'with allowDots true and encodeDotInKeys true', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: false, encodeDotInKeys: false }, + ), + ).toBe('name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: false }, + ), + ).toBe('name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: false, encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); + }); + + test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function () { + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', + // 'with allowDots undefined and encodeDotInKeys true', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); + }); + + test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function () { + // st.equal( + // stringify( + // { 'name.obj': { first: 'John', last: 'Doe' } }, + // { + // encodeDotInKeys: true, + // allowDots: true, + // encodeValuesOnly: true, + // }, + // ), + // 'name%2Eobj.first=John&name%2Eobj.last=Doe', + // ); + expect( + stringify( + { 'name.obj': { first: 'John', last: 'Doe' } }, + { + encodeDotInKeys: true, + allowDots: true, + encodeValuesOnly: true, + }, + ), + ).toBe('name%2Eobj.first=John&name%2Eobj.last=Doe'); + + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, + // ), + // 'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, + ), + ).toBe('name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'); + }); + + test('throws when `commaRoundTrip` is not a boolean', function () { + // st['throws']( + // function () { + // stringify({}, { commaRoundTrip: 'not a boolean' }); + // }, + // TypeError, + // 'throws when `commaRoundTrip` is not a boolean', + // ); + expect(() => { + // @ts-expect-error + stringify({}, { commaRoundTrip: 'not a boolean' }); + }).toThrow(TypeError); + }); + + test('throws when `encodeDotInKeys` is not a boolean', function () { + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); + }).toThrow(TypeError); + }); + + test('adds query prefix', function () { + // st.equal(stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b'); + expect(stringify({ a: 'b' }, { addQueryPrefix: true })).toBe('?a=b'); + }); + + test('with query prefix, outputs blank string given an empty object', function () { + // st.equal(stringify({}, { addQueryPrefix: true }), ''); + expect(stringify({}, { addQueryPrefix: true })).toBe(''); + }); + + test('stringifies nested falsy values', function () { + // st.equal(stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D='); + // st.equal( + // stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), + // 'a%5Bb%5D%5Bc%5D', + // ); + // st.equal(stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false'); + expect(stringify({ a: { b: { c: null } } })).toBe('a%5Bb%5D%5Bc%5D='); + expect(stringify({ a: { b: { c: null } } }, { strictNullHandling: true })).toBe('a%5Bb%5D%5Bc%5D'); + expect(stringify({ a: { b: { c: false } } })).toBe('a%5Bb%5D%5Bc%5D=false'); + }); + + test('stringifies a nested object', function () { + // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); + // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e'); + expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); + expect(stringify({ a: { b: { c: { d: 'e' } } } })).toBe('a%5Bb%5D%5Bc%5D%5Bd%5D=e'); + }); + + test('`allowDots` option: stringifies a nested object with dots notation', function () { + // st.equal(stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c'); + // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e'); + expect(stringify({ a: { b: 'c' } }, { allowDots: true })).toBe('a.b=c'); + expect(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true })).toBe('a.b.c.d=e'); + }); + + test('stringifies an array value', function () { + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }), + // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }), + // 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }), + // 'a=b%2Cc%2Cd', + // 'comma => comma', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }), + // 'a=b%2Cc%2Cd', + // 'comma round trip => comma', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }), + // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + // 'default => indices', + // ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' })).toBe( + 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' })).toBe( + 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', + ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' })).toBe('a=b%2Cc%2Cd'); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( + 'a=b%2Cc%2Cd', + ); + expect(stringify({ a: ['b', 'c', 'd'] })).toBe('a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d'); + }); + + test('`skipNulls` option', function () { + // st.equal( + // stringify({ a: 'b', c: null }, { skipNulls: true }), + // 'a=b', + // 'omits nulls when asked', + // ); + expect(stringify({ a: 'b', c: null }, { skipNulls: true })).toBe('a=b'); + + // st.equal( + // stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), + // 'a%5Bb%5D=c', + // 'omits nested nulls when asked', + // ); + expect(stringify({ a: { b: 'c', d: null } }, { skipNulls: true })).toBe('a%5Bb%5D=c'); + }); + + test('omits array indices when asked', function () { + // st.equal(stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d'); + expect(stringify({ a: ['b', 'c', 'd'] }, { indices: false })).toBe('a=b&a=c&a=d'); + }); + + test('omits object key/value pair when value is empty array', function () { + // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); + expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); + }); + + test('should not omit object key/value pair when value is empty array and when asked', function () { + // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); + // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz'); + // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz'); + expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); + expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false })).toBe('b=zz'); + expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true })).toBe('a[]&b=zz'); + }); + + test('should throw when allowEmptyArrays is not of type boolean', function () { + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); + }).toThrow(TypeError); + }); + + test('allowEmptyArrays + strictNullHandling', function () { + // st.equal( + // stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true }), + // 'testEmptyArray[]', + // ); + expect(stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true })).toBe( + 'testEmptyArray[]', + ); + }); + + describe('stringifies an array value with one item vs multiple items', function () { + test('non-array item', function () { + // s2t.equal( + // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a=c', + // ); + // s2t.equal( + // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a=c', + // ); + // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c'); + // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true })).toBe('a=c'); + }); + + test('array with a single item', function () { + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0]=c', + // ); + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=c', + // ); + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c', + // ); + // s2t.equal( + // stringify( + // { a: ['c'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a[]=c', + // ); // so it parses back as an array + // s2t.equal(stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a[0]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); + expect( + stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('a[]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true })).toBe('a[0]=c'); + }); + + test('array with multiple items', function () { + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0]=c&a[1]=d', + // ); + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=c&a[]=d', + // ); + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c,d', + // ); + // s2t.equal( + // stringify( + // { a: ['c', 'd'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a=c,d', + // ); + // s2t.equal(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d'); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[0]=c&a[1]=d', + ); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=c&a[]=d', + ); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c,d'); + expect( + stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('a=c,d'); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true })).toBe('a[0]=c&a[1]=d'); + }); + + test('array with multiple items with a comma inside', function () { + // s2t.equal( + // stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c%2Cd,e', + // ); + // s2t.equal(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce'); + expect(stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( + 'a=c%2Cd,e', + ); + expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' })).toBe('a=c%2Cd%2Ce'); + + // s2t.equal( + // stringify( + // { a: ['c,d', 'e'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a=c%2Cd,e', + // ); + // s2t.equal( + // stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), + // 'a=c%2Cd%2Ce', + // ); + expect( + stringify( + { a: ['c,d', 'e'] }, + { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + ), + ).toBe('a=c%2Cd,e'); + expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( + 'a=c%2Cd%2Ce', + ); + }); + }); + + test('stringifies a nested array value', function () { + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[b][0]=c&a[b][1]=d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[b][]=c&a[b][]=d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( + 'a[b]=c,d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true })).toBe('a[b][0]=c&a[b][1]=d'); + }); + + test('stringifies comma and empty array values', function () { + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), + // 'a[0]=,&a[1]=&a[2]=c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), + // 'a[]=,&a[]=&a[]=c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), + // 'a=,,,c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), + // 'a=,&a=&a=c,d%', + // ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' })).toBe( + 'a[0]=,&a[1]=&a[2]=c,d%', + ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'a[]=,&a[]=&a[]=c,d%', + ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' })).toBe('a=,,,c,d%'); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' })).toBe( + 'a=,&a=&a=c,d%', + ); + + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[]=%2C&a[]=&a[]=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a=%2C,,c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&a=&a=c%2Cd%25', + // ); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), + ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), + ).toBe('a=%2C%2C%2Cc%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + // ), + // 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + // ), + // 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + // ), + // 'a=%2C%2C%2Cc%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&a=&a=c%2Cd%25', + // ); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), + ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), + ).toBe('a=%2C%2C%2Cc%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + }); + + test('stringifies comma and empty non-array values', function () { + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), + // 'a=,&b=&c=c,d%', + // ); + expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' })).toBe( + 'a=,&b=&c=c,d%', + ); + expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'a=,&b=&c=c,d%', + ); + + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + }); + + test('stringifies a nested array value with dots notation', function () { + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a.b[0]=c&a.b[1]=d', + // 'indices: stringifies with dots + indices', + // ); + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a.b[]=c&a.b[]=d', + // 'brackets: stringifies with dots + brackets', + // ); + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a.b=c,d', + // 'comma: stringifies with dots + comma', + // ); + // st.equal( + // stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true }), + // 'a.b[0]=c&a.b[1]=d', + // 'default: stringifies with dots + indices', + // ); + expect( + stringify( + { a: { b: ['c', 'd'] } }, + { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a.b[0]=c&a.b[1]=d'); + expect( + stringify( + { a: { b: ['c', 'd'] } }, + { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a.b[]=c&a.b[]=d'); + expect( + stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }), + ).toBe('a.b=c,d'); + expect(stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true })).toBe( + 'a.b[0]=c&a.b[1]=d', + ); + }); + + test('stringifies an object inside an array', function () { + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), + // 'a[0][b]=c', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), + // 'a[b]=c', + // 'repeat => repeat', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), + // 'a[][b]=c', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }), + // 'a[0][b]=c', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( + 'a[0][b]=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe('a[b]=c'); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( + 'a[][b]=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true })).toBe('a[0][b]=c'); + + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), + // 'a[0][b][c][0]=1', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), + // 'a[b][c]=1', + // 'repeat => repeat', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), + // 'a[][b][c][]=1', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }), + // 'a[0][b][c][0]=1', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( + 'a[0][b][c][0]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe( + 'a[b][c]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( + 'a[][b][c][]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true })).toBe('a[0][b][c][0]=1'); + }); + + test('stringifies an array with mixed objects and primitives', function () { + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0][b]=1&a[1]=2&a[2]=3', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[][b]=1&a[]=2&a[]=3', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // '???', + // 'brackets => brackets', + // { skip: 'TODO: figure out what this should do' }, + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }), + // 'a[0][b]=1&a[1]=2&a[2]=3', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[0][b]=1&a[1]=2&a[2]=3', + ); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[][b]=1&a[]=2&a[]=3', + ); + // !Skipped: Figure out what this should do + // expect( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // ).toBe('???'); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true })).toBe('a[0][b]=1&a[1]=2&a[2]=3'); + }); + + test('stringifies an object inside an array with dots notation', function () { + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), + // 'a[0].b=c', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: [{ b: 'c' }] }, + // { allowDots: true, encode: false, arrayFormat: 'brackets' }, + // ), + // 'a[].b=c', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false }), + // 'a[0].b=c', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' })).toBe( + 'a[0].b=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' })).toBe( + 'a[].b=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false })).toBe('a[0].b=c'); + + // st.equal( + // stringify( + // { a: [{ b: { c: [1] } }] }, + // { allowDots: true, encode: false, arrayFormat: 'indices' }, + // ), + // 'a[0].b.c[0]=1', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: [{ b: { c: [1] } }] }, + // { allowDots: true, encode: false, arrayFormat: 'brackets' }, + // ), + // 'a[].b.c[]=1', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false }), + // 'a[0].b.c[0]=1', + // 'default => indices', + // ); + expect( + stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), + ).toBe('a[0].b.c[0]=1'); + expect( + stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' }), + ).toBe('a[].b.c[]=1'); + expect(stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false })).toBe('a[0].b.c[0]=1'); + }); + + test('does not omit object keys when indices = false', function () { + // st.equal(stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c'); + expect(stringify({ a: [{ b: 'c' }] }, { indices: false })).toBe('a%5Bb%5D=c'); + }); + + test('uses indices notation for arrays when indices=true', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { indices: true })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses indices notation for arrays when no arrayFormat is specified', function () { + // st.equal(stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses indices notation for arrays when arrayFormat=indices', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses repeat notation for arrays when arrayFormat=repeat', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })).toBe('a=b&a=c'); + }); + + test('uses brackets notation for arrays when arrayFormat=brackets', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })).toBe('a%5B%5D=b&a%5B%5D=c'); + }); + + test('stringifies a complicated object', function () { + // st.equal(stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e'); + expect(stringify({ a: { b: 'c', d: 'e' } })).toBe('a%5Bb%5D=c&a%5Bd%5D=e'); + }); + + test('stringifies an empty value', function () { + // st.equal(stringify({ a: '' }), 'a='); + // st.equal(stringify({ a: null }, { strictNullHandling: true }), 'a'); + expect(stringify({ a: '' })).toBe('a='); + expect(stringify({ a: null }, { strictNullHandling: true })).toBe('a'); + + // st.equal(stringify({ a: '', b: '' }), 'a=&b='); + // st.equal(stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b='); + expect(stringify({ a: '', b: '' })).toBe('a=&b='); + expect(stringify({ a: null, b: '' }, { strictNullHandling: true })).toBe('a&b='); + + // st.equal(stringify({ a: { b: '' } }), 'a%5Bb%5D='); + // st.equal(stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D'); + // st.equal(stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D='); + expect(stringify({ a: { b: '' } })).toBe('a%5Bb%5D='); + expect(stringify({ a: { b: null } }, { strictNullHandling: true })).toBe('a%5Bb%5D'); + expect(stringify({ a: { b: null } }, { strictNullHandling: false })).toBe('a%5Bb%5D='); + }); + + test('stringifies an empty array in different arrayFormat', function () { + // st.equal(stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c'); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false })).toBe('b[0]=&c=c'); + // arrayFormat default + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), + // 'b[0]=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), + // 'b[]=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), + // 'b=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), + // 'b=&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'b[]=&c=c', + // ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' })).toBe( + 'b[0]=&c=c', + ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'b[]=&c=c', + ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' })).toBe('b=&c=c'); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' })).toBe('b=&c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('b[]=&c=c'); + + // with strictNullHandling + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'indices', strictNullHandling: true }, + // ), + // 'b[0]&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, + // ), + // 'b[]&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, + // ), + // 'b&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', strictNullHandling: true }, + // ), + // 'b&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, + // ), + // 'b[]&c=c', + // ); + + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'indices', strictNullHandling: true }, + ), + ).toBe('b[0]&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, + ), + ).toBe('b[]&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, + ), + ).toBe('b&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'comma', strictNullHandling: true }, + ), + ).toBe('b&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, + ), + ).toBe('b[]&c=c'); + + // with skipNulls + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'indices', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'brackets', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'repeat', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', skipNulls: true }, + // ), + // 'c=c', + // ); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), + ).toBe('c=c'); + }); + + test('stringifies a null object', function () { + var obj = Object.create(null); + obj.a = 'b'; + // st.equal(stringify(obj), 'a=b'); + expect(stringify(obj)).toBe('a=b'); + }); + + test('returns an empty string for invalid input', function () { + // st.equal(stringify(undefined), ''); + // st.equal(stringify(false), ''); + // st.equal(stringify(null), ''); + // st.equal(stringify(''), ''); + expect(stringify(undefined)).toBe(''); + expect(stringify(false)).toBe(''); + expect(stringify(null)).toBe(''); + expect(stringify('')).toBe(''); + }); + + test('stringifies an object with a null object as a child', function () { + var obj = { a: Object.create(null) }; + + obj.a.b = 'c'; + // st.equal(stringify(obj), 'a%5Bb%5D=c'); + expect(stringify(obj)).toBe('a%5Bb%5D=c'); + }); + + test('drops keys with a value of undefined', function () { + // st.equal(stringify({ a: undefined }), ''); + expect(stringify({ a: undefined })).toBe(''); + + // st.equal( + // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), + // 'a%5Bc%5D', + // ); + // st.equal( + // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), + // 'a%5Bc%5D=', + // ); + // st.equal(stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D='); + expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true })).toBe('a%5Bc%5D'); + expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false })).toBe('a%5Bc%5D='); + expect(stringify({ a: { b: undefined, c: '' } })).toBe('a%5Bc%5D='); + }); + + test('url encodes values', function () { + // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); + expect(stringify({ a: 'b c' })).toBe('a=b%20c'); + }); + + test('stringifies a date', function () { + var now = new Date(); + var str = 'a=' + encodeURIComponent(now.toISOString()); + // st.equal(stringify({ a: now }), str); + expect(stringify({ a: now })).toBe(str); + }); + + test('stringifies the weird object from qs', function () { + // st.equal( + // stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), + // 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', + // ); + expect(stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' })).toBe( + 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', + ); + }); + + // TODO: Investigate how to to intercept in vitest + // TODO(rob) + test('skips properties that are part of the object prototype', function () { + // st.intercept(Object.prototype, 'crash', { value: 'test' }); + // @ts-expect-error + Object.prototype.crash = 'test'; + + // st.equal(stringify({ a: 'b' }), 'a=b'); + // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); + expect(stringify({ a: 'b' })).toBe('a=b'); + expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); + }); + + test('stringifies boolean values', function () { + // st.equal(stringify({ a: true }), 'a=true'); + // st.equal(stringify({ a: { b: true } }), 'a%5Bb%5D=true'); + // st.equal(stringify({ b: false }), 'b=false'); + // st.equal(stringify({ b: { c: false } }), 'b%5Bc%5D=false'); + expect(stringify({ a: true })).toBe('a=true'); + expect(stringify({ a: { b: true } })).toBe('a%5Bb%5D=true'); + expect(stringify({ b: false })).toBe('b=false'); + expect(stringify({ b: { c: false } })).toBe('b%5Bc%5D=false'); + }); + + test('stringifies buffer values', function () { + // st.equal(stringify({ a: Buffer.from('test') }), 'a=test'); + // st.equal(stringify({ a: { b: Buffer.from('test') } }), 'a%5Bb%5D=test'); + }); + + test('stringifies an object using an alternative delimiter', function () { + // st.equal(stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d'); + expect(stringify({ a: 'b', c: 'd' }, { delimiter: ';' })).toBe('a=b;c=d'); + }); + + // We dont target environments which dont even have Buffer + // test('does not blow up when Buffer global is missing', function () { + // var restore = mockProperty(global, 'Buffer', { delete: true }); + + // var result = stringify({ a: 'b', c: 'd' }); + + // restore(); + + // st.equal(result, 'a=b&c=d'); + // st.end(); + // }); + + test('does not crash when parsing circular references', function () { + var a: any = {}; + a.b = a; + + // st['throws']( + // function () { + // stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); + // }, + // /RangeError: Cyclic object value/, + // 'cyclic values throw', + // ); + expect(() => { + stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); + }).toThrow('Cyclic object value'); + + var circular: any = { + a: 'value', + }; + circular.a = circular; + // st['throws']( + // function () { + // stringify(circular); + // }, + // /RangeError: Cyclic object value/, + // 'cyclic values throw', + // ); + expect(() => { + stringify(circular); + }).toThrow('Cyclic object value'); + + var arr = ['a']; + // st.doesNotThrow(function () { + // stringify({ x: arr, y: arr }); + // }, 'non-cyclic values do not throw'); + expect(() => { + stringify({ x: arr, y: arr }); + }).not.toThrow(); + }); + + test('non-circular duplicated references can still work', function () { + var hourOfDay = { + function: 'hour_of_day', + }; + + var p1 = { + function: 'gte', + arguments: [hourOfDay, 0], + }; + var p2 = { + function: 'lte', + arguments: [hourOfDay, 23], + }; + + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', + // ); + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', + // ); + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', + // ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe( + 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', + ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe( + 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', + ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe( + 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', + ); + }); + + test('selects properties when filter=array', function () { + // st.equal(stringify({ a: 'b' }, { filter: ['a'] }), 'a=b'); + // st.equal(stringify({ a: 1 }, { filter: [] }), ''); + expect(stringify({ a: 'b' }, { filter: ['a'] })).toBe('a=b'); + expect(stringify({ a: 1 }, { filter: [] })).toBe(''); + + // st.equal( + // stringify( + // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + // { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, + // ), + // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + // { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, + // ), + // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] }), + // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + // 'default => indices', + // ); + expect(stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] })).toBe( + 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + ); + expect( + stringify( + { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, + ), + ).toBe('a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3'); + expect( + stringify( + { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, + ), + ).toBe('a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3'); + }); + + test('supports custom representations when filter=function', function () { + var calls = 0; + var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } }; + var filterFunc: StringifyOptions['filter'] = function (prefix, value) { + calls += 1; + if (calls === 1) { + // st.equal(prefix, '', 'prefix is empty'); + // st.equal(value, obj); + expect(prefix).toBe(''); + expect(value).toBe(obj); + } else if (prefix === 'c') { + return void 0; + } else if (value instanceof Date) { + // st.equal(prefix, 'e[f]'); + expect(prefix).toBe('e[f]'); + return value.getTime(); + } + return value; + }; + + // st.equal(stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000'); + // st.equal(calls, 5); + expect(stringify(obj, { filter: filterFunc })).toBe('a=b&e%5Bf%5D=1257894000000'); + expect(calls).toBe(5); + }); + + test('can disable uri encoding', function () { + // st.equal(stringify({ a: 'b' }, { encode: false }), 'a=b'); + // st.equal(stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c'); + // st.equal( + // stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), + // 'a=b&c', + // ); + expect(stringify({ a: 'b' }, { encode: false })).toBe('a=b'); + expect(stringify({ a: { b: 'c' } }, { encode: false })).toBe('a[b]=c'); + expect(stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false })).toBe('a=b&c'); + }); + + test('can sort the keys', function () { + // @ts-expect-error + var sort: NonNullable = function (a: string, b: string) { + return a.localeCompare(b); + }; + // st.equal(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y'); + // st.equal( + // stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), + // 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', + // ); + expect(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort })).toBe('a=c&b=f&z=y'); + expect(stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort })).toBe( + 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', + ); + }); + + test('can sort the keys at depth 3 or more too', function () { + // @ts-expect-error + var sort: NonNullable = function (a: string, b: string) { + return a.localeCompare(b); + }; + // st.equal( + // stringify( + // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + // { sort: sort, encode: false }, + // ), + // 'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb', + // ); + // st.equal( + // stringify( + // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + // { sort: null, encode: false }, + // ), + // 'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b', + // ); + expect( + stringify( + { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + { sort: sort, encode: false }, + ), + ).toBe('a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'); + expect( + stringify( + { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + { sort: null, encode: false }, + ), + ).toBe('a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'); + }); + + test('can stringify with custom encoding', function () { + // st.equal( + // stringify( + // { 県: '大阪府', '': '' }, + // { + // encoder: function (str) { + // if (str.length === 0) { + // return ''; + // } + // var buf = iconv.encode(str, 'shiftjis'); + // var result = []; + // for (var i = 0; i < buf.length; ++i) { + // result.push(buf.readUInt8(i).toString(16)); + // } + // return '%' + result.join('%'); + // }, + // }, + // ), + // '%8c%a7=%91%e5%8d%e3%95%7b&=', + // ); + expect( + stringify( + { 県: '大阪府', '': '' }, + { + encoder: function (str) { + if (str.length === 0) { + return ''; + } + var buf = iconv.encode(str, 'shiftjis'); + var result = []; + for (var i = 0; i < buf.length; ++i) { + result.push(buf.readUInt8(i).toString(16)); + } + return '%' + result.join('%'); + }, + }, + ), + ).toBe('%8c%a7=%91%e5%8d%e3%95%7b&='); + }); + + test('receives the default encoder as a second argument', function () { + // stringify( + // { a: 1, b: new Date(), c: true, d: [1] }, + // { + // encoder: function (str) { + // st.match(typeof str, /^(?:string|number|boolean)$/); + // return ''; + // }, + // }, + // ); + + stringify( + { a: 1, b: new Date(), c: true, d: [1] }, + { + encoder: function (str) { + // st.match(typeof str, /^(?:string|number|boolean)$/); + assert.match(typeof str, /^(?:string|number|boolean)$/); + return ''; + }, + }, + ); + }); + + test('receives the default encoder as a second argument', function () { + // stringify( + // { a: 1 }, + // { + // encoder: function (str, defaultEncoder) { + // st.equal(defaultEncoder, utils.encode); + // }, + // }, + // ); + + stringify( + { a: 1 }, + { + // @ts-ignore + encoder: function (_str, defaultEncoder) { + expect(defaultEncoder).toBe(encode); + }, + }, + ); + }); + + test('throws error with wrong encoder', function () { + // st['throws'](function () { + // stringify({}, { encoder: 'string' }); + // }, new TypeError('Encoder has to be a function.')); + // st.end(); + expect(() => { + // @ts-expect-error + stringify({}, { encoder: 'string' }); + }).toThrow(TypeError); + }); + + (typeof Buffer === 'undefined' ? test.skip : test)( + 'can use custom encoder for a buffer object', + function () { + // st.equal( + // stringify( + // { a: Buffer.from([1]) }, + // { + // encoder: function (buffer) { + // if (typeof buffer === 'string') { + // return buffer; + // } + // return String.fromCharCode(buffer.readUInt8(0) + 97); + // }, + // }, + // ), + // 'a=b', + // ); + expect( + stringify( + { a: Buffer.from([1]) }, + { + encoder: function (buffer) { + if (typeof buffer === 'string') { + return buffer; + } + return String.fromCharCode(buffer.readUInt8(0) + 97); + }, + }, + ), + ).toBe('a=b'); + + // st.equal( + // stringify( + // { a: Buffer.from('a b') }, + // { + // encoder: function (buffer) { + // return buffer; + // }, + // }, + // ), + // 'a=a b', + // ); + expect( + stringify( + { a: Buffer.from('a b') }, + { + encoder: function (buffer) { + return buffer; + }, + }, + ), + ).toBe('a=a b'); + }, + ); + + test('serializeDate option', function () { + var date = new Date(); + // st.equal( + // stringify({ a: date }), + // 'a=' + date.toISOString().replace(/:/g, '%3A'), + // 'default is toISOString', + // ); + expect(stringify({ a: date })).toBe('a=' + date.toISOString().replace(/:/g, '%3A')); + + var mutatedDate = new Date(); + mutatedDate.toISOString = function () { + throw new SyntaxError(); + }; + // st['throws'](function () { + // mutatedDate.toISOString(); + // }, SyntaxError); + expect(() => { + mutatedDate.toISOString(); + }).toThrow(SyntaxError); + // st.equal( + // stringify({ a: mutatedDate }), + // 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), + // 'toISOString works even when method is not locally present', + // ); + expect(stringify({ a: mutatedDate })).toBe( + 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), + ); + + var specificDate = new Date(6); + // st.equal( + // stringify( + // { a: specificDate }, + // { + // serializeDate: function (d) { + // return d.getTime() * 7; + // }, + // }, + // ), + // 'a=42', + // 'custom serializeDate function called', + // ); + expect( + stringify( + { a: specificDate }, + { + // @ts-ignore + serializeDate: function (d) { + return d.getTime() * 7; + }, + }, + ), + ).toBe('a=42'); + + // st.equal( + // stringify( + // { a: [date] }, + // { + // serializeDate: function (d) { + // return d.getTime(); + // }, + // arrayFormat: 'comma', + // }, + // ), + // 'a=' + date.getTime(), + // 'works with arrayFormat comma', + // ); + // st.equal( + // stringify( + // { a: [date] }, + // { + // serializeDate: function (d) { + // return d.getTime(); + // }, + // arrayFormat: 'comma', + // commaRoundTrip: true, + // }, + // ), + // 'a%5B%5D=' + date.getTime(), + // 'works with arrayFormat comma', + // ); + expect( + stringify( + { a: [date] }, + { + // @ts-expect-error + serializeDate: function (d) { + return d.getTime(); + }, + arrayFormat: 'comma', + }, + ), + ).toBe('a=' + date.getTime()); + expect( + stringify( + { a: [date] }, + { + // @ts-expect-error + serializeDate: function (d) { + return d.getTime(); + }, + arrayFormat: 'comma', + commaRoundTrip: true, + }, + ), + ).toBe('a%5B%5D=' + date.getTime()); + }); + + test('RFC 1738 serialization', function () { + // st.equal(stringify({ a: 'b c' }, { format: formats.RFC1738 }), 'a=b+c'); + // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC1738 }), 'a+b=c+d'); + // st.equal( + // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC1738 }), + // 'a+b=a+b', + // ); + expect(stringify({ a: 'b c' }, { format: 'RFC1738' })).toBe('a=b+c'); + expect(stringify({ 'a b': 'c d' }, { format: 'RFC1738' })).toBe('a+b=c+d'); + expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC1738' })).toBe('a+b=a+b'); + + // st.equal(stringify({ 'foo(ref)': 'bar' }, { format: formats.RFC1738 }), 'foo(ref)=bar'); + expect(stringify({ 'foo(ref)': 'bar' }, { format: 'RFC1738' })).toBe('foo(ref)=bar'); + }); + + test('RFC 3986 spaces serialization', function () { + // st.equal(stringify({ a: 'b c' }, { format: formats.RFC3986 }), 'a=b%20c'); + // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC3986 }), 'a%20b=c%20d'); + // st.equal( + // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC3986 }), + // 'a%20b=a%20b', + // ); + expect(stringify({ a: 'b c' }, { format: 'RFC3986' })).toBe('a=b%20c'); + expect(stringify({ 'a b': 'c d' }, { format: 'RFC3986' })).toBe('a%20b=c%20d'); + expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC3986' })).toBe('a%20b=a%20b'); + }); + + test('Backward compatibility to RFC 3986', function () { + // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); + // st.equal(stringify({ 'a b': Buffer.from('a b') }), 'a%20b=a%20b'); + expect(stringify({ a: 'b c' })).toBe('a=b%20c'); + expect(stringify({ 'a b': Buffer.from('a b') })).toBe('a%20b=a%20b'); + }); + + test('Edge cases and unknown formats', function () { + ['UFO1234', false, 1234, null, {}, []].forEach(function (format) { + // st['throws'](function () { + // stringify({ a: 'b c' }, { format: format }); + // }, new TypeError('Unknown format option provided.')); + expect(() => { + // @ts-expect-error + stringify({ a: 'b c' }, { format: format }); + }).toThrow(TypeError); + }); + }); + + test('encodeValuesOnly', function () { + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h', + // 'encodeValuesOnly + indices', + // ); + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h', + // 'encodeValuesOnly + brackets', + // ); + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=b&c=d&c=e%3Df&f=g&f=h', + // 'encodeValuesOnly + repeat', + // ); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h'); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'repeat' }, + ), + ).toBe('a=b&c=d&c=e%3Df&f=g&f=h'); + + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' }), + // 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', + // 'no encodeValuesOnly + indices', + // ); + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' }), + // 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', + // 'no encodeValuesOnly + brackets', + // ); + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' }), + // 'a=b&c=d&c=e&f=g&f=h', + // 'no encodeValuesOnly + repeat', + // ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' })).toBe( + 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', + ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' })).toBe( + 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', + ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' })).toBe( + 'a=b&c=d&c=e&f=g&f=h', + ); + }); + + test('encodeValuesOnly - strictNullHandling', function () { + // st.equal( + // stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true }), + // 'a[b]', + // ); + expect(stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true })).toBe('a[b]'); + }); + + test('throws if an invalid charset is specified', function () { + // st['throws'](function () { + // stringify({ a: 'b' }, { charset: 'foobar' }); + // }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined')); + expect(() => { + // @ts-expect-error + stringify({ a: 'b' }, { charset: 'foobar' }); + }).toThrow(TypeError); + }); + + test('respects a charset of iso-8859-1', function () { + // st.equal(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6'); + expect(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' })).toBe('%E6=%E6'); + }); + + test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function () { + // st.equal(stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B'); + expect(stringify({ a: '☺' }, { charset: 'iso-8859-1' })).toBe('a=%26%239786%3B'); + }); + + test('respects an explicit charset of utf-8 (the default)', function () { + // st.equal(stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6'); + expect(stringify({ a: 'æ' }, { charset: 'utf-8' })).toBe('a=%C3%A6'); + }); + + test('`charsetSentinel` option', function () { + // st.equal( + // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }), + // 'utf8=%E2%9C%93&a=%C3%A6', + // 'adds the right sentinel when instructed to and the charset is utf-8', + // ); + expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' })).toBe( + 'utf8=%E2%9C%93&a=%C3%A6', + ); + + // st.equal( + // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }), + // 'utf8=%26%2310003%3B&a=%E6', + // 'adds the right sentinel when instructed to and the charset is iso-8859-1', + // ); + expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' })).toBe( + 'utf8=%26%2310003%3B&a=%E6', + ); + }); + + test('does not mutate the options argument', function () { + var options = {}; + stringify({}, options); + // st.deepEqual(options, {}); + expect(options).toEqual({}); + }); + + test('strictNullHandling works with custom filter', function () { + // @ts-expect-error + var filter = function (_prefix, value) { + return value; + }; + + var options = { strictNullHandling: true, filter: filter }; + // st.equal(stringify({ key: null }, options), 'key'); + expect(stringify({ key: null }, options)).toBe('key'); + }); + + test('strictNullHandling works with null serializeDate', function () { + var serializeDate = function () { + return null; + }; + var options = { strictNullHandling: true, serializeDate: serializeDate }; + var date = new Date(); + // st.equal(stringify({ key: date }, options), 'key'); + // @ts-expect-error + expect(stringify({ key: date }, options)).toBe('key'); + }); + + test('allows for encoding keys and values differently', function () { + // @ts-expect-error + var encoder = function (str, defaultEncoder, charset, type) { + if (type === 'key') { + return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase(); + } + if (type === 'value') { + return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase(); + } + throw 'this should never happen! type: ' + type; + }; + + // st.deepEqual(stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE'); + expect(stringify({ KeY: 'vAlUe' }, { encoder: encoder })).toBe('key=VALUE'); + }); + + test('objects inside arrays', function () { + var obj = { a: { b: { c: 'd', e: 'f' } } }; + var withArray = { a: { b: [{ c: 'd', e: 'f' }] } }; + + // st.equal( + // stringify(obj, { encode: false }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, no arrayFormat', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'brackets' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, bracket', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'indices' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, indices', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'repeat' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, repeat', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'comma' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, comma', + // ); + expect(stringify(obj, { encode: false })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'indices' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'comma' })).toBe('a[b][c]=d&a[b][e]=f'); + + // st.equal( + // stringify(withArray, { encode: false }), + // 'a[b][0][c]=d&a[b][0][e]=f', + // 'array, no arrayFormat', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'brackets' }), + // 'a[b][][c]=d&a[b][][e]=f', + // 'array, bracket', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'indices' }), + // 'a[b][0][c]=d&a[b][0][e]=f', + // 'array, indices', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'repeat' }), + // 'a[b][c]=d&a[b][e]=f', + // 'array, repeat', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'comma' }), + // '???', + // 'array, comma', + // { skip: 'TODO: figure out what this should do' }, + // ); + expect(stringify(withArray, { encode: false })).toBe('a[b][0][c]=d&a[b][0][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][][c]=d&a[b][][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'indices' })).toBe('a[b][0][c]=d&a[b][0][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); + // !TODo: Figure out what this should do + // expect(stringify(withArray, { encode: false, arrayFormat: 'comma' })).toBe( + // 'a[b][c]=d&a[b][e]=f', + // ); + }); + + test('stringifies sparse arrays', function () { + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[1]=2&a[4]=1', + // ); + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=2&a[]=1', + // ); + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + // 'a=2&a=1', + // ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[1]=2&a[4]=1', + ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=2&a[]=1', + ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' })).toBe( + 'a=2&a=1', + ); + + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][b][2][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][b][][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[b][c]=1', + // ); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][b][2][c]=1'); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][b][][c]=1'); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[b][c]=1'); + + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][2][3][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][][][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[c]=1', + // ); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][2][3][c]=1'); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][][][c]=1'); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[c]=1'); + + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][2][3][c][1]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][][][c][]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[c]=1', + // ); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][2][3][c][1]=1'); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][][][c][]=1'); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[c]=1'); + }); + + test('encodes a very long string', function () { + var chars = []; + var expected = []; + for (var i = 0; i < 5e3; i++) { + chars.push(' ' + i); + + expected.push('%20' + i); + } + + var obj = { + foo: chars.join(''), + }; + + // st.equal( + // stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' }), + // 'foo=' + expected.join(''), + // ); + // @ts-expect-error + expect(stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' })).toBe('foo=' + expected.join('')); + }); +}); + +describe('stringifies empty keys', function () { + empty_test_cases.forEach(function (testCase) { + test('stringifies an object with empty string key with ' + testCase.input, function () { + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }), + // testCase.stringifyOutput.indices, + // 'test case: ' + testCase.input + ', indices', + // ); + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }), + // testCase.stringifyOutput.brackets, + // 'test case: ' + testCase.input + ', brackets', + // ); + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }), + // testCase.stringifyOutput.repeat, + // 'test case: ' + testCase.input + ', repeat', + // ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'indices' })).toBe( + testCase.stringify_output.indices, + ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'brackets' })).toBe( + testCase.stringify_output.brackets, + ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'repeat' })).toBe( + testCase.stringify_output.repeat, + ); + }); + }); + + test('edge case with object/arrays', function () { + // st.deepEqual(stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3'); + // st.deepEqual( + // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), + // '[][0]=2&[][1]=3&[a]=2', + // ); + // st.deepEqual( + // stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), + // '[][0]=2&[][1]=3', + // ); + // st.deepEqual( + // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), + // '[][0]=2&[][1]=3&[a]=2', + // ); + expect(stringify({ '': { '': [2, 3] } }, { encode: false })).toBe('[][0]=2&[][1]=3'); + expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false })).toBe('[][0]=2&[][1]=3&[a]=2'); + expect(stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' })).toBe( + '[][0]=2&[][1]=3', + ); + expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' })).toBe( + '[][0]=2&[][1]=3&[a]=2', + ); + }); +}); diff --git a/tests/qs/utils.test.ts b/tests/qs/utils.test.ts new file mode 100644 index 0000000..14bfa04 --- /dev/null +++ b/tests/qs/utils.test.ts @@ -0,0 +1,169 @@ +import { combine, merge, is_buffer, assign_single_source } from 'parallel-web/internal/qs/utils'; + +describe('merge()', function () { + // t.deepEqual(merge(null, true), [null, true], 'merges true into null'); + expect(merge(null, true)).toEqual([null, true]); + + // t.deepEqual(merge(null, [42]), [null, 42], 'merges null into an array'); + expect(merge(null, [42])).toEqual([null, 42]); + + // t.deepEqual( + // merge({ a: 'b' }, { a: 'c' }), + // { a: ['b', 'c'] }, + // 'merges two objects with the same key', + // ); + expect(merge({ a: 'b' }, { a: 'c' })).toEqual({ a: ['b', 'c'] }); + + var oneMerged = merge({ foo: 'bar' }, { foo: { first: '123' } }); + // t.deepEqual( + // oneMerged, + // { foo: ['bar', { first: '123' }] }, + // 'merges a standalone and an object into an array', + // ); + expect(oneMerged).toEqual({ foo: ['bar', { first: '123' }] }); + + var twoMerged = merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } }); + // t.deepEqual( + // twoMerged, + // { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, + // 'merges a standalone and two objects into an array', + // ); + expect(twoMerged).toEqual({ foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }); + + var sandwiched = merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' }); + // t.deepEqual( + // sandwiched, + // { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, + // 'merges an object sandwiched by two standalones into an array', + // ); + expect(sandwiched).toEqual({ foo: ['bar', { first: '123', second: '456' }, 'baz'] }); + + var nestedArrays = merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] }); + // t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] }); + expect(nestedArrays).toEqual({ foo: ['baz', 'bar', 'xyzzy'] }); + + var noOptionsNonObjectSource = merge({ foo: 'baz' }, 'bar'); + // t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true }); + expect(noOptionsNonObjectSource).toEqual({ foo: 'baz', bar: true }); + + (typeof Object.defineProperty !== 'function' ? test.skip : test)( + 'avoids invoking array setters unnecessarily', + function () { + var setCount = 0; + var getCount = 0; + var observed: any[] = []; + Object.defineProperty(observed, 0, { + get: function () { + getCount += 1; + return { bar: 'baz' }; + }, + set: function () { + setCount += 1; + }, + }); + merge(observed, [null]); + // st.equal(setCount, 0); + // st.equal(getCount, 1); + expect(setCount).toEqual(0); + expect(getCount).toEqual(1); + observed[0] = observed[0]; + // st.equal(setCount, 1); + // st.equal(getCount, 2); + expect(setCount).toEqual(1); + expect(getCount).toEqual(2); + }, + ); +}); + +test('assign()', function () { + var target = { a: 1, b: 2 }; + var source = { b: 3, c: 4 }; + var result = assign_single_source(target, source); + + expect(result).toEqual(target); + expect(target).toEqual({ a: 1, b: 3, c: 4 }); + expect(source).toEqual({ b: 3, c: 4 }); +}); + +describe('combine()', function () { + test('both arrays', function () { + var a = [1]; + var b = [2]; + var combined = combine(a, b); + + // st.deepEqual(a, [1], 'a is not mutated'); + // st.deepEqual(b, [2], 'b is not mutated'); + // st.notEqual(a, combined, 'a !== combined'); + // st.notEqual(b, combined, 'b !== combined'); + // st.deepEqual(combined, [1, 2], 'combined is a + b'); + expect(a).toEqual([1]); + expect(b).toEqual([2]); + expect(combined).toEqual([1, 2]); + expect(a).not.toEqual(combined); + expect(b).not.toEqual(combined); + }); + + test('one array, one non-array', function () { + var aN = 1; + var a = [aN]; + var bN = 2; + var b = [bN]; + + var combinedAnB = combine(aN, b); + // st.deepEqual(b, [bN], 'b is not mutated'); + // st.notEqual(aN, combinedAnB, 'aN + b !== aN'); + // st.notEqual(a, combinedAnB, 'aN + b !== a'); + // st.notEqual(bN, combinedAnB, 'aN + b !== bN'); + // st.notEqual(b, combinedAnB, 'aN + b !== b'); + // st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array'); + expect(b).toEqual([bN]); + expect(combinedAnB).not.toEqual(aN); + expect(combinedAnB).not.toEqual(a); + expect(combinedAnB).not.toEqual(bN); + expect(combinedAnB).not.toEqual(b); + expect(combinedAnB).toEqual([1, 2]); + + var combinedABn = combine(a, bN); + // st.deepEqual(a, [aN], 'a is not mutated'); + // st.notEqual(aN, combinedABn, 'a + bN !== aN'); + // st.notEqual(a, combinedABn, 'a + bN !== a'); + // st.notEqual(bN, combinedABn, 'a + bN !== bN'); + // st.notEqual(b, combinedABn, 'a + bN !== b'); + // st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array'); + expect(a).toEqual([aN]); + expect(combinedABn).not.toEqual(aN); + expect(combinedABn).not.toEqual(a); + expect(combinedABn).not.toEqual(bN); + expect(combinedABn).not.toEqual(b); + expect(combinedABn).toEqual([1, 2]); + }); + + test('neither is an array', function () { + var combined = combine(1, 2); + // st.notEqual(1, combined, '1 + 2 !== 1'); + // st.notEqual(2, combined, '1 + 2 !== 2'); + // st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array'); + expect(combined).not.toEqual(1); + expect(combined).not.toEqual(2); + expect(combined).toEqual([1, 2]); + }); +}); + +test('is_buffer()', function () { + for (const x of [null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g]) { + // t.equal(is_buffer(x), false, inspect(x) + ' is not a buffer'); + expect(is_buffer(x)).toEqual(false); + } + + var fakeBuffer = { constructor: Buffer }; + // t.equal(is_buffer(fakeBuffer), false, 'fake buffer is not a buffer'); + expect(is_buffer(fakeBuffer)).toEqual(false); + + var saferBuffer = Buffer.from('abc'); + // t.equal(is_buffer(saferBuffer), true, 'SaferBuffer instance is a buffer'); + expect(is_buffer(saferBuffer)).toEqual(true); + + var buffer = Buffer.from('abc'); + // t.equal(is_buffer(buffer), true, 'real Buffer instance is a buffer'); + expect(is_buffer(buffer)).toEqual(true); +}); diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index e197fb3..636f339 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -18,10 +18,4 @@ describe(stringifyQuery, () => { expect(stringifyQuery(input)).toEqual(expected); }); } - - for (const value of [[], {}, new Date()]) { - it(`${JSON.stringify(value)} -> `, () => { - expect(() => stringifyQuery({ value })).toThrow(`Cannot stringify type ${typeof value}`); - }); - } }); From e40609a9be369adf7bc7db56b21d1016aa9c8b73 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 03:43:01 +0000 Subject: [PATCH 09/14] feat(api): manual updates --- .stats.yml | 4 ++-- src/resources/beta/api.md | 3 +-- src/resources/beta/beta.ts | 2 -- src/resources/beta/findall.ts | 11 +++-------- src/resources/beta/index.ts | 1 - 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/.stats.yml b/.stats.yml index be05b09..22b55b1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 38 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-6b946abf6bef7f85a11824bfb1298c2887979f309873eb3ab94eee76dd6668ca.yml -openapi_spec_hash: 688facba9b1f3e19785c0ca88953e1c9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-d19934965b68e6b9445877a27f3bcd08e0f8492125faca7d9e66e538abf193f5.yml +openapi_spec_hash: c4fc5b0cb3bc48076f736a0ad2b2e75e config_hash: 0b93697d62ec2afc2b9ed854621dffe7 diff --git a/src/resources/beta/api.md b/src/resources/beta/api.md index e94d1ed..8831a07 100644 --- a/src/resources/beta/api.md +++ b/src/resources/beta/api.md @@ -75,14 +75,13 @@ Types: - FindAllSchemaUpdatedEvent - IngestInput - MatchCondition -- FindAllCancelResponse - FindAllEventsResponse Methods: - client.beta.findall.create({ ...params }) -> FindAllRun - client.beta.findall.retrieve(findallID, { ...params }) -> FindAllRun -- client.beta.findall.cancel(findallID, { ...params }) -> unknown +- client.beta.findall.cancel(findallID, { ...params }) -> void - client.beta.findall.candidates({ ...params }) -> FindAllCandidatesResponse - client.beta.findall.enrich(findallID, { ...params }) -> FindAllSchema - client.beta.findall.events(findallID, { ...params }) -> FindAllEventsResponse diff --git a/src/resources/beta/beta.ts b/src/resources/beta/beta.ts index b53276f..83ccb1c 100644 --- a/src/resources/beta/beta.ts +++ b/src/resources/beta/beta.ts @@ -7,7 +7,6 @@ import * as FindAllAPI from './findall'; import { FindAll, FindAllCancelParams, - FindAllCancelResponse, FindAllCandidate, FindAllCandidateMatchStatusEvent, FindAllCandidateMetrics, @@ -449,7 +448,6 @@ export declare namespace Beta { type FindAllSchemaUpdatedEvent as FindAllSchemaUpdatedEvent, type IngestInput as IngestInput, type MatchCondition as MatchCondition, - type FindAllCancelResponse as FindAllCancelResponse, type FindAllEventsResponse as FindAllEventsResponse, type FindAllCreateParams as FindAllCreateParams, type FindAllRetrieveParams as FindAllRetrieveParams, diff --git a/src/resources/beta/findall.ts b/src/resources/beta/findall.ts index f649d6b..775c8ca 100644 --- a/src/resources/beta/findall.ts +++ b/src/resources/beta/findall.ts @@ -86,21 +86,19 @@ export class FindAll extends APIResource { * * @example * ```ts - * const response = await client.beta.findall.cancel( - * 'findall_id', - * ); + * await client.beta.findall.cancel('findall_id'); * ``` */ cancel( findallID: string, params: FindAllCancelParams | null | undefined = {}, options?: RequestOptions, - ): APIPromise { + ): APIPromise { const { betas } = params ?? {}; return this._client.post(path`/v1beta/findall/runs/${findallID}/cancel`, { ...options, headers: buildHeaders([ - { 'parallel-beta': [...(betas ?? []), 'findall-2025-09-15'].toString() }, + { 'parallel-beta': [...(betas ?? []), 'findall-2025-09-15'].toString(), Accept: '*/*' }, options?.headers, ]), }); @@ -748,8 +746,6 @@ export interface MatchCondition { name: string; } -export type FindAllCancelResponse = unknown; - /** * Event containing full snapshot of FindAll run state. */ @@ -953,7 +949,6 @@ export declare namespace FindAll { type FindAllSchemaUpdatedEvent as FindAllSchemaUpdatedEvent, type IngestInput as IngestInput, type MatchCondition as MatchCondition, - type FindAllCancelResponse as FindAllCancelResponse, type FindAllEventsResponse as FindAllEventsResponse, type FindAllCreateParams as FindAllCreateParams, type FindAllRetrieveParams as FindAllRetrieveParams, diff --git a/src/resources/beta/index.ts b/src/resources/beta/index.ts index 944e12d..87036de 100644 --- a/src/resources/beta/index.ts +++ b/src/resources/beta/index.ts @@ -20,7 +20,6 @@ export { type FindAllSchemaUpdatedEvent, type IngestInput, type MatchCondition, - type FindAllCancelResponse, type FindAllEventsResponse, type FindAllCreateParams, type FindAllRetrieveParams, From 816c149819a5d48307f943460ae1c29538a191f8 Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Tue, 5 May 2026 23:58:22 -0400 Subject: [PATCH 10/14] fix(types): drop FindallCancelResponse alias whose target was removed The findall.cancel endpoint was changed to return 204 No Content, so Stainless dropped FindAllCancelResponse from the generated SDK. The back-compat alias FindallCancelResponse = FindAllCancelResponse can no longer resolve, breaking tsc. Drop the dangling alias and its re-exports. --- src/resources/beta/findall.ts | 3 --- src/resources/beta/index.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/resources/beta/findall.ts b/src/resources/beta/findall.ts index 775c8ca..9f41a12 100644 --- a/src/resources/beta/findall.ts +++ b/src/resources/beta/findall.ts @@ -991,8 +991,6 @@ export type FindallSchema = FindAllSchema; export type FindallSchemaUpdatedEvent = FindAllSchemaUpdatedEvent; /** @deprecated Use `IngestInput` instead. */ export type FindallIngestInput = IngestInput; -/** @deprecated Use `FindAllCancelResponse` instead. */ -export type FindallCancelResponse = FindAllCancelResponse; /** @deprecated Use `FindAllEventsResponse` instead. */ export type FindallEventsResponse = FindAllEventsResponse; /** @deprecated Use `FindAllCreateParams` instead. */ @@ -1026,7 +1024,6 @@ export declare namespace Findall { type FindallSchema as FindallSchema, type FindallSchemaUpdatedEvent as FindallSchemaUpdatedEvent, type FindallIngestInput as FindallIngestInput, - type FindallCancelResponse as FindallCancelResponse, type FindallEventsResponse as FindallEventsResponse, type FindallCreateParams as FindallCreateParams, type FindallRetrieveParams as FindallRetrieveParams, diff --git a/src/resources/beta/index.ts b/src/resources/beta/index.ts index 87036de..5ab0a7f 100644 --- a/src/resources/beta/index.ts +++ b/src/resources/beta/index.ts @@ -41,7 +41,6 @@ export { type FindallSchema, type FindallSchemaUpdatedEvent, type FindallIngestInput, - type FindallCancelResponse, type FindallEventsResponse, type FindallCreateParams, type FindallRetrieveParams, From c6972a001e841209bf4f36ab659bfdf1427f485e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 04:19:13 +0000 Subject: [PATCH 11/14] feat(api): manual updates --- .stats.yml | 4 ++-- src/resources/beta/task-group.ts | 41 ++++++-------------------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/.stats.yml b/.stats.yml index 22b55b1..9a8c935 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 38 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-d19934965b68e6b9445877a27f3bcd08e0f8492125faca7d9e66e538abf193f5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/parallel-web/parallel-sdk-b134f034fe11499d713c03d07778aba8a395d7e3cbbc3d8a4bd2891f0aa970ba.yml openapi_spec_hash: c4fc5b0cb3bc48076f736a0ad2b2e75e -config_hash: 0b93697d62ec2afc2b9ed854621dffe7 +config_hash: 27cd9354fb0ac3129cbf269737cece6c diff --git a/src/resources/beta/task-group.ts b/src/resources/beta/task-group.ts index 9f684e2..233602e 100644 --- a/src/resources/beta/task-group.ts +++ b/src/resources/beta/task-group.ts @@ -12,15 +12,14 @@ import { path } from '../../internal/utils/path'; /** * Tasks (Beta) + * + * @deprecated Use GA Task Group instead */ export class TaskGroup extends APIResource { /** * Initiates a TaskGroup to group and track multiple runs. * - * @example - * ```ts - * const taskGroup = await client.beta.taskGroup.create(); - * ``` + * @deprecated Use GA Task Group instead */ create(body: TaskGroupCreateParams, options?: RequestOptions): APIPromise { return this._client.post('/v1beta/tasks/groups', { @@ -33,12 +32,7 @@ export class TaskGroup extends APIResource { /** * Retrieves aggregated status across runs in a TaskGroup. * - * @example - * ```ts - * const taskGroup = await client.beta.taskGroup.retrieve( - * 'taskgroup_id', - * ); - * ``` + * @deprecated Use GA Task Group instead */ retrieve(taskGroupID: string, options?: RequestOptions): APIPromise { return this._client.get(path`/v1beta/tasks/groups/${taskGroupID}`, { @@ -50,18 +44,7 @@ export class TaskGroup extends APIResource { /** * Initiates multiple task runs within a TaskGroup. * - * @example - * ```ts - * const taskGroupRunResponse = - * await client.beta.taskGroup.addRuns('taskgroup_id', { - * inputs: [ - * { - * input: 'What was the GDP of France in 2023?', - * processor: 'base', - * }, - * ], - * }); - * ``` + * @deprecated Use GA Task Group instead */ addRuns( taskGroupID: string, @@ -86,12 +69,7 @@ export class TaskGroup extends APIResource { * The connection will remain open for up to an hour as long as at least one run in * the group is still active. * - * @example - * ```ts - * const response = await client.beta.taskGroup.events( - * 'taskgroup_id', - * ); - * ``` + * @deprecated Use GA Task Group instead */ events( taskGroupID: string, @@ -121,12 +99,7 @@ export class TaskGroup extends APIResource { * the stream. The stream will resume from the next event after the * `last_event_id`. * - * @example - * ```ts - * const response = await client.beta.taskGroup.getRuns( - * 'taskgroup_id', - * ); - * ``` + * @deprecated Use GA Task Group instead */ getRuns( taskGroupID: string, From e5cafc25e63ccbba105f95356c093343f5185f87 Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Wed, 6 May 2026 00:35:29 -0400 Subject: [PATCH 12/14] fix(types): preserve back-compat namespace members for renamed inline types Stainless's regen moved several previously-nested namespace member types (RunInput.AdvancedSettings, TaskRunEventsResponse.TaskRunProgress*, the FindAll*.MatchCondition / .Status / .Candidate / .Data variants, TaskGroupEventsResponse.TaskGroupStatusEvent, BetaExtractParams.FullContentSettings, AdvancedExtractSettings.FullContentSettings) to top-level model types. The nested member types disappear from the generated SDK; user code that referenced them via 'Parent.Member' would get TS errors after upgrade. Add namespace declarations that re-introduce the old member paths via TS declaration merging, pointing at the new top-level types. Each is JSDoc @deprecated so users get migration hints. --- src/resources/beta/beta.ts | 8 ++++++ src/resources/beta/findall.ts | 42 ++++++++++++++++++++++++++++++++ src/resources/beta/task-group.ts | 9 +++++++ src/resources/beta/task-run.ts | 22 +++++++++++++++++ src/resources/task-run.ts | 27 ++++++++++++++++++++ src/resources/top-level.ts | 9 +++++++ 6 files changed, 117 insertions(+) diff --git a/src/resources/beta/beta.ts b/src/resources/beta/beta.ts index 83ccb1c..7cb6dc4 100644 --- a/src/resources/beta/beta.ts +++ b/src/resources/beta/beta.ts @@ -461,3 +461,11 @@ export declare namespace Beta { type FindAllSchemaParams as FindAllSchemaParams, }; } + +// Backwards-compat namespace member (deprecated). `BetaExtractParams.FullContentSettings` +// was previously a nested interface; the shape now lives as the top-level +// `FullContentSettings` model. +export namespace BetaExtractParams { + /** @deprecated Use `Parallel.FullContentSettings` instead. */ + export type FullContentSettings = TopLevelAPI.FullContentSettings; +} diff --git a/src/resources/beta/findall.ts b/src/resources/beta/findall.ts index 9f41a12..af6af01 100644 --- a/src/resources/beta/findall.ts +++ b/src/resources/beta/findall.ts @@ -1036,3 +1036,45 @@ export declare namespace Findall { type FindallSchemaParams as FindallSchemaParams, }; } + +// Backwards-compat namespace members (deprecated). Previously these types +// existed as nested interfaces under their parent's namespace; they've since +// moved to top-level model types. Declaration merging here preserves the old +// `Parent.Member` import paths. +type _FindAllCandidate = FindAllCandidate; +type _FindAllRunStatus = FindAllRunStatus; +type _FindAllCandidateMetrics = FindAllCandidateMetrics; +type _MatchCondition = MatchCondition; +export namespace FindAllCandidateMatchStatusEvent { + /** @deprecated Use the top-level `FindAllCandidate` instead. */ + export type Data = _FindAllCandidate; +} +export namespace FindAllCreateParams { + /** @deprecated Use the top-level `MatchCondition` instead. */ + export type MatchCondition = _MatchCondition; +} +// FindAllRunInput and FindAllSchema already declare `export namespace`s above; +// they are re-augmented here with the deprecated `MatchCondition` member. +// Note: only `MatchCondition` is added; existing namespace members stay intact. +declare module './findall' { + namespace FindAllRunInput { + /** @deprecated Use the top-level `MatchCondition` instead. */ + type MatchCondition = _MatchCondition; + } + namespace FindAllSchema { + /** @deprecated Use the top-level `MatchCondition` instead. */ + type MatchCondition = _MatchCondition; + } +} +export namespace FindAllRun { + /** @deprecated Use the top-level `FindAllRunStatus` instead. */ + export type Status = _FindAllRunStatus; + export namespace Status { + /** @deprecated Use the top-level `FindAllCandidateMetrics` instead. */ + export type Metrics = _FindAllCandidateMetrics; + } +} +export namespace FindAllRunResult { + /** @deprecated Use the top-level `FindAllCandidate` instead. */ + export type Candidate = _FindAllCandidate; +} diff --git a/src/resources/beta/task-group.ts b/src/resources/beta/task-group.ts index 233602e..bdb72a7 100644 --- a/src/resources/beta/task-group.ts +++ b/src/resources/beta/task-group.ts @@ -212,3 +212,12 @@ export declare namespace TaskGroup { type TaskGroupGetRunsParams as TaskGroupGetRunsParams, }; } + +// Backwards-compat namespace member (deprecated). `TaskGroupEventsResponse.TaskGroupStatusEvent` +// was previously a nested interface; the type now lives at the file's top +// level (re-exported from GA `TaskGroupAPI.TaskGroupStatusEvent`). +type _TaskGroupStatusEvent = TaskGroupStatusEvent; +export namespace TaskGroupEventsResponse { + /** @deprecated Use `Beta.TaskGroupStatusEvent` instead. */ + export type TaskGroupStatusEvent = _TaskGroupStatusEvent; +} diff --git a/src/resources/beta/task-run.ts b/src/resources/beta/task-run.ts index 74b0e91..e711398 100644 --- a/src/resources/beta/task-run.ts +++ b/src/resources/beta/task-run.ts @@ -252,3 +252,25 @@ export declare namespace TaskRun { type TaskRunResultParams as TaskRunResultParams, }; } + +// Backwards-compat namespace members (deprecated). Previously these types +// existed as nested interfaces under their parent's namespace; they've since +// moved to top-level model types in the GA `task-run` module. +type _TaskAdvancedSettings = TaskRunAPI.TaskAdvancedSettings; +type _TaskRunProgressMessageEvent = TaskRunAPI.TaskRunProgressMessageEvent; +type _TaskRunProgressStatsEvent = TaskRunAPI.TaskRunProgressStatsEvent; +type _TaskRunSourceStats = TaskRunAPI.TaskRunSourceStats; +export namespace TaskRunCreateParams { + /** @deprecated Use `Parallel.TaskAdvancedSettings` instead. */ + export type AdvancedSettings = _TaskAdvancedSettings; +} +export namespace TaskRunEventsResponse { + /** @deprecated Use `Parallel.TaskRunProgressMessageEvent` instead. */ + export type TaskRunProgressMessageEvent = _TaskRunProgressMessageEvent; + /** @deprecated Use `Parallel.TaskRunProgressStatsEvent` instead. */ + export type TaskRunProgressStatsEvent = _TaskRunProgressStatsEvent; + export namespace TaskRunProgressStatsEvent { + /** @deprecated Use `Parallel.TaskRunSourceStats` instead. */ + export type SourceStats = _TaskRunSourceStats; + } +} diff --git a/src/resources/task-run.ts b/src/resources/task-run.ts index c5c3410..450f728 100644 --- a/src/resources/task-run.ts +++ b/src/resources/task-run.ts @@ -773,3 +773,30 @@ export declare namespace TaskRun { type TaskRunResultParams as TaskRunResultParams, }; } + +// Backwards-compat namespace members (deprecated). Previously these types +// existed as nested interfaces under the parent type's namespace; they've +// since moved to top-level model types. Declaration merging here preserves +// the old `Parent.Member` import paths. +type _TaskAdvancedSettings = TaskAdvancedSettings; +type _TaskRunProgressMessageEvent = TaskRunProgressMessageEvent; +type _TaskRunProgressStatsEvent = TaskRunProgressStatsEvent; +type _TaskRunSourceStats = TaskRunSourceStats; +export namespace RunInput { + /** @deprecated Use the top-level `TaskAdvancedSettings` instead. */ + export type AdvancedSettings = _TaskAdvancedSettings; +} +export namespace TaskRunCreateParams { + /** @deprecated Use the top-level `TaskAdvancedSettings` instead. */ + export type AdvancedSettings = _TaskAdvancedSettings; +} +export namespace TaskRunEventsResponse { + /** @deprecated Use the top-level `TaskRunProgressMessageEvent` instead. */ + export type TaskRunProgressMessageEvent = _TaskRunProgressMessageEvent; + /** @deprecated Use the top-level `TaskRunProgressStatsEvent` instead. */ + export type TaskRunProgressStatsEvent = _TaskRunProgressStatsEvent; + export namespace TaskRunProgressStatsEvent { + /** @deprecated Use the top-level `TaskRunSourceStats` instead. */ + export type SourceStats = _TaskRunSourceStats; + } +} diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts index 5ae45cd..3bea5a3 100644 --- a/src/resources/top-level.ts +++ b/src/resources/top-level.ts @@ -382,3 +382,12 @@ export declare namespace TopLevel { type SearchParams as SearchParams, }; } + +// Backwards-compat namespace member (deprecated). `AdvancedExtractSettings.FullContentSettings` +// was previously a nested interface; the shape now lives as the top-level +// `FullContentSettings` model. Declaration merging here preserves the old path. +export namespace AdvancedExtractSettings { + /** @deprecated Use the top-level `FullContentSettings` instead. */ + export type FullContentSettings = TopLevelFullContentSettings; +} +type TopLevelFullContentSettings = FullContentSettings; From bc434bfd3130e70585864a1d7b826cf34c88c0f5 Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Wed, 6 May 2026 01:04:34 -0400 Subject: [PATCH 13/14] fix(types): use export namespace for FindAllRunInput/Schema augmentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous `declare module './findall'` doesn't actually augment the same-file namespaces — it's for ambient module declarations. Use plain `export namespace` blocks, which merge with the existing namespaces via TS declaration merging. --- src/resources/beta/findall.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/resources/beta/findall.ts b/src/resources/beta/findall.ts index af6af01..23aacbf 100644 --- a/src/resources/beta/findall.ts +++ b/src/resources/beta/findall.ts @@ -1054,17 +1054,17 @@ export namespace FindAllCreateParams { export type MatchCondition = _MatchCondition; } // FindAllRunInput and FindAllSchema already declare `export namespace`s above; -// they are re-augmented here with the deprecated `MatchCondition` member. -// Note: only `MatchCondition` is added; existing namespace members stay intact. -declare module './findall' { - namespace FindAllRunInput { - /** @deprecated Use the top-level `MatchCondition` instead. */ - type MatchCondition = _MatchCondition; - } - namespace FindAllSchema { - /** @deprecated Use the top-level `MatchCondition` instead. */ - type MatchCondition = _MatchCondition; - } +// these additional `export namespace` blocks merge with them via TS declaration +// merging, augmenting the existing namespaces with the deprecated `MatchCondition` +// member. (`declare module` would not work here — it's for ambient module +// augmentation, not same-file declaration merging.) +export namespace FindAllRunInput { + /** @deprecated Use the top-level `MatchCondition` instead. */ + export type MatchCondition = _MatchCondition; +} +export namespace FindAllSchema { + /** @deprecated Use the top-level `MatchCondition` instead. */ + export type MatchCondition = _MatchCondition; } export namespace FindAllRun { /** @deprecated Use the top-level `FindAllRunStatus` instead. */ From f6299815a382fe72eb2ce57ae19c6ec370073e83 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 05:05:00 +0000 Subject: [PATCH 14/14] release: 0.5.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 26 ++++++++++++++++++++++++++ package.json | 2 +- src/version.ts | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 218393f..f1c1e58 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.1" + ".": "0.5.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d7602..bc69885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 0.5.0 (2026-05-06) + +Full Changelog: [v0.4.1...v0.5.0](https://github.com/parallel-web/parallel-sdk-typescript/compare/v0.4.1...v0.5.0) + +### Features + +* **api:** manual updates ([c6972a0](https://github.com/parallel-web/parallel-sdk-typescript/commit/c6972a001e841209bf4f36ab659bfdf1427f485e)) +* **api:** manual updates ([e40609a](https://github.com/parallel-web/parallel-sdk-typescript/commit/e40609a9be369adf7bc7db56b21d1016aa9c8b73)) +* **api:** manual updates ([98f915c](https://github.com/parallel-web/parallel-sdk-typescript/commit/98f915c996abb163d8a79698fa57259bae3b0584)) +* **api:** Task Groups v1 added to SDK ([b939d5e](https://github.com/parallel-web/parallel-sdk-typescript/commit/b939d5e663c39640ce2f0033fc3590d886f2b571)) +* support setting headers via env ([305fe46](https://github.com/parallel-web/parallel-sdk-typescript/commit/305fe467afbe744697c18b8d9a505d7e0952fabb)) + + +### Bug Fixes + +* **types:** drop FindallCancelResponse alias whose target was removed ([816c149](https://github.com/parallel-web/parallel-sdk-typescript/commit/816c149819a5d48307f943460ae1c29538a191f8)) +* **types:** preserve back-compat namespace members for renamed inline types ([e5cafc2](https://github.com/parallel-web/parallel-sdk-typescript/commit/e5cafc25e63ccbba105f95356c093343f5185f87)) +* **types:** use export namespace for FindAllRunInput/Schema augmentation ([bc434bf](https://github.com/parallel-web/parallel-sdk-typescript/commit/bc434bfd3130e70585864a1d7b826cf34c88c0f5)) + + +### Chores + +* **format:** run eslint and prettier separately ([4ac089c](https://github.com/parallel-web/parallel-sdk-typescript/commit/4ac089c6b95d0b96927c841cf9587a0d395b8a91)) +* **internal:** codegen related update ([c92502e](https://github.com/parallel-web/parallel-sdk-typescript/commit/c92502e8ce716259136efe8dbb3edcb3e7c2899b)) +* **internal:** more robust bootstrap script ([3eaee7d](https://github.com/parallel-web/parallel-sdk-typescript/commit/3eaee7de908bd37cb089f25747dc25f66829baa3)) + ## 0.4.1 (2026-04-22) Full Changelog: [v0.4.0...v0.4.1](https://github.com/parallel-web/parallel-sdk-typescript/compare/v0.4.0...v0.4.1) diff --git a/package.json b/package.json index f58b0bb..1df00af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parallel-web", - "version": "0.4.1", + "version": "0.5.0", "description": "The official TypeScript library for the Parallel API", "author": "Parallel ", "types": "dist/index.d.ts", diff --git a/src/version.ts b/src/version.ts index 99a0031..1f5d158 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.4.1'; // x-release-please-version +export const VERSION = '0.5.0'; // x-release-please-version