From ed7ba69da64579c68b4e4cf91484164396e0d95e Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 15:14:27 +0000 Subject: [PATCH 01/16] fix(typescript): match SDK request/auth shape in dynamic snippets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../typescript-v2/ast/src/ast/TypeLiteral.ts | 4 + .../src/EndpointSnippetGenerator.ts | 124 ++- .../src/__test__/AuthWrapperProperty.test.ts | 108 +++ .../__test__/FlattenRequestParameters.test.ts | 84 ++ .../utils/buildDynamicSnippetsGenerator.ts | 8 +- ...snippets-flatten-body-and-auth-wrapper.yml | 5 + .../dynamic-auth-wrapper-property.yml | 5 + .../authWrapperPropertyDynamic.test.ts | 50 ++ .../test-definitions/accept-header.json | 3 +- .../__test__/test-definitions/any-auth.json | 57 ++ .../basic-auth-environment-variables.json | 6 +- .../basic-auth-pw-omitted.json | 6 +- .../__test__/test-definitions/basic-auth.json | 6 +- .../bearer-token-environment-variable.json | 3 +- .../test-definitions/cli-any-auth.json | 730 ++++++++++++++++++ .../test-definitions/cli-basic-auth.json | 6 +- .../test-definitions/cli-header-auth.json | 6 +- .../cli-multi-scheme-routing.json | 385 +++++++++ .../cli-multi-spec-namespaced.json | 6 +- .../cli-oauth-login-flow.json | 12 +- .../__test__/test-definitions/cli-oauth.json | 12 +- .../test-definitions/client-side-params.json | 36 +- .../csharp-global-header-env.json | 21 +- .../csharp-global-header-literal-env.json | 3 +- .../csharp-oauth-token-optional.json | 3 +- ...sharp-oauth-token-required-grant-type.json | 3 +- .../endpoint-security-auth.json | 152 ++++ .../__test__/test-definitions/examples.json | 33 +- .../__test__/test-definitions/exhaustive.json | 192 +++-- .../test-definitions/go-content-type.json | 3 +- .../go-deterministic-ordering.json | 189 +++-- .../test-definitions/go-global-headers.json | 3 +- .../go-oauth-token-nullable.json | 3 +- .../go-oauth-token-optional.json | 3 +- .../go-optional-header-env.json | 3 +- .../go-undiscriminated-union-wire-tests.json | 3 +- .../header-auth-environment-variable.json | 3 +- .../test-definitions/header-auth.json | 3 +- .../test-definitions/idempotency-headers.json | 6 +- .../__test__/test-definitions/imdb.json | 6 +- .../inferred-auth-explicit.json | 15 +- .../inferred-auth-implicit-api-key.json | 12 +- .../inferred-auth-implicit-no-expiry.json | 15 +- .../inferred-auth-implicit-reference.json | 15 +- .../inferred-auth-implicit.json | 15 +- .../java-builder-extension.json | 3 +- .../java-custom-package-prefix.json | 6 +- ...va-endpoint-security-token-subpackage.json | 84 +- .../java-idempotency-headers-file-upload.json | 3 +- .../java-oauth-token-optional.json | 3 +- ...-oauth-token-required-enum-grant-type.json | 6 +- .../multi-url-environment-no-default.json | 6 +- .../multi-url-environment-reference.json | 9 +- .../multi-url-environment.json | 6 +- .../multiple-request-bodies.json | 6 +- .../test-definitions/no-environment.json | 3 +- ...ent-credentials-custom-prefix-openapi.json | 9 +- .../oauth-client-credentials-custom.json | 15 +- .../oauth-client-credentials-default.json | 12 +- ...ent-credentials-environment-variables.json | 15 +- ...uth-client-credentials-mandatory-auth.json | 12 +- .../oauth-client-credentials-nested-root.json | 12 +- .../oauth-client-credentials-openapi.json | 9 +- .../oauth-client-credentials-reference.json | 6 +- ...uth-client-credentials-with-variables.json | 18 +- .../oauth-client-credentials.json | 15 +- .../openapi-per-spec-base-path-disabled.json | 6 +- .../openapi-per-spec-base-path.json | 6 +- .../test-definitions/pagination-custom.json | 3 +- .../test-definitions/pagination-uri-path.json | 6 +- .../__test__/test-definitions/pagination.json | 87 ++- .../php-global-header-literal-env.json | 3 +- .../python-oauth-token-optional.json | 3 +- ...ndpoint-security-optional-credentials.json | 76 ++ .../__test__/test-definitions/simple-api.json | 3 +- .../single-url-environment-default.json | 3 +- .../single-url-environment-no-default.json | 3 +- .../__test__/test-definitions/trace.json | 105 ++- .../test-definitions/ts-express-casing.json | 6 +- .../ts-oauth-token-optional.json | 3 +- .../websocket-inferred-auth.json | 6 +- .../DynamicSnippetsConverter.ts | 43 +- .../ir-sdk/fern/apis/ir-types-latest/VERSION | 2 +- .../ir-types-latest/changelog/CHANGELOG.md | 5 + .../definition/dynamic/auth.yml | 14 + .../dynamic/resources/auth/types/BaseAuth.ts | 12 + .../dynamic/resources/auth/types/BasicAuth.ts | 2 +- .../resources/auth/types/BearerAuth.ts | 2 +- .../resources/auth/types/HeaderAuth.ts | 2 +- .../resources/auth/types/InferredAuth.ts | 2 +- .../dynamic/resources/auth/types/OAuth.ts | 2 +- .../dynamic/resources/auth/types/index.ts | 1 + .../dynamic/resources/auth/types/BaseAuth.ts | 17 + .../dynamic/resources/auth/types/BasicAuth.ts | 17 +- .../resources/auth/types/BearerAuth.ts | 11 +- .../resources/auth/types/HeaderAuth.ts | 11 +- .../resources/auth/types/InferredAuth.ts | 11 +- .../dynamic/resources/auth/types/OAuth.ts | 15 +- .../dynamic/resources/auth/types/index.ts | 1 + 99 files changed, 2681 insertions(+), 432 deletions(-) create mode 100644 generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts create mode 100644 generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts create mode 100644 generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml create mode 100644 packages/cli/cli/changes/unreleased/dynamic-auth-wrapper-property.yml create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-any-auth.json create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-scheme-routing.json create mode 100644 packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BaseAuth.ts create mode 100644 packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BaseAuth.ts diff --git a/generators/typescript-v2/ast/src/ast/TypeLiteral.ts b/generators/typescript-v2/ast/src/ast/TypeLiteral.ts index 20913f304cb1..663f7f1fa124 100644 --- a/generators/typescript-v2/ast/src/ast/TypeLiteral.ts +++ b/generators/typescript-v2/ast/src/ast/TypeLiteral.ts @@ -338,6 +338,10 @@ export class TypeLiteral extends AstNode { }); } + public getObjectFields(): ObjectField[] | undefined { + return this.internalType.type === "object" ? this.internalType.fields : undefined; + } + public static record({ entries }: { entries: RecordEntry[] }): TypeLiteral { return new this({ type: "record", diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 41b611b3e140..8427245a2a4f 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -13,6 +13,25 @@ const STRING_TYPE_REFERENCE: FernIr.dynamic.TypeReference = { value: "STRING" }; +type AuthFields = + | FernIr.dynamic.BasicAuth + | FernIr.dynamic.BearerAuth + | FernIr.dynamic.HeaderAuth + | FernIr.dynamic.OAuth + | FernIr.dynamic.InferredAuth; + +type AuthWithWrapperProperty = AuthFields & { + wrapperProperty?: FernIr.dynamic.Name; +}; + +function hasAuthWrapperProperty(auth: AuthFields): auth is AuthWithWrapperProperty { + return "wrapperProperty" in auth; +} + +function getAuthWrapperProperty(auth: AuthFields): FernIr.dynamic.Name | undefined { + return hasAuthWrapperProperty(auth) ? auth.wrapperProperty : undefined; +} + export class EndpointSnippetGenerator { private context: DynamicSnippetsGeneratorContext; @@ -275,16 +294,19 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.BasicAuth; values: FernIr.dynamic.BasicAuthValues; }): ts.ObjectField[] { - return [ - { - name: this.context.getPropertyName(auth.username), - value: ts.TypeLiteral.string(values.username) - }, - { - name: this.context.getPropertyName(auth.password), - value: ts.TypeLiteral.string(values.password) - } - ]; + return this.wrapAuthFields({ + auth, + fields: [ + { + name: this.context.getPropertyName(auth.username), + value: ts.TypeLiteral.string(values.username) + }, + { + name: this.context.getPropertyName(auth.password), + value: ts.TypeLiteral.string(values.password) + } + ] + }); } private getConstructorBearerAuthArgs({ @@ -294,12 +316,15 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.BearerAuth; values: FernIr.dynamic.BearerAuthValues; }): ts.ObjectField[] { - return [ - { - name: this.context.getPropertyName(auth.token), - value: ts.TypeLiteral.string(values.token) - } - ]; + return this.wrapAuthFields({ + auth, + fields: [ + { + name: this.context.getPropertyName(auth.token), + value: ts.TypeLiteral.string(values.token) + } + ] + }); } private getConstructorHeaderAuthArgs({ @@ -309,15 +334,18 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.HeaderAuth; values: FernIr.dynamic.HeaderAuthValues; }): ts.ObjectField[] { - return [ - { - name: this.context.getPropertyName(auth.header.name.name), - value: this.context.dynamicTypeLiteralMapper.convert({ - typeReference: auth.header.typeReference, - value: values.value - }) - } - ]; + return this.wrapAuthFields({ + auth, + fields: [ + { + name: this.context.getPropertyName(auth.header.name.name), + value: this.context.dynamicTypeLiteralMapper.convert({ + typeReference: auth.header.typeReference, + value: values.value + }) + } + ] + }); } private getConstructorOAuthArgs({ @@ -327,14 +355,30 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.OAuth; values: FernIr.dynamic.OAuthValues; }): ts.ObjectField[] { + return this.wrapAuthFields({ + auth, + fields: [ + { + name: this.context.getPropertyName(auth.clientId), + value: ts.TypeLiteral.string(values.clientId) + }, + { + name: this.context.getPropertyName(auth.clientSecret), + value: ts.TypeLiteral.string(values.clientSecret) + } + ] + }); + } + + private wrapAuthFields({ auth, fields }: { auth: AuthFields; fields: ts.ObjectField[] }): ts.ObjectField[] { + const wrapperProperty = getAuthWrapperProperty(auth); + if (wrapperProperty == null) { + return fields; + } return [ { - name: this.context.getPropertyName(auth.clientId), - value: ts.TypeLiteral.string(values.clientId) - }, - { - name: this.context.getPropertyName(auth.clientSecret), - value: ts.TypeLiteral.string(values.clientSecret) + name: this.context.getPropertyName(wrapperProperty), + value: ts.TypeLiteral.object({ fields }) } ]; } @@ -644,6 +688,24 @@ export class EndpointSnippetGenerator { case "properties": return this.getInlinedRequestBodyPropertyObjectFields({ parameters: body.value, value }); case "referenced": { + if ( + this.context.customConfig?.flattenRequestParameters === true && + body.bodyType.type === "typeReference" && + body.bodyType.value.type === "named" + ) { + const named = this.context.resolveNamedType({ typeId: body.bodyType.value.value }); + if (named?.type === "object") { + const flattened = this.context.dynamicTypeLiteralMapper.convert({ + typeReference: body.bodyType.value, + value, + convertOpts: { isForRequest: true } + }); + const fields = flattened.getObjectFields(); + if (fields != null) { + return fields; + } + } + } const field = this.getReferencedRequestBodyPropertyObjectField({ body, value }); // an example that omits an optional request body has no value to write, so the // property is dropped rather than passed explicitly as undefined diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts new file mode 100644 index 000000000000..f36f8007a2bc --- /dev/null +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts @@ -0,0 +1,108 @@ +import { FernIr } from "@fern-api/dynamic-ir-sdk"; +import { AbsoluteFilePath, join } from "@fern-api/path-utils"; + +import { buildDynamicSnippetsGenerator } from "./utils/buildDynamicSnippetsGenerator.js"; +import { buildGeneratorConfig } from "./utils/buildGeneratorConfig.js"; + +const DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY = AbsoluteFilePath.of( + `${__dirname}/../../../../../packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions` +); +const IR_FILEPATH = AbsoluteFilePath.of(join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "exhaustive.json")); + +const REQUEST: FernIr.dynamic.EndpointSnippetRequest = { + endpoint: { + method: "PUT", + path: "/http-methods/{id}" + }, + baseURL: undefined, + environment: undefined, + auth: { + type: "bearer", + token: "" + }, + pathParameters: { + id: "id" + }, + queryParameters: undefined, + headers: undefined, + requestBody: undefined +}; + +const bearerAuthWrapperProperty: FernIr.dynamic.Name = { + originalName: "bearer_auth", + camelCase: { + unsafeName: "bearerAuth", + safeName: "bearerAuth" + }, + pascalCase: { + unsafeName: "BearerAuth", + safeName: "BearerAuth" + }, + snakeCase: { + unsafeName: "bearer_auth", + safeName: "bearer_auth" + }, + screamingSnakeCase: { + unsafeName: "BEARER_AUTH", + safeName: "BEARER_AUTH" + } +}; + +function addBearerAuthWrapperProperty( + ir: FernIr.dynamic.DynamicIntermediateRepresentation +): FernIr.dynamic.DynamicIntermediateRepresentation { + const endpointEntry = Object.entries(ir.endpoints).find( + ([endpointId, endpoint]) => + endpointId === "endpoint_endpoints/http-methods.testPut" && endpoint.auth?.type === "bearer" + ); + if (endpointEntry == null || endpointEntry[1].auth?.type !== "bearer") { + throw new Error("No bearer-authenticated endpoint found in fixture"); + } + const [endpointId, endpoint] = endpointEntry; + const bearerAuth = endpoint.auth; + if (bearerAuth == null || bearerAuth.type !== "bearer") { + throw new Error("No bearer auth found on selected endpoint"); + } + const auth = { + ...bearerAuth, + wrapperProperty: bearerAuthWrapperProperty + }; + const modifiedEndpoint: FernIr.dynamic.Endpoint = { + ...endpoint, + auth + }; + return { + ...ir, + endpoints: { + ...ir.endpoints, + [endpointId]: modifiedEndpoint + } + }; +} + +describe("auth wrapperProperty", () => { + it("nests auth constructor options under wrapperProperty", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: IR_FILEPATH, + config: buildGeneratorConfig({}), + modifyIr: addBearerAuthWrapperProperty + }); + + const response = await generator.generate(REQUEST); + + expect(response.snippet).toContain("bearerAuth: {"); + expect(response.snippet).toContain("token:"); + }); + + it("keeps auth constructor options flat without wrapperProperty", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: IR_FILEPATH, + config: buildGeneratorConfig({}) + }); + + const response = await generator.generate(REQUEST); + + expect(response.snippet).toContain("token:"); + expect(response.snippet).not.toContain("bearerAuth"); + }); +}); diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts new file mode 100644 index 000000000000..70d0094ee724 --- /dev/null +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts @@ -0,0 +1,84 @@ +import { FernIr } from "@fern-api/dynamic-ir-sdk"; +import { AbsoluteFilePath, join } from "@fern-api/path-utils"; + +import { buildDynamicSnippetsGenerator } from "./utils/buildDynamicSnippetsGenerator.js"; +import { buildGeneratorConfig } from "./utils/buildGeneratorConfig.js"; + +const DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY = AbsoluteFilePath.of( + `${__dirname}/../../../../../packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions` +); +const IR_FILEPATH = AbsoluteFilePath.of(join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "exhaustive.json")); + +const REQUEST: FernIr.dynamic.EndpointSnippetRequest = { + endpoint: { + method: "POST", + path: "/params/body-and-query" + }, + baseURL: undefined, + environment: undefined, + auth: { + type: "bearer", + token: "" + }, + pathParameters: undefined, + queryParameters: undefined, + headers: undefined, + requestBody: { + string: "value" + } +}; + +describe("flattenRequestParameters", () => { + it("flattens referenced object request bodies when enabled", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + flattenRequestParameters: true + } + }) + }); + + const response = await generator.generate(REQUEST); + + expect(response.snippet).toContain('string: "value"'); + expect(response.snippet).not.toContain("body:"); + }); + + it("preserves the body property by default", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: IR_FILEPATH, + config: buildGeneratorConfig({}) + }); + + const response = await generator.generate(REQUEST); + + expect(response.snippet).toContain("body: {"); + }); + + it("preserves the body property for a non-object referenced body", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + flattenRequestParameters: true + } + }) + }); + + const response = await generator.generate({ + ...REQUEST, + endpoint: { + method: "POST", + path: "/test-headers/custom-header" + }, + headers: { + "X-TEST-SERVICE-HEADER": "service", + "X-TEST-ENDPOINT-HEADER": "endpoint" + }, + requestBody: "value" + }); + + expect(response.snippet).toContain("body:"); + }); +}); diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts index f0c83621cf07..03773a2653aa 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts @@ -6,12 +6,16 @@ import { DynamicSnippetsGenerator } from "../../DynamicSnippetsGenerator.js"; export function buildDynamicSnippetsGenerator({ irFilepath, - config + config, + modifyIr }: { irFilepath: AbsoluteFilePath; config: FernGeneratorExec.GeneratorConfig; + modifyIr?: ( + ir: import("@fern-api/dynamic-ir-sdk").FernIr.dynamic.DynamicIntermediateRepresentation + ) => import("@fern-api/dynamic-ir-sdk").FernIr.dynamic.DynamicIntermediateRepresentation; }): DynamicSnippetsGenerator { const content = readFileSync(irFilepath, "utf-8"); const ir = JSON.parse(content); - return new DynamicSnippetsGenerator({ ir, config }); + return new DynamicSnippetsGenerator({ ir: modifyIr?.(ir) ?? ir, config }); } diff --git a/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml b/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml new file mode 100644 index 000000000000..acd7e487d96e --- /dev/null +++ b/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + TypeScript dynamic snippets honor `flattenRequestParameters` by spreading referenced object bodies instead of emitting `body: {...}`, and nest multi-auth constructor options under the auth scheme key. + type: fix diff --git a/packages/cli/cli/changes/unreleased/dynamic-auth-wrapper-property.yml b/packages/cli/cli/changes/unreleased/dynamic-auth-wrapper-property.yml new file mode 100644 index 000000000000..535b2e16d747 --- /dev/null +++ b/packages/cli/cli/changes/unreleased/dynamic-auth-wrapper-property.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + dynamic IR now sets `wrapperProperty` on auth schemes when the API has multiple auth schemes so snippet generators can nest constructor options the way generated SDKs expect. + type: fix diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts new file mode 100644 index 000000000000..682678edb479 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts @@ -0,0 +1,50 @@ +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { convertIrToDynamicSnippetsIr } from "@fern-api/ir-generator"; +import { FernIr } from "@fern-api/ir-sdk"; +import path from "path"; + +import { generateIRFromPath } from "../../ir/__test__/generateAndSnapshotIR.js"; + +const TEST_DEFINITIONS_DIR = path.join(__dirname, "../../../../../../../test-definitions"); + +type AuthWithWrapperProperty = FernIr.dynamic.Auth & { + wrapperProperty?: FernIr.dynamic.Name; +}; + +function hasAuthWrapperProperty(auth: FernIr.dynamic.Auth): auth is AuthWithWrapperProperty { + return "wrapperProperty" in auth; +} + +function getAuthWrapperProperty(auth: FernIr.dynamic.Auth | undefined): FernIr.dynamic.Name | undefined { + if (auth == null || !hasAuthWrapperProperty(auth)) { + return undefined; + } + return auth.wrapperProperty; +} + +describe("dynamic auth wrapperProperty", () => { + it("leaves wrapperProperty unset for a single auth scheme", async () => { + const ir = await generateIRFromPath({ + absolutePathToWorkspace: AbsoluteFilePath.of(path.join(TEST_DEFINITIONS_DIR, "fern/apis/basic-auth")), + workspaceName: "dynamicAuthWrapperPropertySingle", + audiences: { type: "all" } + }); + const dynamicIr = convertIrToDynamicSnippetsIr({ ir, smartCasing: true, disableExamples: true }); + const endpoint = Object.values(dynamicIr.endpoints)[0]; + + expect(getAuthWrapperProperty(endpoint?.auth)).toBeUndefined(); + }); + + it("sets wrapperProperty to the camelCase auth scheme key for ANY auth", async () => { + const ir = await generateIRFromPath({ + absolutePathToWorkspace: AbsoluteFilePath.of(path.join(TEST_DEFINITIONS_DIR, "fern/apis/any-auth")), + workspaceName: "dynamicAuthWrapperPropertyAny", + audiences: { type: "all" } + }); + const dynamicIr = convertIrToDynamicSnippetsIr({ ir, smartCasing: true, disableExamples: true }); + const endpoint = Object.values(dynamicIr.endpoints)[0]; + + expect(getAuthWrapperProperty(endpoint?.auth)?.camelCase.safeName).toBe("bearer"); + expect(endpoint?.auth?.type).toBe("bearer"); + }); +}); diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/accept-header.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/accept-header.json index 50a5eff72e81..c43a20e5b97e 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/accept-header.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/accept-header.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/any-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/any-auth.json index 6a7fd718f93e..20ac568eda70 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/any-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/any-auth.json @@ -322,6 +322,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -623,6 +642,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -725,6 +763,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-environment-variables.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-environment-variables.json index bc5b1406501e..07cb78108488 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-environment-variables.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-environment-variables.json @@ -148,7 +148,8 @@ "safeName": "AccessToken" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -271,7 +272,8 @@ "safeName": "AccessToken" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-pw-omitted.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-pw-omitted.json index a3499eac54bf..24242840ba3a 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-pw-omitted.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth-pw-omitted.json @@ -148,7 +148,8 @@ "safeName": "Password" } }, - "passwordOmit": true + "passwordOmit": true, + "wrapperProperty": null }, "declaration": { "name": { @@ -271,7 +272,8 @@ "safeName": "Password" } }, - "passwordOmit": true + "passwordOmit": true, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth.json index 179811421523..efab6c94e2b1 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/basic-auth.json @@ -148,7 +148,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -271,7 +272,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/bearer-token-environment-variable.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/bearer-token-environment-variable.json index 1d30b21a156a..3fd082b0376e 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/bearer-token-environment-variable.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/bearer-token-environment-variable.json @@ -58,7 +58,8 @@ "unsafeName": "APIKey", "safeName": "APIKey" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-any-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-any-auth.json new file mode 100644 index 000000000000..56417631eb6b --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-any-auth.json @@ -0,0 +1,730 @@ +{ + "version": "1.0.0", + "types": { + "type_:TokenResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "properties": [ + { + "name": { + "wireValue": "access_token", + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "expires_in", + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false, + "deferredUnionBaseProperties": null + } + }, + "headers": [], + "endpoints": { + "endpoint_auth.getToken": { + "auth": { + "type": "basic", + "username": { + "originalName": "username", + "camelCase": { + "unsafeName": "username", + "safeName": "username" + }, + "snakeCase": { + "unsafeName": "username", + "safeName": "username" + }, + "screamingSnakeCase": { + "unsafeName": "USERNAME", + "safeName": "USERNAME" + }, + "pascalCase": { + "unsafeName": "Username", + "safeName": "Username" + } + }, + "usernameOmit": null, + "password": { + "originalName": "password", + "camelCase": { + "unsafeName": "password", + "safeName": "password" + }, + "snakeCase": { + "unsafeName": "password", + "safeName": "password" + }, + "screamingSnakeCase": { + "unsafeName": "PASSWORD", + "safeName": "PASSWORD" + }, + "pascalCase": { + "unsafeName": "Password", + "safeName": "Password" + } + }, + "passwordOmit": null, + "wrapperProperty": { + "originalName": "BasicAuth", + "camelCase": { + "unsafeName": "basicAuth", + "safeName": "basicAuth" + }, + "snakeCase": { + "unsafeName": "basic_auth", + "safeName": "basic_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC_AUTH", + "safeName": "BASIC_AUTH" + }, + "pascalCase": { + "unsafeName": "BasicAuth", + "safeName": "BasicAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "getToken", + "camelCase": { + "unsafeName": "getToken", + "safeName": "getToken" + }, + "snakeCase": { + "unsafeName": "get_token", + "safeName": "get_token" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN", + "safeName": "GET_TOKEN" + }, + "pascalCase": { + "unsafeName": "GetToken", + "safeName": "GetToken" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + } + }, + "location": { + "method": "POST", + "path": "/token" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetTokenAuthRequest", + "camelCase": { + "unsafeName": "getTokenAuthRequest", + "safeName": "getTokenAuthRequest" + }, + "snakeCase": { + "unsafeName": "get_token_auth_request", + "safeName": "get_token_auth_request" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN_AUTH_REQUEST", + "safeName": "GET_TOKEN_AUTH_REQUEST" + }, + "pascalCase": { + "unsafeName": "GetTokenAuthRequest", + "safeName": "GetTokenAuthRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "wireValue": "client_id", + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "client_secret", + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "grant_type", + "name": { + "originalName": "grant_type", + "camelCase": { + "unsafeName": "grantType", + "safeName": "grantType" + }, + "snakeCase": { + "unsafeName": "grant_type", + "safeName": "grant_type" + }, + "screamingSnakeCase": { + "unsafeName": "GRANT_TYPE", + "safeName": "GRANT_TYPE" + }, + "pascalCase": { + "unsafeName": "GrantType", + "safeName": "GrantType" + } + } + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_widgets.list": { + "auth": { + "type": "basic", + "username": { + "originalName": "username", + "camelCase": { + "unsafeName": "username", + "safeName": "username" + }, + "snakeCase": { + "unsafeName": "username", + "safeName": "username" + }, + "screamingSnakeCase": { + "unsafeName": "USERNAME", + "safeName": "USERNAME" + }, + "pascalCase": { + "unsafeName": "Username", + "safeName": "Username" + } + }, + "usernameOmit": null, + "password": { + "originalName": "password", + "camelCase": { + "unsafeName": "password", + "safeName": "password" + }, + "snakeCase": { + "unsafeName": "password", + "safeName": "password" + }, + "screamingSnakeCase": { + "unsafeName": "PASSWORD", + "safeName": "PASSWORD" + }, + "pascalCase": { + "unsafeName": "Password", + "safeName": "Password" + } + }, + "passwordOmit": null, + "wrapperProperty": { + "originalName": "BasicAuth", + "camelCase": { + "unsafeName": "basicAuth", + "safeName": "basicAuth" + }, + "snakeCase": { + "unsafeName": "basic_auth", + "safeName": "basic_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC_AUTH", + "safeName": "BASIC_AUTH" + }, + "pascalCase": { + "unsafeName": "BasicAuth", + "safeName": "BasicAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + ], + "packagePath": [], + "file": { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + } + }, + "location": { + "method": "GET", + "path": "/widgets" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_system.health": { + "auth": { + "type": "basic", + "username": { + "originalName": "username", + "camelCase": { + "unsafeName": "username", + "safeName": "username" + }, + "snakeCase": { + "unsafeName": "username", + "safeName": "username" + }, + "screamingSnakeCase": { + "unsafeName": "USERNAME", + "safeName": "USERNAME" + }, + "pascalCase": { + "unsafeName": "Username", + "safeName": "Username" + } + }, + "usernameOmit": null, + "password": { + "originalName": "password", + "camelCase": { + "unsafeName": "password", + "safeName": "password" + }, + "snakeCase": { + "unsafeName": "password", + "safeName": "password" + }, + "screamingSnakeCase": { + "unsafeName": "PASSWORD", + "safeName": "PASSWORD" + }, + "pascalCase": { + "unsafeName": "Password", + "safeName": "Password" + } + }, + "passwordOmit": null, + "wrapperProperty": { + "originalName": "BasicAuth", + "camelCase": { + "unsafeName": "basicAuth", + "safeName": "basicAuth" + }, + "snakeCase": { + "unsafeName": "basic_auth", + "safeName": "basic_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC_AUTH", + "safeName": "BASIC_AUTH" + }, + "pascalCase": { + "unsafeName": "BasicAuth", + "safeName": "BasicAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "health", + "camelCase": { + "unsafeName": "health", + "safeName": "health" + }, + "snakeCase": { + "unsafeName": "health", + "safeName": "health" + }, + "screamingSnakeCase": { + "unsafeName": "HEALTH", + "safeName": "HEALTH" + }, + "pascalCase": { + "unsafeName": "Health", + "safeName": "Health" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + ], + "packagePath": [], + "file": { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + } + }, + "location": { + "method": "GET", + "path": "/health" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": { + "defaultEnvironment": "Production", + "environments": { + "type": "singleBaseUrl", + "environments": [ + { + "id": "Production", + "name": { + "originalName": "Production", + "camelCase": { + "unsafeName": "production", + "safeName": "production" + }, + "snakeCase": { + "unsafeName": "production", + "safeName": "production" + }, + "screamingSnakeCase": { + "unsafeName": "PRODUCTION", + "safeName": "PRODUCTION" + }, + "pascalCase": { + "unsafeName": "Production", + "safeName": "Production" + } + }, + "url": "https://api.us1.example.com", + "docs": null + } + ] + } + }, + "variables": null, + "globalParameters": null, + "generatorConfig": null +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-basic-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-basic-auth.json index 8226131da2a0..4bc18a2c88c7 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-basic-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-basic-auth.json @@ -242,7 +242,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -365,7 +366,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-header-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-header-auth.json index 994ebf0e1f12..5086bd4222ac 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-header-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-header-auth.json @@ -232,7 +232,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -345,7 +346,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-scheme-routing.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-scheme-routing.json new file mode 100644 index 000000000000..8722adc04dc4 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-scheme-routing.json @@ -0,0 +1,385 @@ +{ + "version": "1.0.0", + "types": {}, + "headers": [], + "endpoints": { + "endpoint_widgets.list": { + "auth": { + "type": "header", + "header": { + "name": { + "wireValue": "X-Api-Key", + "name": { + "originalName": "apiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + "wrapperProperty": null + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + ], + "packagePath": [], + "file": { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + } + }, + "location": { + "method": "GET", + "path": "/widgets" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_admin.listUsers": { + "auth": { + "type": "header", + "header": { + "name": { + "wireValue": "X-Api-Key", + "name": { + "originalName": "apiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + "wrapperProperty": null + }, + "declaration": { + "name": { + "originalName": "listUsers", + "camelCase": { + "unsafeName": "listUsers", + "safeName": "listUsers" + }, + "snakeCase": { + "unsafeName": "list_users", + "safeName": "list_users" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_USERS", + "safeName": "LIST_USERS" + }, + "pascalCase": { + "unsafeName": "ListUsers", + "safeName": "ListUsers" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "admin", + "camelCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "snakeCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "screamingSnakeCase": { + "unsafeName": "ADMIN", + "safeName": "ADMIN" + }, + "pascalCase": { + "unsafeName": "Admin", + "safeName": "Admin" + } + } + ], + "packagePath": [], + "file": { + "originalName": "admin", + "camelCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "snakeCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "screamingSnakeCase": { + "unsafeName": "ADMIN", + "safeName": "ADMIN" + }, + "pascalCase": { + "unsafeName": "Admin", + "safeName": "Admin" + } + } + } + }, + "location": { + "method": "GET", + "path": "/admin/users" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_system.health": { + "auth": { + "type": "header", + "header": { + "name": { + "wireValue": "X-Api-Key", + "name": { + "originalName": "apiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + "wrapperProperty": null + }, + "declaration": { + "name": { + "originalName": "health", + "camelCase": { + "unsafeName": "health", + "safeName": "health" + }, + "snakeCase": { + "unsafeName": "health", + "safeName": "health" + }, + "screamingSnakeCase": { + "unsafeName": "HEALTH", + "safeName": "HEALTH" + }, + "pascalCase": { + "unsafeName": "Health", + "safeName": "Health" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + ], + "packagePath": [], + "file": { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + } + }, + "location": { + "method": "GET", + "path": "/health" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": { + "defaultEnvironment": "Default", + "environments": { + "type": "singleBaseUrl", + "environments": [ + { + "id": "Default", + "name": { + "originalName": "Default", + "camelCase": { + "unsafeName": "default", + "safeName": "default" + }, + "snakeCase": { + "unsafeName": "default", + "safeName": "default" + }, + "screamingSnakeCase": { + "unsafeName": "DEFAULT", + "safeName": "DEFAULT" + }, + "pascalCase": { + "unsafeName": "Default", + "safeName": "Default" + } + }, + "url": "https://api.example.com", + "docs": null + } + ] + } + }, + "variables": null, + "globalParameters": null, + "generatorConfig": null +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-spec-namespaced.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-spec-namespaced.json index b2b7c534f81e..ff3c0dcc6cc9 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-spec-namespaced.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-multi-spec-namespaced.json @@ -467,7 +467,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -571,7 +572,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth-login-flow.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth-login-flow.json index 48c04c1a9e25..4cb212b1370d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth-login-flow.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth-login-flow.json @@ -335,7 +335,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -457,7 +458,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -679,7 +681,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -934,7 +937,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth.json index 7b8707c041a4..e5e59e65eb19 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/cli-oauth.json @@ -536,7 +536,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1039,7 +1040,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1385,7 +1387,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1598,7 +1601,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/client-side-params.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/client-side-params.json index 72bf6ff27c64..b71a25f5a227 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/client-side-params.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/client-side-params.json @@ -4183,7 +4183,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4571,7 +4572,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4834,7 +4836,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5142,7 +5145,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5578,7 +5582,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5847,7 +5852,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5955,7 +5961,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6094,7 +6101,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6227,7 +6235,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6498,7 +6507,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6734,7 +6744,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7173,7 +7184,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-env.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-env.json index 36bd474fa535..22dde5a5b111 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-env.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-env.json @@ -79,7 +79,26 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": { + "originalName": "Basic", + "camelCase": { + "unsafeName": "basic", + "safeName": "basic" + }, + "snakeCase": { + "unsafeName": "basic", + "safeName": "basic" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC", + "safeName": "BASIC" + }, + "pascalCase": { + "unsafeName": "Basic", + "safeName": "Basic" + } + } }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json index 6fb724c8f661..39b7f91f0d43 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json @@ -58,7 +58,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-optional.json index dd5b155c17cb..172f70c0e732 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-optional.json @@ -216,7 +216,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-required-grant-type.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-required-grant-type.json index eabdfb428cd0..82bb842d8d27 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-required-grant-type.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-oauth-token-required-grant-type.json @@ -247,7 +247,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/endpoint-security-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/endpoint-security-auth.json index 50627a537005..6916fffc053e 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/endpoint-security-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/endpoint-security-auth.json @@ -322,6 +322,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -623,6 +642,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -725,6 +763,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -827,6 +884,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -929,6 +1005,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -1031,6 +1126,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -1133,6 +1247,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -1235,6 +1368,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/examples.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/examples.json index 37c928f33eea..8c0275213d33 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/examples.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/examples.json @@ -5568,7 +5568,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5638,7 +5639,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5708,7 +5710,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5918,7 +5921,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6229,7 +6233,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6401,7 +6406,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6542,7 +6548,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6675,7 +6682,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6783,7 +6791,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7055,7 +7064,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7163,7 +7173,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/exhaustive.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/exhaustive.json index e674a64211c8..ffe12b1f0a6e 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/exhaustive.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/exhaustive.json @@ -5258,7 +5258,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5408,7 +5409,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5558,7 +5560,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5708,7 +5711,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5858,7 +5862,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6012,7 +6017,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6166,7 +6172,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6320,7 +6327,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6474,7 +6482,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6624,7 +6633,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6771,7 +6781,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6918,7 +6929,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7065,7 +7077,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7237,7 +7250,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7384,7 +7398,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7562,7 +7577,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7740,7 +7756,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7912,7 +7929,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8059,7 +8077,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8206,7 +8225,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8353,7 +8373,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8500,7 +8521,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8678,7 +8700,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8828,7 +8851,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8975,7 +8999,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9122,7 +9147,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9269,7 +9295,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9416,7 +9443,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9563,7 +9591,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9710,7 +9739,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10026,7 +10056,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10198,7 +10229,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10478,7 +10510,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10788,7 +10821,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11104,7 +11138,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11415,7 +11450,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11726,7 +11762,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11904,7 +11941,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12212,7 +12250,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12386,7 +12425,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12697,7 +12737,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13004,7 +13045,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13176,7 +13218,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13348,7 +13391,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13495,7 +13539,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13642,7 +13687,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13789,7 +13835,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13936,7 +13983,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14083,7 +14131,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14230,7 +14279,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14377,7 +14427,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14524,7 +14575,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14671,7 +14723,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14951,7 +15004,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15098,7 +15152,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15239,7 +15294,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15380,7 +15436,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15521,7 +15578,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15662,7 +15720,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15927,7 +15986,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -16163,7 +16223,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -16270,7 +16331,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -16372,7 +16434,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -16474,7 +16537,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-content-type.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-content-type.json index d44451674cb7..b1e8bb14889d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-content-type.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-content-type.json @@ -157,7 +157,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json index d82fef1cb41e..e85793478824 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json @@ -3968,7 +3968,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4118,7 +4119,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4268,7 +4270,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4418,7 +4421,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4568,7 +4572,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4722,7 +4727,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -4876,7 +4882,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5030,7 +5037,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5180,7 +5188,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5327,7 +5336,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5474,7 +5484,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -5787,7 +5798,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6101,7 +6113,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6417,7 +6430,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -6730,7 +6744,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7044,7 +7059,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7360,7 +7376,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7673,7 +7690,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7987,7 +8005,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8303,7 +8322,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8450,7 +8470,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8622,7 +8643,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8769,7 +8791,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8947,7 +8970,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9125,7 +9149,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9297,7 +9322,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9444,7 +9470,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9591,7 +9618,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9738,7 +9766,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9885,7 +9914,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10063,7 +10093,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10213,7 +10244,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10360,7 +10392,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10507,7 +10540,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10823,7 +10857,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10995,7 +11030,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11275,7 +11311,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11585,7 +11622,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11901,7 +11939,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12212,7 +12251,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12523,7 +12563,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12701,7 +12742,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13009,7 +13051,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13183,7 +13226,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13330,7 +13374,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13477,7 +13522,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13624,7 +13670,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13771,7 +13818,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13918,7 +13966,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14065,7 +14114,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14212,7 +14262,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14359,7 +14410,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14506,7 +14558,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14786,7 +14839,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14933,7 +14987,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15074,7 +15129,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15215,7 +15271,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15356,7 +15413,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15497,7 +15555,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15762,7 +15821,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15869,7 +15929,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -15971,7 +16032,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -16073,7 +16135,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-global-headers.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-global-headers.json index 183367eabc69..3fb2fbc2a9be 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-global-headers.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-global-headers.json @@ -118,7 +118,8 @@ "unsafeName": "APIKey", "safeName": "APIKey" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-nullable.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-nullable.json index f9df5c81af50..60df9c122b6e 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-nullable.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-nullable.json @@ -216,7 +216,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-optional.json index dd5b155c17cb..172f70c0e732 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-oauth-token-optional.json @@ -216,7 +216,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-optional-header-env.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-optional-header-env.json index af009bf00c97..549f75f4a510 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-optional-header-env.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-optional-header-env.json @@ -58,7 +58,8 @@ "unsafeName": "APIKey", "safeName": "APIKey" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-undiscriminated-union-wire-tests.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-undiscriminated-union-wire-tests.json index 7e8206a36c24..0cb6640fa385 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-undiscriminated-union-wire-tests.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-undiscriminated-union-wire-tests.json @@ -444,7 +444,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth-environment-variable.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth-environment-variable.json index 99c1895228ed..6b42cffeec23 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth-environment-variable.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth-environment-variable.json @@ -35,7 +35,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth.json index 99c1895228ed..6b42cffeec23 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/header-auth.json @@ -35,7 +35,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/idempotency-headers.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/idempotency-headers.json index 2f417e427400..85bfa198f751 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/idempotency-headers.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/idempotency-headers.json @@ -138,7 +138,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -373,7 +374,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json index 4d72d4dfaab0..c6a098a824a9 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json @@ -182,7 +182,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -417,7 +418,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-explicit.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-explicit.json index 2cf2b78cfa77..20f61dc7cf98 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-explicit.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-explicit.json @@ -296,7 +296,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -767,7 +768,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1268,7 +1270,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1515,7 +1518,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1762,7 +1766,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-api-key.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-api-key.json index f12194c50e7c..2d4de885afe0 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-api-key.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-api-key.json @@ -233,7 +233,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -448,7 +449,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -602,7 +604,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -756,7 +759,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-no-expiry.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-no-expiry.json index 69789b07f2f1..0ca0b6b902a8 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-no-expiry.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-no-expiry.json @@ -266,7 +266,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -737,7 +738,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1238,7 +1240,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1485,7 +1488,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1732,7 +1736,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-reference.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-reference.json index 2bd5414d545f..c82c6504f536 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-reference.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit-reference.json @@ -758,7 +758,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -942,7 +943,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1126,7 +1128,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1343,7 +1346,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1560,7 +1564,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit.json index 9eace0776cf7..67f0eb7663e1 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/inferred-auth-implicit.json @@ -299,7 +299,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -776,7 +777,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1280,7 +1282,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1530,7 +1533,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1780,7 +1784,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-builder-extension.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-builder-extension.json index 4cd90667c6ff..e8106352033d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-builder-extension.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-builder-extension.json @@ -127,7 +127,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-custom-package-prefix.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-custom-package-prefix.json index ef1905375eec..a65f2d0fd2a7 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-custom-package-prefix.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-custom-package-prefix.json @@ -390,7 +390,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -498,7 +499,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-endpoint-security-token-subpackage.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-endpoint-security-token-subpackage.json index 3023ff1058b6..fb8e1b3ae398 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-endpoint-security-token-subpackage.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-endpoint-security-token-subpackage.json @@ -342,7 +342,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { @@ -732,7 +751,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { @@ -854,7 +892,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { @@ -976,7 +1033,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-idempotency-headers-file-upload.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-idempotency-headers-file-upload.json index 29478c12af28..20d66b354bec 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-idempotency-headers-file-upload.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-idempotency-headers-file-upload.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-optional.json index dd5b155c17cb..172f70c0e732 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-optional.json @@ -216,7 +216,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json index 6661a10348d4..b0925c2c17aa 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json @@ -435,7 +435,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -790,7 +791,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-no-default.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-no-default.json index 503b5b913518..1520a21e7417 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-no-default.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-no-default.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -229,7 +230,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-reference.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-reference.json index 4df9cadab3ec..f905bf957fde 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-reference.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment-reference.json @@ -127,7 +127,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -229,7 +230,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -464,7 +466,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment.json index 0d64b17feaf4..bc6f02e29a4c 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multi-url-environment.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -229,7 +230,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multiple-request-bodies.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multiple-request-bodies.json index 92f076aa65dd..f44aba94ae2c 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multiple-request-bodies.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/multiple-request-bodies.json @@ -332,7 +332,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -533,7 +534,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/no-environment.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/no-environment.json index e15c634693fd..d3ca3b3ea8b1 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/no-environment.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/no-environment.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json index 26857cdcae9d..0d7c0d3fe71f 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json @@ -299,7 +299,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -554,7 +555,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -676,7 +678,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom.json index 8f18f25c9e43..45a61987ec4d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-custom.json @@ -271,7 +271,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -782,7 +783,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1227,7 +1229,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1449,7 +1452,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1671,7 +1675,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-default.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-default.json index d92174e5a843..95261ef9393a 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-default.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-default.json @@ -177,7 +177,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -465,7 +466,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -626,7 +628,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -787,7 +790,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-environment-variables.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-environment-variables.json index dfca75a2b1c0..44755110520d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-environment-variables.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-environment-variables.json @@ -210,7 +210,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -564,7 +565,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -948,7 +950,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1109,7 +1112,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1270,7 +1274,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json index 92153c58bf6c..310313b05ec0 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json @@ -210,7 +210,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -564,7 +565,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -948,7 +950,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1109,7 +1112,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-nested-root.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-nested-root.json index fe868257cb34..bc9df5dab653 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-nested-root.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-nested-root.json @@ -212,7 +212,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -570,7 +571,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -731,7 +733,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -892,7 +895,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-openapi.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-openapi.json index 26857cdcae9d..0d7c0d3fe71f 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-openapi.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-openapi.json @@ -299,7 +299,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -554,7 +555,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -676,7 +678,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-reference.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-reference.json index 7e757e2dc66f..d837694a1ff2 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-reference.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-reference.json @@ -309,7 +309,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -437,7 +438,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-with-variables.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-with-variables.json index 125b50fdd0b9..faa628e1acd6 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-with-variables.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials-with-variables.json @@ -210,7 +210,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -564,7 +565,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -948,7 +950,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1109,7 +1112,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1270,7 +1274,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1423,7 +1428,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials.json index dfca75a2b1c0..44755110520d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/oauth-client-credentials.json @@ -210,7 +210,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -564,7 +565,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -948,7 +950,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1109,7 +1112,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1270,7 +1274,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path-disabled.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path-disabled.json index 5b07b17e6823..61aa2428bf23 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path-disabled.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path-disabled.json @@ -179,7 +179,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -438,7 +439,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path.json index 867b5dddf82d..d82289749a6e 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/openapi-per-spec-base-path.json @@ -179,7 +179,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -438,7 +439,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-custom.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-custom.json index 97ff40efae2a..72af545780f4 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-custom.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-custom.json @@ -348,7 +348,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-uri-path.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-uri-path.json index 80b37c486181..4d0129af2dc7 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-uri-path.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination-uri-path.json @@ -433,7 +433,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -535,7 +536,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination.json index cfb3d2bc2ed5..25b48b56307c 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/pagination.json @@ -7287,7 +7287,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7426,7 +7427,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -7808,7 +7810,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8091,7 +8094,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8377,7 +8381,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -8759,7 +8764,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9141,7 +9147,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9427,7 +9434,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -9776,7 +9784,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10125,7 +10134,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10408,7 +10418,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10691,7 +10702,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -10974,7 +10986,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11257,7 +11270,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11561,7 +11575,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11766,7 +11781,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -11974,7 +11990,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12215,7 +12232,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12519,7 +12537,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -12823,7 +12842,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13031,7 +13051,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13302,7 +13323,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13573,7 +13595,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13778,7 +13801,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -13983,7 +14007,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14188,7 +14213,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14393,7 +14419,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14598,7 +14625,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -14803,7 +14831,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json index 6fb724c8f661..39b7f91f0d43 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json @@ -58,7 +58,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/python-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/python-oauth-token-optional.json index dd5b155c17cb..172f70c0e732 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/python-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/python-oauth-token-optional.json @@ -216,7 +216,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json index 8c946d14ea7f..7ed5c56d4b2b 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json @@ -333,6 +333,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { @@ -645,6 +664,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { @@ -758,6 +796,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { @@ -871,6 +928,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/simple-api.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/simple-api.json index d54d844cae38..03a3f6c82048 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/simple-api.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/simple-api.json @@ -187,7 +187,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-default.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-default.json index 204d952978b4..005e7268a77c 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-default.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-default.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-no-default.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-no-default.json index c729d1a7c69a..c6f9924dfd9c 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-no-default.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/single-url-environment-no-default.json @@ -24,7 +24,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/trace.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/trace.json index 218207102f93..faa7f57eeb92 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/trace.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/trace.json @@ -36743,7 +36743,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -36847,7 +36848,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -36986,7 +36988,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37125,7 +37128,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37264,7 +37268,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37403,7 +37408,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37702,7 +37708,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37874,7 +37881,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38143,7 +38151,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38285,7 +38294,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38387,7 +38397,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38498,7 +38509,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38700,7 +38712,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38994,7 +39007,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39359,7 +39373,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39522,7 +39537,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39694,7 +39710,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39857,7 +39874,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39965,7 +39983,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40104,7 +40123,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40237,7 +40257,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40505,7 +40526,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40638,7 +40660,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40771,7 +40794,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40904,7 +40928,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41006,7 +41031,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41169,7 +41195,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41271,7 +41298,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41412,7 +41440,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41553,7 +41582,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41725,7 +41755,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41927,7 +41958,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42106,7 +42138,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42285,7 +42318,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42495,7 +42529,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-express-casing.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-express-casing.json index 0a2ecb520ec9..066b6c0849fb 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-express-casing.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-express-casing.json @@ -258,7 +258,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -523,7 +524,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-oauth-token-optional.json index dd5b155c17cb..172f70c0e732 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-oauth-token-optional.json @@ -216,7 +216,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/websocket-inferred-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/websocket-inferred-auth.json index bcac4265cf56..89095752539d 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/websocket-inferred-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/websocket-inferred-auth.json @@ -1190,7 +1190,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1661,7 +1662,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts index 213f01de7403..c3e4407cb056 100644 --- a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts +++ b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts @@ -755,40 +755,57 @@ export class DynamicSnippetsConverter { return undefined; } const scheme = auth.schemes[0]; + const wrapperProperty = + auth.requirement === "ANY" || auth.requirement === "ENDPOINT_SECURITY" + ? this.fullCasingsGenerator.generateName(scheme.key) + : undefined; switch (scheme.type) { case "basic": { - return DynamicSnippets.Auth.basic({ + const basicAuth = { + wrapperProperty, username: this.inflateName(scheme.username), usernameOmit: scheme.usernameOmit, password: this.inflateName(scheme.password), passwordOmit: scheme.passwordOmit - }); + }; + return DynamicSnippets.Auth.basic(basicAuth); } - case "bearer": - return DynamicSnippets.Auth.bearer({ + case "bearer": { + const bearerAuth = { + wrapperProperty, token: this.inflateName(scheme.token) - }); - case "header": - return DynamicSnippets.Auth.header({ + }; + return DynamicSnippets.Auth.bearer(bearerAuth); + } + case "header": { + const headerAuth = { + wrapperProperty, header: { name: this.inflateNameAndWireValue(scheme.name), typeReference: this.convertTypeReference(scheme.valueType), propertyAccess: undefined, variable: undefined } - }); + }; + return DynamicSnippets.Auth.header(headerAuth); + } case "oauth": { const customProperties = this.getOAuthCustomProperties(scheme); - return DynamicSnippets.Auth.oauth({ + const oauth = { + wrapperProperty, clientId: this.fullCasingsGenerator.generateName("clientId"), clientSecret: this.fullCasingsGenerator.generateName("clientSecret"), customProperties: customProperties.length > 0 ? customProperties : undefined - }); + }; + return DynamicSnippets.Auth.oauth(oauth); } - case "inferred": - return DynamicSnippets.Auth.inferred({ + case "inferred": { + const inferredAuth = { + wrapperProperty, parameters: this.getInferredAuthParameters(scheme) - }); + }; + return DynamicSnippets.Auth.inferred(inferredAuth); + } default: assertNever(scheme); } diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/VERSION b/packages/ir-sdk/fern/apis/ir-types-latest/VERSION index 4e360d5fa32c..293376e3bc85 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/VERSION +++ b/packages/ir-sdk/fern/apis/ir-types-latest/VERSION @@ -1 +1 @@ -67.25.0 +67.26.0 diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md b/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md index 477bf6115b36..9dcbbf3388e7 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md +++ b/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v67.26.0] - 2026-09-15 + +- Add optional `wrapperProperty` to dynamic auth schemes so snippet generators can nest + constructor options under the auth scheme key when the API uses multiple auth schemes. + ## [v67.25.0] - 2026-09-14 - Add `Encoding.xml` (`XmlEncoding`: `name`, optional `namespace`/`prefix`) on `TypeDeclaration` diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml b/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml index 3edae1c61b43..b6363b6e8737 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml +++ b/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml @@ -19,7 +19,17 @@ types: oauth: OAuthValues inferred: InferredAuthValues + BaseAuth: + properties: + wrapperProperty: + type: optional + docs: | + Set when the API declares multiple auth schemes (requirement ANY / + ENDPOINT_SECURITY) and the SDK's constructor options for this scheme + must be nested under this property, named after the scheme key. + BasicAuth: + extends: BaseAuth properties: username: commons.Name usernameOmit: @@ -36,6 +46,7 @@ types: password: string InferredAuth: + extends: BaseAuth docs: | Inferred auth retrieves tokens dynamically. The client constructor parameters are defined by the parameters list. @@ -54,6 +65,7 @@ types: docs: Map of wire values to their example values for snippets. BearerAuth: + extends: BaseAuth properties: token: commons.Name @@ -62,6 +74,7 @@ types: token: string HeaderAuth: + extends: BaseAuth properties: header: types.NamedParameter @@ -70,6 +83,7 @@ types: value: unknown OAuth: + extends: BaseAuth properties: clientId: commons.Name clientSecret: commons.Name diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BaseAuth.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BaseAuth.ts new file mode 100644 index 000000000000..485ae56e6470 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BaseAuth.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../../index.js"; + +export interface BaseAuth { + /** + * Set when the API declares multiple auth schemes (requirement ANY / + * ENDPOINT_SECURITY) and the SDK's constructor options for this scheme + * must be nested under this property, named after the scheme key. + */ + wrapperProperty: FernIr.dynamic.Name | undefined; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts index e222698dfc4c..1346166b3a1a 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts @@ -2,7 +2,7 @@ import type * as FernIr from "../../../../../index.js"; -export interface BasicAuth { +export interface BasicAuth extends FernIr.dynamic.BaseAuth { username: FernIr.dynamic.Name; /** If true, the username will be omitted from the SDK. */ usernameOmit: boolean | undefined; diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BearerAuth.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BearerAuth.ts index 3bb3b22aa125..1326ccfb7d75 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BearerAuth.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BearerAuth.ts @@ -2,6 +2,6 @@ import type * as FernIr from "../../../../../index.js"; -export interface BearerAuth { +export interface BearerAuth extends FernIr.dynamic.BaseAuth { token: FernIr.dynamic.Name; } diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/HeaderAuth.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/HeaderAuth.ts index d3d1a5f04fa9..78d8601e8aba 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/HeaderAuth.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/HeaderAuth.ts @@ -2,6 +2,6 @@ import type * as FernIr from "../../../../../index.js"; -export interface HeaderAuth { +export interface HeaderAuth extends FernIr.dynamic.BaseAuth { header: FernIr.dynamic.NamedParameter; } diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/InferredAuth.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/InferredAuth.ts index 76e3ef9243d2..5c67ecd77046 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/InferredAuth.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/InferredAuth.ts @@ -6,7 +6,7 @@ import type * as FernIr from "../../../../../index.js"; * Inferred auth retrieves tokens dynamically. The client constructor parameters * are defined by the parameters list. */ -export interface InferredAuth { +export interface InferredAuth extends FernIr.dynamic.BaseAuth { /** List of parameters (from token endpoint) needed for auth, including their types. */ parameters: FernIr.dynamic.NamedParameter[] | undefined; } diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/OAuth.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/OAuth.ts index b194cee72fe0..994dfdb1b088 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/OAuth.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/OAuth.ts @@ -2,7 +2,7 @@ import type * as FernIr from "../../../../../index.js"; -export interface OAuth { +export interface OAuth extends FernIr.dynamic.BaseAuth { clientId: FernIr.dynamic.Name; clientSecret: FernIr.dynamic.Name; /** Custom properties required for the OAuth token endpoint request, beyond clientId and clientSecret. */ diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/index.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/index.ts index e42ca550d847..b47fb5665024 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/index.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/index.ts @@ -1,5 +1,6 @@ export * from "./Auth.js"; export * from "./AuthValues.js"; +export * from "./BaseAuth.js"; export * from "./BasicAuth.js"; export * from "./BasicAuthValues.js"; export * from "./BearerAuth.js"; diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BaseAuth.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BaseAuth.ts new file mode 100644 index 000000000000..cfd79e63670c --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BaseAuth.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../../../api/index.js"; +import * as core from "../../../../../../core/index.js"; +import type * as serializers from "../../../../../index.js"; +import { Name } from "../../commons/types/Name.js"; + +export const BaseAuth: core.serialization.ObjectSchema = + core.serialization.objectWithoutOptionalProperties({ + wrapperProperty: Name.optional(), + }); + +export declare namespace BaseAuth { + export interface Raw { + wrapperProperty?: Name.Raw | null; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BasicAuth.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BasicAuth.ts index f0355d2a3f59..c0457cc438ac 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BasicAuth.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BasicAuth.ts @@ -4,17 +4,20 @@ import type * as FernIr from "../../../../../../api/index.js"; import * as core from "../../../../../../core/index.js"; import type * as serializers from "../../../../../index.js"; import { Name } from "../../commons/types/Name.js"; +import { BaseAuth } from "./BaseAuth.js"; export const BasicAuth: core.serialization.ObjectSchema = - core.serialization.objectWithoutOptionalProperties({ - username: Name, - usernameOmit: core.serialization.boolean().optional(), - password: Name, - passwordOmit: core.serialization.boolean().optional(), - }); + core.serialization + .objectWithoutOptionalProperties({ + username: Name, + usernameOmit: core.serialization.boolean().optional(), + password: Name, + passwordOmit: core.serialization.boolean().optional(), + }) + .extend(BaseAuth); export declare namespace BasicAuth { - export interface Raw { + export interface Raw extends BaseAuth.Raw { username: Name.Raw; usernameOmit?: boolean | null; password: Name.Raw; diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BearerAuth.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BearerAuth.ts index 1834fdf1ae9a..88868dff1ca5 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BearerAuth.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BearerAuth.ts @@ -4,16 +4,19 @@ import type * as FernIr from "../../../../../../api/index.js"; import * as core from "../../../../../../core/index.js"; import type * as serializers from "../../../../../index.js"; import { Name } from "../../commons/types/Name.js"; +import { BaseAuth } from "./BaseAuth.js"; export const BearerAuth: core.serialization.ObjectSchema< serializers.dynamic.BearerAuth.Raw, FernIr.dynamic.BearerAuth -> = core.serialization.objectWithoutOptionalProperties({ - token: Name, -}); +> = core.serialization + .objectWithoutOptionalProperties({ + token: Name, + }) + .extend(BaseAuth); export declare namespace BearerAuth { - export interface Raw { + export interface Raw extends BaseAuth.Raw { token: Name.Raw; } } diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/HeaderAuth.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/HeaderAuth.ts index 2f5c05a8d854..73397b07e359 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/HeaderAuth.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/HeaderAuth.ts @@ -4,16 +4,19 @@ import type * as FernIr from "../../../../../../api/index.js"; import * as core from "../../../../../../core/index.js"; import type * as serializers from "../../../../../index.js"; import { NamedParameter } from "../../types/types/NamedParameter.js"; +import { BaseAuth } from "./BaseAuth.js"; export const HeaderAuth: core.serialization.ObjectSchema< serializers.dynamic.HeaderAuth.Raw, FernIr.dynamic.HeaderAuth -> = core.serialization.objectWithoutOptionalProperties({ - header: NamedParameter, -}); +> = core.serialization + .objectWithoutOptionalProperties({ + header: NamedParameter, + }) + .extend(BaseAuth); export declare namespace HeaderAuth { - export interface Raw { + export interface Raw extends BaseAuth.Raw { header: NamedParameter.Raw; } } diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/InferredAuth.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/InferredAuth.ts index 1fbb089e09e2..3dd27d77e8de 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/InferredAuth.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/InferredAuth.ts @@ -4,16 +4,19 @@ import type * as FernIr from "../../../../../../api/index.js"; import * as core from "../../../../../../core/index.js"; import type * as serializers from "../../../../../index.js"; import { NamedParameter } from "../../types/types/NamedParameter.js"; +import { BaseAuth } from "./BaseAuth.js"; export const InferredAuth: core.serialization.ObjectSchema< serializers.dynamic.InferredAuth.Raw, FernIr.dynamic.InferredAuth -> = core.serialization.objectWithoutOptionalProperties({ - parameters: core.serialization.list(NamedParameter).optional(), -}); +> = core.serialization + .objectWithoutOptionalProperties({ + parameters: core.serialization.list(NamedParameter).optional(), + }) + .extend(BaseAuth); export declare namespace InferredAuth { - export interface Raw { + export interface Raw extends BaseAuth.Raw { parameters?: NamedParameter.Raw[] | null; } } diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/OAuth.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/OAuth.ts index f16ba909450a..e975dff41f9d 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/OAuth.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/OAuth.ts @@ -5,16 +5,19 @@ import * as core from "../../../../../../core/index.js"; import type * as serializers from "../../../../../index.js"; import { Name } from "../../commons/types/Name.js"; import { NamedParameter } from "../../types/types/NamedParameter.js"; +import { BaseAuth } from "./BaseAuth.js"; export const OAuth: core.serialization.ObjectSchema = - core.serialization.objectWithoutOptionalProperties({ - clientId: Name, - clientSecret: Name, - customProperties: core.serialization.list(NamedParameter).optional(), - }); + core.serialization + .objectWithoutOptionalProperties({ + clientId: Name, + clientSecret: Name, + customProperties: core.serialization.list(NamedParameter).optional(), + }) + .extend(BaseAuth); export declare namespace OAuth { - export interface Raw { + export interface Raw extends BaseAuth.Raw { clientId: Name.Raw; clientSecret: Name.Raw; customProperties?: NamedParameter.Raw[] | null; diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/index.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/index.ts index e42ca550d847..b47fb5665024 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/index.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/index.ts @@ -1,5 +1,6 @@ export * from "./Auth.js"; export * from "./AuthValues.js"; +export * from "./BaseAuth.js"; export * from "./BasicAuth.js"; export * from "./BasicAuthValues.js"; export * from "./BearerAuth.js"; From f741603e947148163cdf8f7fa028f416e75d0489 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 15:20:08 +0000 Subject: [PATCH 02/16] chore(typescript): address review nits on dynamic snippet auth/flatten Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 14 ++++++++----- .../src/__test__/AuthWrapperProperty.test.ts | 2 +- .../utils/buildDynamicSnippetsGenerator.ts | 5 +++-- .../authWrapperPropertyDynamic.test.ts | 20 ++----------------- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 8427245a2a4f..277d4dfc31ed 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -24,6 +24,7 @@ type AuthWithWrapperProperty = AuthFields & { wrapperProperty?: FernIr.dynamic.Name; }; +// TODO: remove once @fern-api/dynamic-ir-sdk >= 67.26.0 ships wrapperProperty on Auth function hasAuthWrapperProperty(auth: AuthFields): auth is AuthWithWrapperProperty { return "wrapperProperty" in auth; } @@ -688,6 +689,7 @@ export class EndpointSnippetGenerator { case "properties": return this.getInlinedRequestBodyPropertyObjectFields({ parameters: body.value, value }); case "referenced": { + let literal: ts.TypeLiteral | undefined; if ( this.context.customConfig?.flattenRequestParameters === true && body.bodyType.type === "typeReference" && @@ -695,18 +697,18 @@ export class EndpointSnippetGenerator { ) { const named = this.context.resolveNamedType({ typeId: body.bodyType.value.value }); if (named?.type === "object") { - const flattened = this.context.dynamicTypeLiteralMapper.convert({ + literal = this.context.dynamicTypeLiteralMapper.convert({ typeReference: body.bodyType.value, value, convertOpts: { isForRequest: true } }); - const fields = flattened.getObjectFields(); + const fields = literal.getObjectFields(); if (fields != null) { return fields; } } } - const field = this.getReferencedRequestBodyPropertyObjectField({ body, value }); + const field = this.getReferencedRequestBodyPropertyObjectField({ body, value, literal }); // an example that omits an optional request body has no value to write, so the // property is dropped rather than passed explicitly as undefined return ts.TypeLiteral.isNop(field.value) ? [] : [field]; @@ -731,14 +733,16 @@ export class EndpointSnippetGenerator { private getReferencedRequestBodyPropertyObjectField({ body, - value + value, + literal }: { body: FernIr.dynamic.ReferencedRequestBody; value: unknown; + literal?: ts.TypeLiteral; }): ts.ObjectField { return { name: this.context.getPropertyName(body.bodyKey), - value: this.getReferencedRequestBodyPropertyTypeLiteral({ body: body.bodyType, value }) + value: literal ?? this.getReferencedRequestBodyPropertyTypeLiteral({ body: body.bodyType, value }) }; } diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts index f36f8007a2bc..87daf6500c29 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts @@ -103,6 +103,6 @@ describe("auth wrapperProperty", () => { const response = await generator.generate(REQUEST); expect(response.snippet).toContain("token:"); - expect(response.snippet).not.toContain("bearerAuth"); + expect(response.snippet).not.toContain("bearerAuth: {"); }); }); diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts index 03773a2653aa..cb4ce70c3c82 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts @@ -1,4 +1,5 @@ import { FernGeneratorExec } from "@fern-api/browser-compatible-base-generator"; +import { FernIr } from "@fern-api/dynamic-ir-sdk"; import { AbsoluteFilePath } from "@fern-api/path-utils"; import { readFileSync } from "fs"; @@ -12,8 +13,8 @@ export function buildDynamicSnippetsGenerator({ irFilepath: AbsoluteFilePath; config: FernGeneratorExec.GeneratorConfig; modifyIr?: ( - ir: import("@fern-api/dynamic-ir-sdk").FernIr.dynamic.DynamicIntermediateRepresentation - ) => import("@fern-api/dynamic-ir-sdk").FernIr.dynamic.DynamicIntermediateRepresentation; + ir: FernIr.dynamic.DynamicIntermediateRepresentation + ) => FernIr.dynamic.DynamicIntermediateRepresentation; }): DynamicSnippetsGenerator { const content = readFileSync(irFilepath, "utf-8"); const ir = JSON.parse(content); diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts index 682678edb479..75aabf4274ba 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts @@ -1,27 +1,11 @@ import { AbsoluteFilePath } from "@fern-api/fs-utils"; import { convertIrToDynamicSnippetsIr } from "@fern-api/ir-generator"; -import { FernIr } from "@fern-api/ir-sdk"; import path from "path"; import { generateIRFromPath } from "../../ir/__test__/generateAndSnapshotIR.js"; const TEST_DEFINITIONS_DIR = path.join(__dirname, "../../../../../../../test-definitions"); -type AuthWithWrapperProperty = FernIr.dynamic.Auth & { - wrapperProperty?: FernIr.dynamic.Name; -}; - -function hasAuthWrapperProperty(auth: FernIr.dynamic.Auth): auth is AuthWithWrapperProperty { - return "wrapperProperty" in auth; -} - -function getAuthWrapperProperty(auth: FernIr.dynamic.Auth | undefined): FernIr.dynamic.Name | undefined { - if (auth == null || !hasAuthWrapperProperty(auth)) { - return undefined; - } - return auth.wrapperProperty; -} - describe("dynamic auth wrapperProperty", () => { it("leaves wrapperProperty unset for a single auth scheme", async () => { const ir = await generateIRFromPath({ @@ -32,7 +16,7 @@ describe("dynamic auth wrapperProperty", () => { const dynamicIr = convertIrToDynamicSnippetsIr({ ir, smartCasing: true, disableExamples: true }); const endpoint = Object.values(dynamicIr.endpoints)[0]; - expect(getAuthWrapperProperty(endpoint?.auth)).toBeUndefined(); + expect(endpoint?.auth?.wrapperProperty).toBeUndefined(); }); it("sets wrapperProperty to the camelCase auth scheme key for ANY auth", async () => { @@ -44,7 +28,7 @@ describe("dynamic auth wrapperProperty", () => { const dynamicIr = convertIrToDynamicSnippetsIr({ ir, smartCasing: true, disableExamples: true }); const endpoint = Object.values(dynamicIr.endpoints)[0]; - expect(getAuthWrapperProperty(endpoint?.auth)?.camelCase.safeName).toBe("bearer"); + expect(endpoint?.auth?.wrapperProperty?.camelCase.safeName).toBe("bearer"); expect(endpoint?.auth?.type).toBe("bearer"); }); }); From 8e66d5e9811da63e833795c344ef169c3960db2a Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 15:23:23 +0000 Subject: [PATCH 03/16] fix(typescript): drop path params that collide with flattened body fields in snippets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 9 ++- .../__test__/FlattenRequestParameters.test.ts | 68 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 277d4dfc31ed..1e937e41e7cd 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -671,8 +671,15 @@ export class EndpointSnippetGenerator { : []; this.context.errors.unscope(); + const bodyFieldNames = new Set(requestBodyFields.map((field) => field.name)); + return ts.TypeLiteral.object({ - fields: [...pathParameterFields, ...queryParameterFields, ...headerFields, ...requestBodyFields] + fields: [ + ...pathParameterFields.filter((field) => !bodyFieldNames.has(field.name)), + ...queryParameterFields, + ...headerFields, + ...requestBodyFields + ] }); } diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts index 70d0094ee724..136d928855c3 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts @@ -9,6 +9,37 @@ const DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY = AbsoluteFilePath.of( ); const IR_FILEPATH = AbsoluteFilePath.of(join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "exhaustive.json")); +const STRING_NAME: FernIr.dynamic.Name = { + originalName: "string", + camelCase: { + unsafeName: "string", + safeName: "string" + }, + pascalCase: { + unsafeName: "String", + safeName: "String" + }, + snakeCase: { + unsafeName: "string", + safeName: "string" + }, + screamingSnakeCase: { + unsafeName: "STRING", + safeName: "STRING" + } +}; + +const STRING_PATH_PARAMETER: FernIr.dynamic.NamedParameter = { + name: { + name: STRING_NAME, + wireValue: "string" + }, + typeReference: { + type: "primitive", + value: "STRING" + } +}; + const REQUEST: FernIr.dynamic.EndpointSnippetRequest = { endpoint: { method: "POST", @@ -81,4 +112,41 @@ describe("flattenRequestParameters", () => { expect(response.snippet).toContain("body:"); }); + + it("omits path parameters that collide with flattened body fields", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + flattenRequestParameters: true + } + }), + modifyIr: (ir) => { + const endpoint = ir.endpoints["endpoint_endpoints/params.createWithBodyAndQuery"]; + if (endpoint == null || endpoint.request.type !== "inlined") { + throw new Error("Expected the body-and-query endpoint to have an inlined request"); + } + endpoint.location.path = "/params/body-and-query/{string}"; + endpoint.request.pathParameters = [STRING_PATH_PARAMETER]; + return ir; + } + }); + + const response = await generator.generate({ + ...REQUEST, + endpoint: { + method: "POST", + path: "/params/body-and-query/{string}" + }, + pathParameters: { + string: "path" + }, + requestBody: { + string: "body" + } + }); + + expect(response.snippet.match(/\bstring:/g)?.length).toBe(1); + expect(response.snippet).toContain('string: "body"'); + }); }); From 21d16f973aa3cf16ce8825b36c1d844ef6f08322 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 15:57:09 +0000 Subject: [PATCH 04/16] test(cli): update auth wrapperProperty snapshots Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../endpoint-security-per-endpoint-auth.json | 76 + .../__snapshots__/dependencies.test.ts.snap | 2 +- .../tests/ir/__snapshots__/ir.test.ts.snap | 3 +- .../code-samples-open-api.json | 3 +- .../multiple-request-bodies.json | 6 +- .../test-definitions-openapi/names.json | 3 +- .../test-definitions/accept-header.json | 3 +- .../__test__/test-definitions/any-auth.json | 57 + .../basic-auth-environment-variables.json | 6 +- .../basic-auth-pw-omitted.json | 6 +- .../__test__/test-definitions/basic-auth.json | 6 +- .../bearer-token-environment-variable.json | 3 +- .../test-definitions/cli-any-auth.json | 2029 +++++++++++++++++ .../test-definitions/cli-basic-auth.json | 6 +- .../test-definitions/cli-header-auth.json | 6 +- .../cli-multi-scheme-routing.json | 1175 ++++++++++ .../cli-multi-spec-namespaced.json | 6 +- .../cli-oauth-login-flow.json | 12 +- .../__test__/test-definitions/cli-oauth.json | 12 +- .../test-definitions/client-side-params.json | 36 +- .../csharp-global-header-env.json | 21 +- .../csharp-global-header-literal-env.json | 3 +- .../csharp-oauth-token-optional.json | 3 +- ...sharp-oauth-token-required-grant-type.json | 3 +- .../endpoint-security-auth.json | 152 ++ .../__test__/test-definitions/examples.json | 33 +- .../__test__/test-definitions/exhaustive.json | 192 +- .../test-definitions/go-content-type.json | 3 +- .../go-deterministic-ordering.json | 189 +- .../test-definitions/go-global-headers.json | 3 +- .../go-oauth-token-nullable.json | 3 +- .../go-oauth-token-optional.json | 3 +- .../go-optional-header-env.json | 3 +- .../go-undiscriminated-union-wire-tests.json | 3 +- .../header-auth-environment-variable.json | 3 +- .../test-definitions/header-auth.json | 3 +- .../test-definitions/idempotency-headers.json | 6 +- .../ir/__test__/test-definitions/imdb.json | 6 +- .../inferred-auth-explicit.json | 15 +- .../inferred-auth-implicit-api-key.json | 12 +- .../inferred-auth-implicit-no-expiry.json | 15 +- .../inferred-auth-implicit-reference.json | 15 +- .../inferred-auth-implicit.json | 15 +- .../java-builder-extension.json | 3 +- .../java-custom-package-prefix.json | 6 +- ...va-endpoint-security-token-subpackage.json | 84 +- .../java-idempotency-headers-file-upload.json | 3 +- .../java-oauth-token-optional.json | 3 +- ...-oauth-token-required-enum-grant-type.json | 6 +- .../multi-url-environment-no-default.json | 6 +- .../multi-url-environment-reference.json | 9 +- .../multi-url-environment.json | 6 +- .../multiple-request-bodies.json | 6 +- .../test-definitions/no-environment.json | 3 +- ...ent-credentials-custom-prefix-openapi.json | 9 +- .../oauth-client-credentials-custom.json | 15 +- .../oauth-client-credentials-default.json | 12 +- ...ent-credentials-environment-variables.json | 15 +- ...uth-client-credentials-mandatory-auth.json | 12 +- .../oauth-client-credentials-nested-root.json | 12 +- .../oauth-client-credentials-openapi.json | 9 +- .../oauth-client-credentials-reference.json | 6 +- ...uth-client-credentials-with-variables.json | 18 +- .../oauth-client-credentials.json | 15 +- .../openapi-per-spec-base-path-disabled.json | 6 +- .../openapi-per-spec-base-path.json | 6 +- .../test-definitions/pagination-custom.json | 3 +- .../test-definitions/pagination-uri-path.json | 6 +- .../__test__/test-definitions/pagination.json | 87 +- .../php-global-header-literal-env.json | 3 +- .../python-oauth-token-optional.json | 3 +- ...ndpoint-security-optional-credentials.json | 76 + .../__test__/test-definitions/simple-api.json | 3 +- .../single-url-environment-default.json | 3 +- .../single-url-environment-no-default.json | 3 +- .../ir/__test__/test-definitions/trace.json | 105 +- .../test-definitions/ts-express-casing.json | 6 +- .../ts-oauth-token-optional.json | 3 +- .../websocket-inferred-auth.json | 6 +- 79 files changed, 4376 insertions(+), 361 deletions(-) create mode 100644 packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json create mode 100644 packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json diff --git a/packages/cli/api-importers/v3-importer-tests/src/__test__/__snapshots__/baseline-sdks/endpoint-security-per-endpoint-auth.json b/packages/cli/api-importers/v3-importer-tests/src/__test__/__snapshots__/baseline-sdks/endpoint-security-per-endpoint-auth.json index 5f6c222c00c2..02ba2e927464 100644 --- a/packages/cli/api-importers/v3-importer-tests/src/__test__/__snapshots__/baseline-sdks/endpoint-security-per-endpoint-auth.json +++ b/packages/cli/api-importers/v3-importer-tests/src/__test__/__snapshots__/baseline-sdks/endpoint-security-per-endpoint-auth.json @@ -771,6 +771,25 @@ "endpoints": { "endpoint_.getToken": { "auth": { + "wrapperProperty": { + "originalName": "PlantOAuth", + "camelCase": { + "unsafeName": "plantOAuth", + "safeName": "plantOAuth" + }, + "snakeCase": { + "unsafeName": "plant_o_auth", + "safeName": "plant_o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "PLANT_O_AUTH", + "safeName": "PLANT_O_AUTH" + }, + "pascalCase": { + "unsafeName": "PlantOAuth", + "safeName": "PlantOAuth" + } + }, "clientId": { "originalName": "clientId", "camelCase": { @@ -943,6 +962,25 @@ }, "endpoint_.getPlant": { "auth": { + "wrapperProperty": { + "originalName": "PlantOAuth", + "camelCase": { + "unsafeName": "plantOAuth", + "safeName": "plantOAuth" + }, + "snakeCase": { + "unsafeName": "plant_o_auth", + "safeName": "plant_o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "PLANT_O_AUTH", + "safeName": "PLANT_O_AUTH" + }, + "pascalCase": { + "unsafeName": "PlantOAuth", + "safeName": "PlantOAuth" + } + }, "clientId": { "originalName": "clientId", "camelCase": { @@ -1083,6 +1121,25 @@ }, "endpoint_.listPremiumPlants": { "auth": { + "wrapperProperty": { + "originalName": "PlantOAuth", + "camelCase": { + "unsafeName": "plantOAuth", + "safeName": "plantOAuth" + }, + "snakeCase": { + "unsafeName": "plant_o_auth", + "safeName": "plant_o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "PLANT_O_AUTH", + "safeName": "PLANT_O_AUTH" + }, + "pascalCase": { + "unsafeName": "PlantOAuth", + "safeName": "PlantOAuth" + } + }, "clientId": { "originalName": "clientId", "camelCase": { @@ -1163,6 +1220,25 @@ }, "endpoint_.listPublicPlants": { "auth": { + "wrapperProperty": { + "originalName": "PlantOAuth", + "camelCase": { + "unsafeName": "plantOAuth", + "safeName": "plantOAuth" + }, + "snakeCase": { + "unsafeName": "plant_o_auth", + "safeName": "plant_o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "PLANT_O_AUTH", + "safeName": "PLANT_O_AUTH" + }, + "pascalCase": { + "unsafeName": "PlantOAuth", + "safeName": "PlantOAuth" + } + }, "clientId": { "originalName": "clientId", "camelCase": { diff --git a/packages/cli/ete-tests/src/tests/dependencies/__snapshots__/dependencies.test.ts.snap b/packages/cli/ete-tests/src/tests/dependencies/__snapshots__/dependencies.test.ts.snap index cdab2822237c..6fb9b26e2ae7 100644 --- a/packages/cli/ete-tests/src/tests/dependencies/__snapshots__/dependencies.test.ts.snap +++ b/packages/cli/ete-tests/src/tests/dependencies/__snapshots__/dependencies.test.ts.snap @@ -783,4 +783,4 @@ exports[`dependencies > correctly incorporates dependencies 1`] = ` }" `; -exports[`dependencies > file dependencies 1`] = `2445305`; +exports[`dependencies > file dependencies 1`] = `2445710`; diff --git a/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap b/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap index 6b206403b7e7..5db7e74ff214 100644 --- a/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap +++ b/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap @@ -17559,7 +17559,8 @@ exports[`ir > {"name":"nested-example-reference"} 1`] = ` "safeName": "ClientSecret" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/code-samples-open-api.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/code-samples-open-api.json index 9361d418b962..296ee8bbc48b 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/code-samples-open-api.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/code-samples-open-api.json @@ -1384,7 +1384,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/multiple-request-bodies.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/multiple-request-bodies.json index 7496aae6aadc..ca60ca422813 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/multiple-request-bodies.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/multiple-request-bodies.json @@ -1694,7 +1694,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1895,7 +1896,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/names.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/names.json index ad4e9aeb43a9..5dfcb7dfaa26 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/names.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions-openapi/names.json @@ -1559,7 +1559,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/accept-header.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/accept-header.json index 86ab1cb55010..beba714165e6 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/accept-header.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/accept-header.json @@ -259,7 +259,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/any-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/any-auth.json index a90fdf5c379e..6f51fe09822a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/any-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/any-auth.json @@ -1962,6 +1962,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -2263,6 +2282,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -2365,6 +2403,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-environment-variables.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-environment-variables.json index 796625cde530..be478df75d7f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-environment-variables.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-environment-variables.json @@ -855,7 +855,8 @@ "safeName": "AccessToken" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -978,7 +979,8 @@ "safeName": "AccessToken" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-pw-omitted.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-pw-omitted.json index dabf98d166de..54e6777a65a9 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-pw-omitted.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth-pw-omitted.json @@ -932,7 +932,8 @@ "safeName": "Password" } }, - "passwordOmit": true + "passwordOmit": true, + "wrapperProperty": null }, "declaration": { "name": { @@ -1055,7 +1056,8 @@ "safeName": "Password" } }, - "passwordOmit": true + "passwordOmit": true, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth.json index c568e7435a92..297ab1c6af31 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/basic-auth.json @@ -932,7 +932,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1055,7 +1056,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/bearer-token-environment-variable.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/bearer-token-environment-variable.json index 2f566226c89c..5cce80d077af 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/bearer-token-environment-variable.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/bearer-token-environment-variable.json @@ -268,7 +268,8 @@ "unsafeName": "APIKey", "safeName": "APIKey" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json new file mode 100644 index 000000000000..b09fe24dbbd7 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json @@ -0,0 +1,2029 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "specVersion": "1.0.0", + "apiName": "api", + "apiDisplayName": "Any Auth CLI", + "apiDocs": null, + "auth": { + "requirement": "ANY", + "schemes": [ + { + "_type": "basic", + "username": "username", + "usernameEnvVar": "ACME_ACCOUNT_SID", + "usernameOmit": null, + "usernamePlaceholder": null, + "password": "password", + "passwordEnvVar": "ACME_AUTH_TOKEN", + "passwordOmit": null, + "passwordPlaceholder": null, + "key": "BasicAuth", + "playgroundDocs": null, + "docs": null + }, + { + "_type": "oauth", + "configuration": { + "type": "clientCredentials", + "clientIdEnvVar": "ACME_CLIENT_ID", + "clientSecretEnvVar": "ACME_CLIENT_SECRET", + "tokenPrefix": null, + "tokenHeader": null, + "scopes": null, + "tokenEndpoint": { + "endpointReference": { + "endpointId": "endpoint_auth.getToken", + "serviceId": "service_auth", + "subpackageId": "subpackage_auth" + }, + "requestProperties": { + "clientId": { + "propertyPath": [], + "property": { + "type": "body", + "name": "client_id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + }, + "clientSecret": { + "propertyPath": [], + "property": { + "type": "body", + "name": "client_secret", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + }, + "scopes": null, + "customProperties": [ + { + "propertyPath": [], + "property": { + "type": "body", + "name": "grant_type", + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + } + ] + }, + "responseProperties": { + "accessToken": { + "propertyPath": [], + "property": { + "name": "access_token", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + }, + "expiresIn": { + "propertyPath": [], + "property": { + "name": "expires_in", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + }, + "refreshToken": null + } + }, + "refreshEndpoint": null + }, + "key": "OAuth2", + "playgroundDocs": null, + "docs": null + } + ], + "docs": null + }, + "headers": [], + "idempotencyHeaders": [], + "types": { + "type_:TokenResponse": { + "inline": null, + "name": { + "name": "TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": "access_token", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": "expires_in", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "deferredUnionBaseProperties": null + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + } + }, + "errors": {}, + "services": { + "service_auth": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "auth" + ], + "packagePath": [], + "file": "auth" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_auth.getToken", + "name": "getToken", + "displayName": "Exchange client credentials for an access token", + "subtitle": null, + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/token", + "parts": [] + }, + "fullPath": { + "head": "token", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "inlinedRequestBody", + "name": "GetTokenAuthRequest", + "extends": [], + "properties": [ + { + "name": "client_id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": "client_secret", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": "grant_type", + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "docs": null, + "v2Examples": null, + "contentType": "application/json" + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": "GetTokenAuthRequest", + "bodyKey": "body", + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": "request", + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": "TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse", + "default": null, + "inline": null + }, + "docs": "Token", + "v2Examples": null + } + }, + "status-code": 200, + "isWildcardStatusCode": null, + "docs": "Token" + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "df67dd0c", + "name": null, + "url": "/token", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": "client_id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_id" + } + } + }, + "jsonExample": "client_id" + }, + "originalTypeDeclaration": null + }, + { + "name": "client_secret", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_secret" + } + } + }, + "jsonExample": "client_secret" + }, + "originalTypeDeclaration": null + } + ], + "extraProperties": null, + "jsonExample": { + "client_id": "client_id", + "client_secret": "client_secret" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "TokenResponse", + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": "access_token", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "access_token" + } + } + }, + "jsonExample": "access_token" + }, + "originalTypeDeclaration": { + "typeId": "type_:TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "TokenResponse", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "expires_in", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "originalTypeDeclaration": { + "typeId": "type_:TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "TokenResponse", + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "access_token": "access_token", + "expires_in": 1 + } + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "7ff1252", + "url": "/token", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": "client_id", + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_id" + } + } + }, + "jsonExample": "client_id" + } + }, + { + "name": "client_secret", + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_secret" + } + } + }, + "jsonExample": "client_secret" + } + }, + { + "name": "grant_type", + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + ], + "extraProperties": null, + "jsonExample": { + "client_id": "client_id", + "client_secret": "client_secret" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": "access_token", + "originalTypeDeclaration": { + "name": "TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "access_token" + } + } + }, + "jsonExample": "access_token" + }, + "propertyAccess": null + }, + { + "name": "expires_in", + "originalTypeDeclaration": { + "name": "TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": "TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + } + }, + "jsonExample": { + "access_token": "access_token", + "expires_in": 1 + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_widgets": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "widgets" + ], + "packagePath": [], + "file": "widgets" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_widgets.list", + "name": "list", + "displayName": "List widgets", + "subtitle": null, + "auth": true, + "security": [ + { + "BasicAuth": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/widgets", + "parts": [] + }, + "fullPath": { + "head": "widgets", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": "Widgets", + "v2Examples": null + } + }, + "status-code": 200, + "isWildcardStatusCode": null, + "docs": "Widgets" + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "bb6a4a21", + "name": null, + "url": "/widgets", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string" + ] + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "eecccf02", + "url": "/widgets", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string", + "string" + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_system": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "system" + ], + "packagePath": [], + "file": "system" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_system.health", + "name": "health", + "displayName": "Health check", + "subtitle": null, + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/health", + "parts": [] + }, + "fullPath": { + "head": "health", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "5465b825", + "name": null, + "url": "/health", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "b8b5a106", + "url": "/health", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": "errorInstanceId" + }, + "environments": { + "defaultEnvironment": "Production", + "environments": { + "type": "singleBaseUrl", + "environments": [ + { + "id": "Production", + "name": "Production", + "url": "https://api.us1.example.com", + "audiences": null, + "defaultUrl": "https://api.example.com", + "urlTemplate": "https://api.{region}.example.com", + "urlVariables": [ + { + "id": "region", + "name": "region", + "default": "us1", + "values": null + } + ], + "docs": null + } + ] + } + }, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "globalParameters": null, + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": { + "service_auth": [ + "type_:TokenResponse" + ] + }, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": { + "type_:TokenResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "properties": [ + { + "name": { + "wireValue": "access_token", + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "expires_in", + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false, + "deferredUnionBaseProperties": null + } + }, + "headers": [], + "endpoints": { + "endpoint_auth.getToken": { + "auth": { + "type": "basic", + "username": { + "originalName": "username", + "camelCase": { + "unsafeName": "username", + "safeName": "username" + }, + "snakeCase": { + "unsafeName": "username", + "safeName": "username" + }, + "screamingSnakeCase": { + "unsafeName": "USERNAME", + "safeName": "USERNAME" + }, + "pascalCase": { + "unsafeName": "Username", + "safeName": "Username" + } + }, + "usernameOmit": null, + "password": { + "originalName": "password", + "camelCase": { + "unsafeName": "password", + "safeName": "password" + }, + "snakeCase": { + "unsafeName": "password", + "safeName": "password" + }, + "screamingSnakeCase": { + "unsafeName": "PASSWORD", + "safeName": "PASSWORD" + }, + "pascalCase": { + "unsafeName": "Password", + "safeName": "Password" + } + }, + "passwordOmit": null, + "wrapperProperty": { + "originalName": "BasicAuth", + "camelCase": { + "unsafeName": "basicAuth", + "safeName": "basicAuth" + }, + "snakeCase": { + "unsafeName": "basic_auth", + "safeName": "basic_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC_AUTH", + "safeName": "BASIC_AUTH" + }, + "pascalCase": { + "unsafeName": "BasicAuth", + "safeName": "BasicAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "getToken", + "camelCase": { + "unsafeName": "getToken", + "safeName": "getToken" + }, + "snakeCase": { + "unsafeName": "get_token", + "safeName": "get_token" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN", + "safeName": "GET_TOKEN" + }, + "pascalCase": { + "unsafeName": "GetToken", + "safeName": "GetToken" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + } + }, + "location": { + "method": "POST", + "path": "/token" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetTokenAuthRequest", + "camelCase": { + "unsafeName": "getTokenAuthRequest", + "safeName": "getTokenAuthRequest" + }, + "snakeCase": { + "unsafeName": "get_token_auth_request", + "safeName": "get_token_auth_request" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN_AUTH_REQUEST", + "safeName": "GET_TOKEN_AUTH_REQUEST" + }, + "pascalCase": { + "unsafeName": "GetTokenAuthRequest", + "safeName": "GetTokenAuthRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "auth", + "camelCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "snakeCase": { + "unsafeName": "auth", + "safeName": "auth" + }, + "screamingSnakeCase": { + "unsafeName": "AUTH", + "safeName": "AUTH" + }, + "pascalCase": { + "unsafeName": "Auth", + "safeName": "Auth" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "wireValue": "client_id", + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "client_secret", + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "grant_type", + "name": { + "originalName": "grant_type", + "camelCase": { + "unsafeName": "grantType", + "safeName": "grantType" + }, + "snakeCase": { + "unsafeName": "grant_type", + "safeName": "grant_type" + }, + "screamingSnakeCase": { + "unsafeName": "GRANT_TYPE", + "safeName": "GRANT_TYPE" + }, + "pascalCase": { + "unsafeName": "GrantType", + "safeName": "GrantType" + } + } + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_widgets.list": { + "auth": { + "type": "basic", + "username": { + "originalName": "username", + "camelCase": { + "unsafeName": "username", + "safeName": "username" + }, + "snakeCase": { + "unsafeName": "username", + "safeName": "username" + }, + "screamingSnakeCase": { + "unsafeName": "USERNAME", + "safeName": "USERNAME" + }, + "pascalCase": { + "unsafeName": "Username", + "safeName": "Username" + } + }, + "usernameOmit": null, + "password": { + "originalName": "password", + "camelCase": { + "unsafeName": "password", + "safeName": "password" + }, + "snakeCase": { + "unsafeName": "password", + "safeName": "password" + }, + "screamingSnakeCase": { + "unsafeName": "PASSWORD", + "safeName": "PASSWORD" + }, + "pascalCase": { + "unsafeName": "Password", + "safeName": "Password" + } + }, + "passwordOmit": null, + "wrapperProperty": { + "originalName": "BasicAuth", + "camelCase": { + "unsafeName": "basicAuth", + "safeName": "basicAuth" + }, + "snakeCase": { + "unsafeName": "basic_auth", + "safeName": "basic_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC_AUTH", + "safeName": "BASIC_AUTH" + }, + "pascalCase": { + "unsafeName": "BasicAuth", + "safeName": "BasicAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + ], + "packagePath": [], + "file": { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + } + }, + "location": { + "method": "GET", + "path": "/widgets" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_system.health": { + "auth": { + "type": "basic", + "username": { + "originalName": "username", + "camelCase": { + "unsafeName": "username", + "safeName": "username" + }, + "snakeCase": { + "unsafeName": "username", + "safeName": "username" + }, + "screamingSnakeCase": { + "unsafeName": "USERNAME", + "safeName": "USERNAME" + }, + "pascalCase": { + "unsafeName": "Username", + "safeName": "Username" + } + }, + "usernameOmit": null, + "password": { + "originalName": "password", + "camelCase": { + "unsafeName": "password", + "safeName": "password" + }, + "snakeCase": { + "unsafeName": "password", + "safeName": "password" + }, + "screamingSnakeCase": { + "unsafeName": "PASSWORD", + "safeName": "PASSWORD" + }, + "pascalCase": { + "unsafeName": "Password", + "safeName": "Password" + } + }, + "passwordOmit": null, + "wrapperProperty": { + "originalName": "BasicAuth", + "camelCase": { + "unsafeName": "basicAuth", + "safeName": "basicAuth" + }, + "snakeCase": { + "unsafeName": "basic_auth", + "safeName": "basic_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC_AUTH", + "safeName": "BASIC_AUTH" + }, + "pascalCase": { + "unsafeName": "BasicAuth", + "safeName": "BasicAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "health", + "camelCase": { + "unsafeName": "health", + "safeName": "health" + }, + "snakeCase": { + "unsafeName": "health", + "safeName": "health" + }, + "screamingSnakeCase": { + "unsafeName": "HEALTH", + "safeName": "HEALTH" + }, + "pascalCase": { + "unsafeName": "Health", + "safeName": "Health" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + ], + "packagePath": [], + "file": { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + } + }, + "location": { + "method": "GET", + "path": "/health" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": { + "defaultEnvironment": "Production", + "environments": { + "type": "singleBaseUrl", + "environments": [ + { + "id": "Production", + "name": { + "originalName": "Production", + "camelCase": { + "unsafeName": "production", + "safeName": "production" + }, + "snakeCase": { + "unsafeName": "production", + "safeName": "production" + }, + "screamingSnakeCase": { + "unsafeName": "PRODUCTION", + "safeName": "PRODUCTION" + }, + "pascalCase": { + "unsafeName": "Production", + "safeName": "Production" + } + }, + "url": "https://api.us1.example.com", + "docs": null + } + ] + } + }, + "variables": null, + "globalParameters": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "casingsConfig": { + "generationLanguage": null, + "keywords": null, + "smartCasing": true, + "smartCasingDigitWordBoundary": null + }, + "subpackages": { + "subpackage_auth": { + "name": "auth", + "displayName": null, + "fernFilepath": { + "allParts": [ + "auth" + ], + "packagePath": [], + "file": "auth" + }, + "service": "service_auth", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "subpackage_widgets": { + "name": "widgets", + "displayName": null, + "fernFilepath": { + "allParts": [ + "widgets" + ], + "packagePath": [], + "file": "widgets" + }, + "service": "service_widgets", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "subpackage_system": { + "name": "system", + "displayName": null, + "fernFilepath": { + "allParts": [ + "system" + ], + "packagePath": [], + "file": "system" + }, + "service": "service_system", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [ + "type_:TokenResponse" + ], + "errors": [], + "subpackages": [ + "subpackage_auth", + "subpackage_widgets", + "subpackage_system" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": false, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "hasFileDownloadEndpoints": false, + "idempotencyKeyGeneration": null, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-basic-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-basic-auth.json index 0c5beea22fb5..17403d6d2e69 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-basic-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-basic-auth.json @@ -1161,7 +1161,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1284,7 +1285,8 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-header-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-header-auth.json index 2dce9c85ab08..67c5eb4c810c 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-header-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-header-auth.json @@ -1161,7 +1161,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1274,7 +1275,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json new file mode 100644 index 000000000000..ac541824f0c9 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json @@ -0,0 +1,1175 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "specVersion": "1.0.0", + "apiName": "api", + "apiDisplayName": "Multi Scheme Routing CLI", + "apiDocs": null, + "auth": { + "requirement": "ALL", + "schemes": [ + { + "_type": "header", + "name": { + "wireValue": "X-Api-Key", + "name": "apiKey" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "prefix": null, + "headerEnvVar": null, + "headerPlaceholder": null, + "key": "ApiKeyAuth", + "playgroundDocs": null, + "docs": null + } + ], + "docs": null + }, + "headers": [], + "idempotencyHeaders": [], + "types": {}, + "errors": {}, + "services": { + "service_widgets": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "widgets" + ], + "packagePath": [], + "file": "widgets" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_widgets.list", + "name": "list", + "displayName": "List widgets", + "subtitle": null, + "auth": true, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/widgets", + "parts": [] + }, + "fullPath": { + "head": "widgets", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": "Widgets", + "v2Examples": null + } + }, + "status-code": 200, + "isWildcardStatusCode": null, + "docs": "Widgets" + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "bb6a4a21", + "name": null, + "url": "/widgets", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string" + ] + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "eecccf02", + "url": "/widgets", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string", + "string" + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_admin": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "admin" + ], + "packagePath": [], + "file": "admin" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_admin.listUsers", + "name": "listUsers", + "displayName": "List users", + "subtitle": null, + "auth": true, + "security": [ + { + "AdminBasic": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/admin/users", + "parts": [] + }, + "fullPath": { + "head": "admin/users", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": "Users", + "v2Examples": null + } + }, + "status-code": 200, + "isWildcardStatusCode": null, + "docs": "Users" + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "bb6a4a21", + "name": null, + "url": "/admin/users", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string" + ] + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "eecccf02", + "url": "/admin/users", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string", + "string" + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_system": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "system" + ], + "packagePath": [], + "file": "system" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_system.health", + "name": "health", + "displayName": "Health check", + "subtitle": null, + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/health", + "parts": [] + }, + "fullPath": { + "head": "health", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "5465b825", + "name": null, + "url": "/health", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "b8b5a106", + "url": "/health", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": "errorInstanceId" + }, + "environments": { + "defaultEnvironment": "Default", + "environments": { + "type": "singleBaseUrl", + "environments": [ + { + "id": "Default", + "name": "Default", + "url": "https://api.example.com", + "audiences": null, + "defaultUrl": null, + "urlTemplate": null, + "urlVariables": null, + "docs": null + } + ] + } + }, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "globalParameters": null, + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": {}, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": {}, + "headers": [], + "endpoints": { + "endpoint_widgets.list": { + "auth": { + "type": "header", + "header": { + "name": { + "wireValue": "X-Api-Key", + "name": { + "originalName": "apiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + "wrapperProperty": null + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + ], + "packagePath": [], + "file": { + "originalName": "widgets", + "camelCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "snakeCase": { + "unsafeName": "widgets", + "safeName": "widgets" + }, + "screamingSnakeCase": { + "unsafeName": "WIDGETS", + "safeName": "WIDGETS" + }, + "pascalCase": { + "unsafeName": "Widgets", + "safeName": "Widgets" + } + } + } + }, + "location": { + "method": "GET", + "path": "/widgets" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_admin.listUsers": { + "auth": { + "type": "header", + "header": { + "name": { + "wireValue": "X-Api-Key", + "name": { + "originalName": "apiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + "wrapperProperty": null + }, + "declaration": { + "name": { + "originalName": "listUsers", + "camelCase": { + "unsafeName": "listUsers", + "safeName": "listUsers" + }, + "snakeCase": { + "unsafeName": "list_users", + "safeName": "list_users" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_USERS", + "safeName": "LIST_USERS" + }, + "pascalCase": { + "unsafeName": "ListUsers", + "safeName": "ListUsers" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "admin", + "camelCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "snakeCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "screamingSnakeCase": { + "unsafeName": "ADMIN", + "safeName": "ADMIN" + }, + "pascalCase": { + "unsafeName": "Admin", + "safeName": "Admin" + } + } + ], + "packagePath": [], + "file": { + "originalName": "admin", + "camelCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "snakeCase": { + "unsafeName": "admin", + "safeName": "admin" + }, + "screamingSnakeCase": { + "unsafeName": "ADMIN", + "safeName": "ADMIN" + }, + "pascalCase": { + "unsafeName": "Admin", + "safeName": "Admin" + } + } + } + }, + "location": { + "method": "GET", + "path": "/admin/users" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_system.health": { + "auth": { + "type": "header", + "header": { + "name": { + "wireValue": "X-Api-Key", + "name": { + "originalName": "apiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + "wrapperProperty": null + }, + "declaration": { + "name": { + "originalName": "health", + "camelCase": { + "unsafeName": "health", + "safeName": "health" + }, + "snakeCase": { + "unsafeName": "health", + "safeName": "health" + }, + "screamingSnakeCase": { + "unsafeName": "HEALTH", + "safeName": "HEALTH" + }, + "pascalCase": { + "unsafeName": "Health", + "safeName": "Health" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + ], + "packagePath": [], + "file": { + "originalName": "system", + "camelCase": { + "unsafeName": "system", + "safeName": "system" + }, + "snakeCase": { + "unsafeName": "system", + "safeName": "system" + }, + "screamingSnakeCase": { + "unsafeName": "SYSTEM", + "safeName": "SYSTEM" + }, + "pascalCase": { + "unsafeName": "System", + "safeName": "System" + } + } + } + }, + "location": { + "method": "GET", + "path": "/health" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": { + "defaultEnvironment": "Default", + "environments": { + "type": "singleBaseUrl", + "environments": [ + { + "id": "Default", + "name": { + "originalName": "Default", + "camelCase": { + "unsafeName": "default", + "safeName": "default" + }, + "snakeCase": { + "unsafeName": "default", + "safeName": "default" + }, + "screamingSnakeCase": { + "unsafeName": "DEFAULT", + "safeName": "DEFAULT" + }, + "pascalCase": { + "unsafeName": "Default", + "safeName": "Default" + } + }, + "url": "https://api.example.com", + "docs": null + } + ] + } + }, + "variables": null, + "globalParameters": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "casingsConfig": { + "generationLanguage": null, + "keywords": null, + "smartCasing": true, + "smartCasingDigitWordBoundary": null + }, + "subpackages": { + "subpackage_widgets": { + "name": "widgets", + "displayName": null, + "fernFilepath": { + "allParts": [ + "widgets" + ], + "packagePath": [], + "file": "widgets" + }, + "service": "service_widgets", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "subpackage_admin": { + "name": "admin", + "displayName": null, + "fernFilepath": { + "allParts": [ + "admin" + ], + "packagePath": [], + "file": "admin" + }, + "service": "service_admin", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "subpackage_system": { + "name": "system", + "displayName": null, + "fernFilepath": { + "allParts": [ + "system" + ], + "packagePath": [], + "file": "system" + }, + "service": "service_system", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_widgets", + "subpackage_admin", + "subpackage_system" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": false, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "hasFileDownloadEndpoints": false, + "idempotencyKeyGeneration": null, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-spec-namespaced.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-spec-namespaced.json index 26e0b4a7f326..97d3293969f7 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-spec-namespaced.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-spec-namespaced.json @@ -2303,7 +2303,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -2407,7 +2408,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth-login-flow.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth-login-flow.json index 05c511a7b700..4ec1cce02b1f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth-login-flow.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth-login-flow.json @@ -1961,7 +1961,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2083,7 +2084,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2305,7 +2307,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2560,7 +2563,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth.json index 34586c8f3ec6..62ffcfe2b79c 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-oauth.json @@ -2821,7 +2821,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3324,7 +3325,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3670,7 +3672,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3883,7 +3886,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/client-side-params.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/client-side-params.json index 3b9df2e460ad..946441a2d85f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/client-side-params.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/client-side-params.json @@ -30506,7 +30506,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -30894,7 +30895,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -31157,7 +31159,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -31465,7 +31468,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -31901,7 +31905,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -32170,7 +32175,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -32278,7 +32284,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -32417,7 +32424,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -32550,7 +32558,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -32821,7 +32830,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -33057,7 +33067,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -33496,7 +33507,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-env.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-env.json index 699769bdee2d..ebd8f6b2128d 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-env.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-env.json @@ -309,7 +309,26 @@ "safeName": "Password" } }, - "passwordOmit": null + "passwordOmit": null, + "wrapperProperty": { + "originalName": "Basic", + "camelCase": { + "unsafeName": "basic", + "safeName": "basic" + }, + "snakeCase": { + "unsafeName": "basic", + "safeName": "basic" + }, + "screamingSnakeCase": { + "unsafeName": "BASIC", + "safeName": "BASIC" + }, + "pascalCase": { + "unsafeName": "Basic", + "safeName": "Basic" + } + } }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json index e02a871c135d..8c45e9913dba 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json @@ -268,7 +268,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-optional.json index 30da83ad153c..dc2fd1270f64 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-optional.json @@ -1287,7 +1287,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-required-grant-type.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-required-grant-type.json index 286542212af5..98001bdd3d71 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-required-grant-type.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-oauth-token-required-grant-type.json @@ -1279,7 +1279,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/endpoint-security-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/endpoint-security-auth.json index 093633cabd2b..f6ee2f8a65a6 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/endpoint-security-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/endpoint-security-auth.json @@ -3490,6 +3490,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -3791,6 +3810,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -3893,6 +3931,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -3995,6 +4052,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -4097,6 +4173,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -4199,6 +4294,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -4301,6 +4415,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { @@ -4403,6 +4536,25 @@ "unsafeName": "Token", "safeName": "Token" } + }, + "wrapperProperty": { + "originalName": "Bearer", + "camelCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "snakeCase": { + "unsafeName": "bearer", + "safeName": "bearer" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER", + "safeName": "BEARER" + }, + "pascalCase": { + "unsafeName": "Bearer", + "safeName": "Bearer" + } } }, "declaration": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/examples.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/examples.json index c229bb905901..5ee881ef1018 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/examples.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/examples.json @@ -20731,7 +20731,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -20801,7 +20802,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -20871,7 +20873,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -21081,7 +21084,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -21392,7 +21396,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -21564,7 +21569,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -21705,7 +21711,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -21838,7 +21845,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -21946,7 +21954,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -22218,7 +22227,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -22326,7 +22336,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/exhaustive.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/exhaustive.json index a932c61e1ca9..f2a511ac222a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/exhaustive.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/exhaustive.json @@ -41212,7 +41212,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41362,7 +41363,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41512,7 +41514,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41662,7 +41665,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41812,7 +41816,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41966,7 +41971,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42120,7 +42126,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42274,7 +42281,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42428,7 +42436,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42578,7 +42587,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42725,7 +42735,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42872,7 +42883,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43019,7 +43031,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43191,7 +43204,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43338,7 +43352,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43516,7 +43531,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43694,7 +43710,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43866,7 +43883,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44013,7 +44031,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44160,7 +44179,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44307,7 +44327,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44454,7 +44475,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44632,7 +44654,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44782,7 +44805,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44929,7 +44953,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45076,7 +45101,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45223,7 +45249,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45370,7 +45397,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45517,7 +45545,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45664,7 +45693,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45980,7 +46010,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46152,7 +46183,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46432,7 +46464,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46742,7 +46775,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47058,7 +47092,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47369,7 +47404,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47680,7 +47716,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47858,7 +47895,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48166,7 +48204,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48340,7 +48379,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48651,7 +48691,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48958,7 +48999,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49130,7 +49172,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49302,7 +49345,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49449,7 +49493,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49596,7 +49641,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49743,7 +49789,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49890,7 +49937,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -50037,7 +50085,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -50184,7 +50233,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -50331,7 +50381,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -50478,7 +50529,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -50625,7 +50677,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -50905,7 +50958,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -51052,7 +51106,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -51193,7 +51248,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -51334,7 +51390,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -51475,7 +51532,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -51616,7 +51674,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -51881,7 +51940,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -52117,7 +52177,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -52224,7 +52285,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -52326,7 +52388,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -52428,7 +52491,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-content-type.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-content-type.json index 3d151ebb6607..e2be23fe09e2 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-content-type.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-content-type.json @@ -554,7 +554,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json index 8ea889e8eaac..8343383ea996 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json @@ -37196,7 +37196,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37346,7 +37347,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37496,7 +37498,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37646,7 +37649,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37796,7 +37800,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -37950,7 +37955,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38104,7 +38110,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38258,7 +38265,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38408,7 +38416,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38555,7 +38564,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -38702,7 +38712,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39015,7 +39026,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39329,7 +39341,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39645,7 +39658,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39958,7 +39972,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40272,7 +40287,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40588,7 +40604,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40901,7 +40918,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41215,7 +41233,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41531,7 +41550,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41678,7 +41698,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41850,7 +41871,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41997,7 +42019,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42175,7 +42198,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42353,7 +42377,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42525,7 +42550,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42672,7 +42698,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42819,7 +42846,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42966,7 +42994,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43113,7 +43142,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43291,7 +43321,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43441,7 +43472,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43588,7 +43620,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43735,7 +43768,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44051,7 +44085,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44223,7 +44258,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44503,7 +44539,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44813,7 +44850,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45129,7 +45167,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45440,7 +45479,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45751,7 +45791,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45929,7 +45970,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46237,7 +46279,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46411,7 +46454,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46558,7 +46602,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46705,7 +46750,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46852,7 +46898,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46999,7 +47046,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47146,7 +47194,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47293,7 +47342,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47440,7 +47490,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47587,7 +47638,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -47734,7 +47786,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48014,7 +48067,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48161,7 +48215,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48302,7 +48357,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48443,7 +48499,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48584,7 +48641,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48725,7 +48783,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -48990,7 +49049,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49097,7 +49157,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49199,7 +49260,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -49301,7 +49363,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-global-headers.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-global-headers.json index d8aff945d602..f74e28ef3cd1 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-global-headers.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-global-headers.json @@ -387,7 +387,8 @@ "unsafeName": "APIKey", "safeName": "APIKey" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-nullable.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-nullable.json index 65f050d4254d..003d16fddad7 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-nullable.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-nullable.json @@ -1252,7 +1252,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-optional.json index 7931ae6fc629..adb920216a41 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-oauth-token-optional.json @@ -1287,7 +1287,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-optional-header-env.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-optional-header-env.json index 103542602299..6a9b476cea96 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-optional-header-env.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-optional-header-env.json @@ -278,7 +278,8 @@ "unsafeName": "APIKey", "safeName": "APIKey" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-undiscriminated-union-wire-tests.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-undiscriminated-union-wire-tests.json index 60ea16b109f0..56d139c12fb8 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-undiscriminated-union-wire-tests.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-undiscriminated-union-wire-tests.json @@ -1608,7 +1608,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth-environment-variable.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth-environment-variable.json index 7ddcfd3e4e21..88d6218cb908 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth-environment-variable.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth-environment-variable.json @@ -234,7 +234,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth.json index af53c25ef08e..798a6b182e7c 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/header-auth.json @@ -234,7 +234,8 @@ }, "propertyAccess": null, "variable": null - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/idempotency-headers.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/idempotency-headers.json index 5992824721c6..ad034397528d 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/idempotency-headers.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/idempotency-headers.json @@ -680,7 +680,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -915,7 +916,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json index bdb8c1e15d20..6ef3a836d2eb 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json @@ -1429,7 +1429,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1664,7 +1665,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-explicit.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-explicit.json index 8e566d331b37..2492b71bef4f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-explicit.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-explicit.json @@ -1918,7 +1918,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2389,7 +2390,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2890,7 +2892,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3137,7 +3140,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3384,7 +3388,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-api-key.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-api-key.json index c4d011359ffd..e0ce812791b4 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-api-key.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-api-key.json @@ -1141,7 +1141,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1356,7 +1357,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1510,7 +1512,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -1664,7 +1667,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-no-expiry.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-no-expiry.json index 93c21e8b109b..9ed7f0bfd09a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-no-expiry.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-no-expiry.json @@ -1786,7 +1786,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2257,7 +2258,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2758,7 +2760,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3005,7 +3008,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3252,7 +3256,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-reference.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-reference.json index e202f5e6e1d4..e8b72489363b 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-reference.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit-reference.json @@ -2609,7 +2609,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2793,7 +2794,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2977,7 +2979,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3194,7 +3197,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3411,7 +3415,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit.json index 1c47a8d69613..704c4a981ff5 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/inferred-auth-implicit.json @@ -1936,7 +1936,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2413,7 +2414,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2917,7 +2919,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3167,7 +3170,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3417,7 +3421,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-builder-extension.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-builder-extension.json index 75284e20a0a5..753fabd89d40 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-builder-extension.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-builder-extension.json @@ -458,7 +458,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-custom-package-prefix.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-custom-package-prefix.json index 6e3d3a443c20..236be6df9168 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-custom-package-prefix.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-custom-package-prefix.json @@ -1455,7 +1455,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1563,7 +1564,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-endpoint-security-token-subpackage.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-endpoint-security-token-subpackage.json index 4177ef7f3885..18bc949f92d9 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-endpoint-security-token-subpackage.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-endpoint-security-token-subpackage.json @@ -2369,7 +2369,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { @@ -2759,7 +2778,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { @@ -2881,7 +2919,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { @@ -3003,7 +3060,26 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": { + "originalName": "OAuth", + "camelCase": { + "unsafeName": "oAuth", + "safeName": "oAuth" + }, + "snakeCase": { + "unsafeName": "o_auth", + "safeName": "o_auth" + }, + "screamingSnakeCase": { + "unsafeName": "O_AUTH", + "safeName": "O_AUTH" + }, + "pascalCase": { + "unsafeName": "OAuth", + "safeName": "OAuth" + } + } }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-idempotency-headers-file-upload.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-idempotency-headers-file-upload.json index f0c8f44d6106..47ee2e914019 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-idempotency-headers-file-upload.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-idempotency-headers-file-upload.json @@ -280,7 +280,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-optional.json index ca0a021638c6..d111e35bf594 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-optional.json @@ -1287,7 +1287,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json index 83d0dd9d0e6b..31cd1f3a0b5f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/java-oauth-token-required-enum-grant-type.json @@ -2041,7 +2041,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2396,7 +2397,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-no-default.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-no-default.json index f7dd4276121d..8a3b145dc02d 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-no-default.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-no-default.json @@ -491,7 +491,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -696,7 +697,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-reference.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-reference.json index b01773c04cee..7b96c8fe9780 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-reference.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment-reference.json @@ -1163,7 +1163,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1265,7 +1266,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1500,7 +1502,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment.json index c5623378bcf5..3eb8d61d2df4 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multi-url-environment.json @@ -491,7 +491,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -696,7 +697,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multiple-request-bodies.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multiple-request-bodies.json index 670e7f1a5835..f58a8fa75ae7 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multiple-request-bodies.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/multiple-request-bodies.json @@ -1694,7 +1694,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1895,7 +1896,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/no-environment.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/no-environment.json index 51f8b7444637..ce6fb73aa579 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/no-environment.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/no-environment.json @@ -208,7 +208,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json index 711281fec0e2..17410eb86299 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom-prefix-openapi.json @@ -2323,7 +2323,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2578,7 +2579,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2700,7 +2702,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom.json index 012023505d0f..ba65b74bd2e2 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-custom.json @@ -2197,7 +2197,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -2708,7 +2709,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3153,7 +3155,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3375,7 +3378,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -3597,7 +3601,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-default.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-default.json index 75a0a40d2b2e..af309467065a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-default.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-default.json @@ -1137,7 +1137,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1425,7 +1426,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1586,7 +1588,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1747,7 +1750,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-environment-variables.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-environment-variables.json index 73e115ea52a1..7a11009c6a5a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-environment-variables.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-environment-variables.json @@ -1902,7 +1902,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2256,7 +2257,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2640,7 +2642,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2801,7 +2804,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2962,7 +2966,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json index dfa8d71fcc00..286fb20bad6c 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-mandatory-auth.json @@ -2356,7 +2356,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2710,7 +2711,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3094,7 +3096,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3255,7 +3258,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-nested-root.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-nested-root.json index 98e7965530ef..06ea4d1d0ddd 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-nested-root.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-nested-root.json @@ -1416,7 +1416,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1774,7 +1775,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1935,7 +1937,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2096,7 +2099,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-openapi.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-openapi.json index cf22986537b4..1e3d33b01d90 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-openapi.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-openapi.json @@ -2323,7 +2323,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2578,7 +2579,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2700,7 +2702,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-reference.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-reference.json index cd2e86d7ac7f..3ab01e06f03f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-reference.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-reference.json @@ -1283,7 +1283,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1411,7 +1412,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-with-variables.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-with-variables.json index e75984aa753f..d6d9e0b6b3b7 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-with-variables.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials-with-variables.json @@ -2099,7 +2099,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2453,7 +2454,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2837,7 +2839,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2998,7 +3001,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3159,7 +3163,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3312,7 +3317,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials.json index 0623061f77ec..e5468b5c443e 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/oauth-client-credentials.json @@ -2461,7 +2461,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -2815,7 +2816,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3199,7 +3201,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3360,7 +3363,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -3521,7 +3525,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path-disabled.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path-disabled.json index b34d23729407..5c6f192f6a7a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path-disabled.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path-disabled.json @@ -1153,7 +1153,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1412,7 +1413,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path.json index 7d7a4fcf6d1d..a3978d8e3fab 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/openapi-per-spec-base-path.json @@ -1153,7 +1153,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { @@ -1412,7 +1413,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-custom.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-custom.json index 5b985182980e..8c1b147d86ce 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-custom.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-custom.json @@ -1407,7 +1407,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-uri-path.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-uri-path.json index a50bc5f3cffd..234dcb5847a0 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-uri-path.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination-uri-path.json @@ -2999,7 +2999,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -3101,7 +3102,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination.json index da8e51cfb131..fc79531f8cdb 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/pagination.json @@ -39365,7 +39365,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39504,7 +39505,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -39886,7 +39888,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40169,7 +40172,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40455,7 +40459,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -40837,7 +40842,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41219,7 +41225,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41505,7 +41512,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -41854,7 +41862,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42203,7 +42212,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42486,7 +42496,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -42769,7 +42780,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43052,7 +43064,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43335,7 +43348,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43639,7 +43653,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -43844,7 +43859,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44052,7 +44068,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44293,7 +44310,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44597,7 +44615,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -44901,7 +44920,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45109,7 +45129,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45380,7 +45401,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45651,7 +45673,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -45856,7 +45879,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46061,7 +46085,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46266,7 +46291,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46471,7 +46497,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46676,7 +46703,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -46881,7 +46909,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json index fc8b48cda412..195b59c4f588 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json @@ -268,7 +268,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/python-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/python-oauth-token-optional.json index c89ca13a09dc..0ce5929c4e9d 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/python-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/python-oauth-token-optional.json @@ -1318,7 +1318,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json index f913513314f4..2dcd6bbbfae8 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ruby-endpoint-security-optional-credentials.json @@ -2176,6 +2176,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { @@ -2488,6 +2507,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { @@ -2601,6 +2639,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { @@ -2714,6 +2771,25 @@ }, "propertyAccess": null, "variable": null + }, + "wrapperProperty": { + "originalName": "ApiKey", + "camelCase": { + "unsafeName": "apiKey", + "safeName": "apiKey" + }, + "snakeCase": { + "unsafeName": "api_key", + "safeName": "api_key" + }, + "screamingSnakeCase": { + "unsafeName": "API_KEY", + "safeName": "API_KEY" + }, + "pascalCase": { + "unsafeName": "APIKey", + "safeName": "APIKey" + } } }, "declaration": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/simple-api.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/simple-api.json index de7fa3c328f9..8c434e2a597b 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/simple-api.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/simple-api.json @@ -686,7 +686,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-default.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-default.json index 63c08142d7bd..6af59726df4a 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-default.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-default.json @@ -235,7 +235,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-no-default.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-no-default.json index 34f9db9757b0..f5e8a439de1b 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-no-default.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/single-url-environment-no-default.json @@ -235,7 +235,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/trace.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/trace.json index 8503b6309f40..5d3bc9a1dd8b 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/trace.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/trace.json @@ -151903,7 +151903,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -152007,7 +152008,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -152146,7 +152148,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -152285,7 +152288,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -152424,7 +152428,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -152563,7 +152568,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -152862,7 +152868,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -153034,7 +153041,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -153303,7 +153311,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -153445,7 +153454,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -153547,7 +153557,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -153658,7 +153669,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -153860,7 +153872,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -154154,7 +154167,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -154519,7 +154533,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -154682,7 +154697,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -154854,7 +154870,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155017,7 +155034,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155125,7 +155143,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155264,7 +155283,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155397,7 +155417,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155665,7 +155686,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155798,7 +155820,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -155931,7 +155954,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156064,7 +156088,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156166,7 +156191,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156329,7 +156355,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156431,7 +156458,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156572,7 +156600,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156713,7 +156742,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -156885,7 +156915,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -157087,7 +157118,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -157266,7 +157298,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -157445,7 +157478,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -157655,7 +157689,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-express-casing.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-express-casing.json index 3a29de959eea..4b77ecae4ec7 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-express-casing.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-express-casing.json @@ -1278,7 +1278,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { @@ -1543,7 +1544,8 @@ "unsafeName": "Token", "safeName": "Token" } - } + }, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-oauth-token-optional.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-oauth-token-optional.json index d49fff0821a9..876c1ea343d6 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-oauth-token-optional.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-oauth-token-optional.json @@ -1287,7 +1287,8 @@ "safeName": "ClientSecret" } }, - "customProperties": null + "customProperties": null, + "wrapperProperty": null }, "declaration": { "name": { diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/websocket-inferred-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/websocket-inferred-auth.json index b9523cc056c0..6186028b2ea1 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/websocket-inferred-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/websocket-inferred-auth.json @@ -3864,7 +3864,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { @@ -4335,7 +4336,8 @@ "propertyAccess": null, "variable": null } - ] + ], + "wrapperProperty": null }, "declaration": { "name": { From b881ed0fe325b995670fde858a420d1545cd858e Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 20:52:32 +0000 Subject: [PATCH 05/16] test(cli): add TypeScript flattening fixture snapshots Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ts-flatten-request-any-auth.json | 552 +++++ .../ts-flatten-request-any-auth.json | 1815 +++++++++++++++++ .../definition/api.yml | 19 + .../definition/users.yml | 39 + .../generators.yml | 22 + 5 files changed, 2447 insertions(+) create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-flatten-request-any-auth.json create mode 100644 packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json create mode 100644 test-definitions/fern/apis/ts-flatten-request-any-auth/definition/api.yml create mode 100644 test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml create mode 100644 test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-flatten-request-any-auth.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-flatten-request-any-auth.json new file mode 100644 index 000000000000..e0c5a1403060 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/ts-flatten-request-any-auth.json @@ -0,0 +1,552 @@ +{ + "version": "1.0.0", + "types": { + "type_users:UpdateUser": { + "type": "object", + "declaration": { + "name": { + "originalName": "UpdateUser", + "camelCase": { + "unsafeName": "updateUser", + "safeName": "updateUser" + }, + "snakeCase": { + "unsafeName": "update_user", + "safeName": "update_user" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER", + "safeName": "UPDATE_USER" + }, + "pascalCase": { + "unsafeName": "UpdateUser", + "safeName": "UpdateUser" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "properties": [ + { + "name": { + "wireValue": "id", + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "name", + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false, + "deferredUnionBaseProperties": null + } + }, + "headers": [], + "endpoints": { + "endpoint_users.updateUser": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + }, + "wrapperProperty": { + "originalName": "BearerAuth", + "camelCase": { + "unsafeName": "bearerAuth", + "safeName": "bearerAuth" + }, + "snakeCase": { + "unsafeName": "bearer_auth", + "safeName": "bearer_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER_AUTH", + "safeName": "BEARER_AUTH" + }, + "pascalCase": { + "unsafeName": "BearerAuth", + "safeName": "BearerAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "updateUser", + "camelCase": { + "unsafeName": "updateUser", + "safeName": "updateUser" + }, + "snakeCase": { + "unsafeName": "update_user", + "safeName": "update_user" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER", + "safeName": "UPDATE_USER" + }, + "pascalCase": { + "unsafeName": "UpdateUser", + "safeName": "UpdateUser" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/users/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "UpdateUserRequest", + "camelCase": { + "unsafeName": "updateUserRequest", + "safeName": "updateUserRequest" + }, + "snakeCase": { + "unsafeName": "update_user_request", + "safeName": "update_user_request" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER_REQUEST", + "safeName": "UPDATE_USER_REQUEST" + }, + "pascalCase": { + "unsafeName": "UpdateUserRequest", + "safeName": "UpdateUserRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": { + "type": "referenced", + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "bodyType": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_users:UpdateUser" + } + } + }, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_users.updateUserProfile": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + }, + "wrapperProperty": { + "originalName": "BearerAuth", + "camelCase": { + "unsafeName": "bearerAuth", + "safeName": "bearerAuth" + }, + "snakeCase": { + "unsafeName": "bearer_auth", + "safeName": "bearer_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER_AUTH", + "safeName": "BEARER_AUTH" + }, + "pascalCase": { + "unsafeName": "BearerAuth", + "safeName": "BearerAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "updateUserProfile", + "camelCase": { + "unsafeName": "updateUserProfile", + "safeName": "updateUserProfile" + }, + "snakeCase": { + "unsafeName": "update_user_profile", + "safeName": "update_user_profile" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER_PROFILE", + "safeName": "UPDATE_USER_PROFILE" + }, + "pascalCase": { + "unsafeName": "UpdateUserProfile", + "safeName": "UpdateUserProfile" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/users/{id}/profile" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_users:UpdateUser" + } + }, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "globalParameters": null, + "generatorConfig": null +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json new file mode 100644 index 000000000000..7825adda72da --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json @@ -0,0 +1,1815 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "specVersion": null, + "apiName": "ts-flatten-request-any-auth", + "apiDisplayName": null, + "apiDocs": null, + "auth": { + "requirement": "ANY", + "schemes": [ + { + "_type": "bearer", + "token": "token", + "tokenEnvVar": "MY_TOKEN", + "tokenPlaceholder": null, + "key": "BearerAuth", + "playgroundDocs": null, + "docs": null + }, + { + "_type": "header", + "name": { + "wireValue": "X-API-Key", + "name": "ApiKey" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "prefix": null, + "headerEnvVar": "MY_API_KEY", + "headerPlaceholder": null, + "key": "ApiKey", + "playgroundDocs": null, + "docs": null + } + ], + "docs": null + }, + "headers": [], + "idempotencyHeaders": [], + "types": { + "type_users:UpdateUser": { + "inline": null, + "name": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": "id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": "name", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "defaultValue": null, + "xml": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "deferredUnionBaseProperties": null + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + } + }, + "errors": {}, + "services": { + "service_users": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null, + "xml": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_users.updateUser", + "name": "updateUser", + "displayName": null, + "subtitle": null, + "auth": true, + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKey": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PUT", + "basePath": null, + "path": { + "head": "/users/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "users/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": "id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "clientDefault": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": "id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "clientDefault": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser", + "default": null, + "inline": null + }, + "required": null, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": "UpdateUserRequest", + "bodyKey": "body", + "includePathParameters": true, + "onlyPathParameters": false + }, + "requestParameterName": "request", + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "f10ef3d8", + "name": null, + "url": "/users/path-id", + "rootPathParameters": [], + "endpointPathParameters": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "path-id" + } + } + }, + "jsonExample": "path-id" + } + } + ], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "typeName": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "body-id" + } + } + }, + "jsonExample": "body-id" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "name", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "Ada" + } + } + }, + "jsonExample": "Ada" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "id": "body-id", + "name": "Ada" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "5d5a50bb", + "url": "/users/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + }, + "propertyAccess": null + }, + { + "name": "name", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + } + }, + "jsonExample": { + "id": "id", + "name": "name" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + }, + "propertyAccess": null + }, + { + "name": "name", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + } + }, + "jsonExample": { + "id": "id", + "name": "name" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_users.updateUserProfile", + "name": "updateUserProfile", + "displayName": null, + "subtitle": null, + "auth": true, + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKey": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PUT", + "basePath": null, + "path": { + "head": "/users/", + "parts": [ + { + "pathParameter": "id", + "tail": "/profile" + } + ] + }, + "fullPath": { + "head": "users/", + "parts": [ + { + "pathParameter": "id", + "tail": "/profile" + } + ] + }, + "pathParameters": [ + { + "name": "id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "clientDefault": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": "id", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "clientDefault": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser", + "default": null, + "inline": null + }, + "required": null, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser", + "default": null, + "inline": null + }, + "required": null, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": "request", + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "f10ef3d8", + "name": null, + "url": "/users/path-id/profile", + "rootPathParameters": [], + "endpointPathParameters": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "path-id" + } + } + }, + "jsonExample": "path-id" + } + } + ], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "typeName": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "body-id" + } + } + }, + "jsonExample": "body-id" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "name", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "Ada" + } + } + }, + "jsonExample": "Ada" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "id": "body-id", + "name": "Ada" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "5d5a50bb", + "url": "/users/id/profile", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + }, + "propertyAccess": null + }, + { + "name": "name", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + } + }, + "jsonExample": { + "id": "id", + "name": "name" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + }, + "propertyAccess": null + }, + { + "name": "name", + "originalTypeDeclaration": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": "UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "displayName": null, + "typeId": "type_users:UpdateUser" + } + }, + "jsonExample": { + "id": "id", + "name": "name" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": "errorInstanceId" + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "globalParameters": null, + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": { + "service_users": [ + "type_users:UpdateUser" + ] + }, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": { + "type_users:UpdateUser": { + "type": "object", + "declaration": { + "name": { + "originalName": "UpdateUser", + "camelCase": { + "unsafeName": "updateUser", + "safeName": "updateUser" + }, + "snakeCase": { + "unsafeName": "update_user", + "safeName": "update_user" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER", + "safeName": "UPDATE_USER" + }, + "pascalCase": { + "unsafeName": "UpdateUser", + "safeName": "UpdateUser" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "properties": [ + { + "name": { + "wireValue": "id", + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "name", + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false, + "deferredUnionBaseProperties": null + } + }, + "headers": [], + "endpoints": { + "endpoint_users.updateUser": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + }, + "wrapperProperty": { + "originalName": "BearerAuth", + "camelCase": { + "unsafeName": "bearerAuth", + "safeName": "bearerAuth" + }, + "snakeCase": { + "unsafeName": "bearer_auth", + "safeName": "bearer_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER_AUTH", + "safeName": "BEARER_AUTH" + }, + "pascalCase": { + "unsafeName": "BearerAuth", + "safeName": "BearerAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "updateUser", + "camelCase": { + "unsafeName": "updateUser", + "safeName": "updateUser" + }, + "snakeCase": { + "unsafeName": "update_user", + "safeName": "update_user" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER", + "safeName": "UPDATE_USER" + }, + "pascalCase": { + "unsafeName": "UpdateUser", + "safeName": "UpdateUser" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/users/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "UpdateUserRequest", + "camelCase": { + "unsafeName": "updateUserRequest", + "safeName": "updateUserRequest" + }, + "snakeCase": { + "unsafeName": "update_user_request", + "safeName": "update_user_request" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER_REQUEST", + "safeName": "UPDATE_USER_REQUEST" + }, + "pascalCase": { + "unsafeName": "UpdateUserRequest", + "safeName": "UpdateUserRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": { + "type": "referenced", + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "bodyType": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_users:UpdateUser" + } + } + }, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_users.updateUserProfile": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + }, + "wrapperProperty": { + "originalName": "BearerAuth", + "camelCase": { + "unsafeName": "bearerAuth", + "safeName": "bearerAuth" + }, + "snakeCase": { + "unsafeName": "bearer_auth", + "safeName": "bearer_auth" + }, + "screamingSnakeCase": { + "unsafeName": "BEARER_AUTH", + "safeName": "BEARER_AUTH" + }, + "pascalCase": { + "unsafeName": "BearerAuth", + "safeName": "BearerAuth" + } + } + }, + "declaration": { + "name": { + "originalName": "updateUserProfile", + "camelCase": { + "unsafeName": "updateUserProfile", + "safeName": "updateUserProfile" + }, + "snakeCase": { + "unsafeName": "update_user_profile", + "safeName": "update_user_profile" + }, + "screamingSnakeCase": { + "unsafeName": "UPDATE_USER_PROFILE", + "safeName": "UPDATE_USER_PROFILE" + }, + "pascalCase": { + "unsafeName": "UpdateUserProfile", + "safeName": "UpdateUserProfile" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + ], + "packagePath": [], + "file": { + "originalName": "users", + "camelCase": { + "unsafeName": "users", + "safeName": "users" + }, + "snakeCase": { + "unsafeName": "users", + "safeName": "users" + }, + "screamingSnakeCase": { + "unsafeName": "USERS", + "safeName": "USERS" + }, + "pascalCase": { + "unsafeName": "Users", + "safeName": "Users" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/users/{id}/profile" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_users:UpdateUser" + } + }, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "globalParameters": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "casingsConfig": { + "generationLanguage": null, + "keywords": null, + "smartCasing": true, + "smartCasingDigitWordBoundary": null + }, + "subpackages": { + "subpackage_users": { + "name": "users", + "displayName": null, + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "service": "service_users", + "types": [ + "type_users:UpdateUser" + ], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_users" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": true, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "hasFileDownloadEndpoints": false, + "idempotencyKeyGeneration": null, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/api.yml b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/api.yml new file mode 100644 index 000000000000..1082b553549c --- /dev/null +++ b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/api.yml @@ -0,0 +1,19 @@ +imports: + users: users.yml + +name: ts-flatten-request-any-auth + +auth: + any: + - BearerAuth + - ApiKey + +auth-schemes: + BearerAuth: + scheme: bearer + token: + env: MY_TOKEN + ApiKey: + header: X-API-Key + type: string + env: MY_API_KEY diff --git a/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml new file mode 100644 index 000000000000..35f34782e692 --- /dev/null +++ b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml @@ -0,0 +1,39 @@ +types: + UpdateUser: + properties: + id: string + name: string + +service: + auth: true + base-path: "" + endpoints: + updateUser: + path: /users/{id} + method: PUT + request: + name: UpdateUserRequest + path-parameters: + id: string + body: UpdateUser + response: UpdateUser + examples: + - path-parameters: + id: path-id + request: + id: body-id + name: Ada + + updateUserProfile: + path: /users/{id}/profile + method: PUT + path-parameters: + id: string + request: UpdateUser + response: UpdateUser + examples: + - path-parameters: + id: path-id + request: + id: body-id + name: Ada diff --git a/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml b/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml new file mode 100644 index 000000000000..2e674b398233 --- /dev/null +++ b/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: + php-sdk: + generators: + - name: fernapi/fern-php-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/php-sdk-tests + branch: any-auth + go-sdk: + generators: + - name: fernapi/fern-go-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/go-sdk-tests + branch: any-auth From 7ad7a452d442bad57db5d8d22bcbf0e3d39f2c57 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 20:52:41 +0000 Subject: [PATCH 06/16] test(ts-sdk): add flattening seed fixture output Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- seed/ts-sdk/seed.yml | 6 + .../.fern/metadata.json | 12 + .../.fern/verify.sh | 5 + .../.github/workflows/ci.yml | 46 + .../flatten-request-parameters/.gitignore | 3 + .../CONTRIBUTING.md | 133 ++ .../flatten-request-parameters/README.md | 298 ++++ .../flatten-request-parameters/biome.json | 74 + .../flatten-request-parameters/package.json | 80 ++ .../pnpm-workspace.yaml | 1 + .../flatten-request-parameters/reference.md | 114 ++ .../scripts/rename-to-esm-files.js | 188 +++ .../flatten-request-parameters/snippet.json | 27 + .../src/BaseClient.ts | 120 ++ .../flatten-request-parameters/src/Client.ts | 56 + .../src/api/index.ts | 1 + .../src/api/resources/index.ts | 3 + .../src/api/resources/users/client/Client.ts | 159 +++ .../src/api/resources/users/client/index.ts | 1 + .../client/requests/UpdateUserRequest.ts | 13 + .../resources/users/client/requests/index.ts | 1 + .../src/api/resources/users/exports.ts | 4 + .../src/api/resources/users/index.ts | 2 + .../api/resources/users/types/UpdateUser.ts | 6 + .../src/api/resources/users/types/index.ts | 1 + .../src/auth/AnyAuthProvider.ts | 59 + .../src/auth/BearerAuthProvider.ts | 52 + .../src/auth/HeaderAuthProvider.ts | 53 + .../src/auth/index.ts | 3 + .../src/core/auth/AuthProvider.ts | 15 + .../src/core/auth/AuthRequest.ts | 9 + .../src/core/auth/BasicAuth.ts | 37 + .../src/core/auth/BearerToken.ts | 20 + .../src/core/auth/NoOpAuthProvider.ts | 8 + .../src/core/auth/index.ts | 5 + .../src/core/base64.ts | 27 + .../src/core/exports.ts | 1 + .../src/core/fetcher/APIResponse.ts | 23 + .../src/core/fetcher/BinaryResponse.ts | 34 + .../src/core/fetcher/EndpointMetadata.ts | 13 + .../src/core/fetcher/EndpointSupplier.ts | 14 + .../src/core/fetcher/Fetcher.ts | 311 +++++ .../src/core/fetcher/Headers.ts | 93 ++ .../src/core/fetcher/HttpResponsePromise.ts | 116 ++ .../src/core/fetcher/RawResponse.ts | 61 + .../src/core/fetcher/Supplier.ts | 11 + .../src/core/fetcher/createRequestUrl.ts | 6 + .../src/core/fetcher/getErrorResponseBody.ts | 33 + .../src/core/fetcher/getFetchFn.ts | 3 + .../src/core/fetcher/getHeader.ts | 8 + .../src/core/fetcher/getRequestBody.ts | 20 + .../src/core/fetcher/getResponseBody.ts | 70 + .../src/core/fetcher/index.ts | 13 + .../core/fetcher/makePassthroughRequest.ts | 211 +++ .../src/core/fetcher/makeRequest.ts | 70 + .../src/core/fetcher/redactUrl.ts | 102 ++ .../src/core/fetcher/requestWithRetries.ts | 68 + .../src/core/fetcher/signals.ts | 35 + .../src/core/headers.ts | 33 + .../src/core/index.ts | 6 + .../src/core/json.ts | 27 + .../src/core/logging/exports.ts | 19 + .../src/core/logging/index.ts | 1 + .../src/core/logging/logger.ts | 203 +++ .../src/core/requestBody.ts | 26 + .../src/core/runtime/index.ts | 1 + .../src/core/runtime/runtime.ts | 231 ++++ .../src/core/url/QueryStringBuilder.ts | 87 ++ .../src/core/url/encodePathParam.ts | 18 + .../src/core/url/index.ts | 4 + .../src/core/url/join.ts | 79 ++ .../src/core/url/qs.ts | 87 ++ .../SeedTsFlattenRequestAnyAuthError.ts | 68 + ...SeedTsFlattenRequestAnyAuthTimeoutError.ts | 18 + .../src/errors/handleNonStatusCodeError.ts | 43 + .../src/errors/index.ts | 2 + .../flatten-request-parameters/src/exports.ts | 1 + .../flatten-request-parameters/src/index.ts | 5 + .../flatten-request-parameters/src/version.ts | 1 + .../tests/custom.test.ts | 13 + .../tests/mock-server/MockServer.ts | 29 + .../tests/mock-server/MockServerPool.ts | 106 ++ .../tests/mock-server/mockEndpointBuilder.ts | 234 ++++ .../tests/mock-server/randomBaseUrl.ts | 4 + .../tests/mock-server/setup.ts | 10 + .../tests/mock-server/withFormUrlEncoded.ts | 104 ++ .../tests/mock-server/withHeaders.ts | 70 + .../tests/mock-server/withJson.ts | 173 +++ .../flatten-request-parameters/tests/setup.ts | 80 ++ .../tests/tsconfig.json | 10 + .../tests/unit/auth/BasicAuth.test.ts | 112 ++ .../tests/unit/auth/BearerToken.test.ts | 14 + .../tests/unit/base64.test.ts | 53 + .../tests/unit/fetcher/Fetcher.test.ts | 262 ++++ .../unit/fetcher/HttpResponsePromise.test.ts | 143 ++ .../tests/unit/fetcher/RawResponse.test.ts | 34 + .../unit/fetcher/createRequestUrl.test.ts | 167 +++ .../tests/unit/fetcher/getRequestBody.test.ts | 129 ++ .../unit/fetcher/getResponseBody.test.ts | 123 ++ .../tests/unit/fetcher/logging.test.ts | 517 +++++++ .../fetcher/makePassthroughRequest.test.ts | 504 +++++++ .../tests/unit/fetcher/makeRequest.test.ts | 158 +++ .../tests/unit/fetcher/redacting.test.ts | 1221 +++++++++++++++++ .../unit/fetcher/requestWithRetries.test.ts | 282 ++++ .../tests/unit/fetcher/signals.test.ts | 114 ++ .../tests/unit/fetcher/test-file.txt | 1 + .../tests/unit/logging/logger.test.ts | 454 ++++++ .../tests/unit/url/QueryStringBuilder.test.ts | 236 ++++ .../tests/unit/url/join.test.ts | 284 ++++ .../tests/unit/url/qs.test.ts | 374 +++++ .../tests/wire/.gitkeep | 0 .../tests/wire/users.test.ts | 50 + .../tsconfig.base.json | 17 + .../tsconfig.cjs.json | 9 + .../tsconfig.esm.json | 11 + .../flatten-request-parameters/tsconfig.json | 3 + .../vitest.config.mts | 32 + .../no-custom-config/.fern/metadata.json | 9 + .../no-custom-config/.fern/verify.sh | 5 + .../no-custom-config/.github/workflows/ci.yml | 46 + .../no-custom-config/.gitignore | 3 + .../no-custom-config/CONTRIBUTING.md | 133 ++ .../no-custom-config/README.md | 301 ++++ .../no-custom-config/biome.json | 74 + .../no-custom-config/package.json | 80 ++ .../no-custom-config/pnpm-workspace.yaml | 1 + .../no-custom-config/reference.md | 117 ++ .../scripts/rename-to-esm-files.js | 188 +++ .../no-custom-config/snippet.json | 27 + .../no-custom-config/src/BaseClient.ts | 120 ++ .../no-custom-config/src/Client.ts | 56 + .../no-custom-config/src/api/index.ts | 1 + .../src/api/resources/index.ts | 3 + .../src/api/resources/users/client/Client.ts | 163 +++ .../src/api/resources/users/client/index.ts | 1 + .../client/requests/UpdateUserRequest.ts | 18 + .../resources/users/client/requests/index.ts | 1 + .../src/api/resources/users/exports.ts | 4 + .../src/api/resources/users/index.ts | 2 + .../api/resources/users/types/UpdateUser.ts | 6 + .../src/api/resources/users/types/index.ts | 1 + .../src/auth/AnyAuthProvider.ts | 59 + .../src/auth/BearerAuthProvider.ts | 52 + .../src/auth/HeaderAuthProvider.ts | 53 + .../no-custom-config/src/auth/index.ts | 3 + .../src/core/auth/AuthProvider.ts | 15 + .../src/core/auth/AuthRequest.ts | 9 + .../src/core/auth/BasicAuth.ts | 37 + .../src/core/auth/BearerToken.ts | 20 + .../src/core/auth/NoOpAuthProvider.ts | 8 + .../no-custom-config/src/core/auth/index.ts | 5 + .../no-custom-config/src/core/base64.ts | 27 + .../no-custom-config/src/core/exports.ts | 1 + .../src/core/fetcher/APIResponse.ts | 23 + .../src/core/fetcher/BinaryResponse.ts | 34 + .../src/core/fetcher/EndpointMetadata.ts | 13 + .../src/core/fetcher/EndpointSupplier.ts | 14 + .../src/core/fetcher/Fetcher.ts | 311 +++++ .../src/core/fetcher/Headers.ts | 93 ++ .../src/core/fetcher/HttpResponsePromise.ts | 116 ++ .../src/core/fetcher/RawResponse.ts | 61 + .../src/core/fetcher/Supplier.ts | 11 + .../src/core/fetcher/createRequestUrl.ts | 6 + .../src/core/fetcher/getErrorResponseBody.ts | 33 + .../src/core/fetcher/getFetchFn.ts | 3 + .../src/core/fetcher/getHeader.ts | 8 + .../src/core/fetcher/getRequestBody.ts | 20 + .../src/core/fetcher/getResponseBody.ts | 70 + .../src/core/fetcher/index.ts | 13 + .../core/fetcher/makePassthroughRequest.ts | 211 +++ .../src/core/fetcher/makeRequest.ts | 70 + .../src/core/fetcher/redactUrl.ts | 102 ++ .../src/core/fetcher/requestWithRetries.ts | 68 + .../src/core/fetcher/signals.ts | 35 + .../no-custom-config/src/core/headers.ts | 33 + .../no-custom-config/src/core/index.ts | 6 + .../no-custom-config/src/core/json.ts | 27 + .../src/core/logging/exports.ts | 19 + .../src/core/logging/index.ts | 1 + .../src/core/logging/logger.ts | 203 +++ .../no-custom-config/src/core/requestBody.ts | 26 + .../src/core/runtime/index.ts | 1 + .../src/core/runtime/runtime.ts | 231 ++++ .../src/core/url/QueryStringBuilder.ts | 87 ++ .../src/core/url/encodePathParam.ts | 18 + .../no-custom-config/src/core/url/index.ts | 4 + .../no-custom-config/src/core/url/join.ts | 79 ++ .../no-custom-config/src/core/url/qs.ts | 87 ++ .../SeedTsFlattenRequestAnyAuthError.ts | 68 + ...SeedTsFlattenRequestAnyAuthTimeoutError.ts | 18 + .../src/errors/handleNonStatusCodeError.ts | 43 + .../no-custom-config/src/errors/index.ts | 2 + .../no-custom-config/src/exports.ts | 1 + .../no-custom-config/src/index.ts | 5 + .../no-custom-config/src/version.ts | 1 + .../no-custom-config/tests/custom.test.ts | 13 + .../tests/mock-server/MockServer.ts | 29 + .../tests/mock-server/MockServerPool.ts | 106 ++ .../tests/mock-server/mockEndpointBuilder.ts | 234 ++++ .../tests/mock-server/randomBaseUrl.ts | 4 + .../tests/mock-server/setup.ts | 10 + .../tests/mock-server/withFormUrlEncoded.ts | 104 ++ .../tests/mock-server/withHeaders.ts | 70 + .../tests/mock-server/withJson.ts | 173 +++ .../no-custom-config/tests/setup.ts | 80 ++ .../no-custom-config/tests/tsconfig.json | 10 + .../tests/unit/auth/BasicAuth.test.ts | 112 ++ .../tests/unit/auth/BearerToken.test.ts | 14 + .../tests/unit/base64.test.ts | 53 + .../tests/unit/fetcher/Fetcher.test.ts | 262 ++++ .../unit/fetcher/HttpResponsePromise.test.ts | 143 ++ .../tests/unit/fetcher/RawResponse.test.ts | 34 + .../unit/fetcher/createRequestUrl.test.ts | 167 +++ .../tests/unit/fetcher/getRequestBody.test.ts | 129 ++ .../unit/fetcher/getResponseBody.test.ts | 123 ++ .../tests/unit/fetcher/logging.test.ts | 517 +++++++ .../fetcher/makePassthroughRequest.test.ts | 504 +++++++ .../tests/unit/fetcher/makeRequest.test.ts | 158 +++ .../tests/unit/fetcher/redacting.test.ts | 1221 +++++++++++++++++ .../unit/fetcher/requestWithRetries.test.ts | 282 ++++ .../tests/unit/fetcher/signals.test.ts | 114 ++ .../tests/unit/fetcher/test-file.txt | 1 + .../tests/unit/logging/logger.test.ts | 454 ++++++ .../tests/unit/url/QueryStringBuilder.test.ts | 236 ++++ .../tests/unit/url/join.test.ts | 284 ++++ .../tests/unit/url/qs.test.ts | 374 +++++ .../no-custom-config/tests/wire/.gitkeep | 0 .../no-custom-config/tests/wire/users.test.ts | 53 + .../no-custom-config/tsconfig.base.json | 17 + .../no-custom-config/tsconfig.cjs.json | 9 + .../no-custom-config/tsconfig.esm.json | 11 + .../no-custom-config/tsconfig.json | 3 + .../no-custom-config/vitest.config.mts | 32 + 233 files changed, 20119 insertions(+) create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/metadata.json create mode 100755 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/verify.sh create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.github/workflows/ci.yml create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.gitignore create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/CONTRIBUTING.md create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/biome.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/package.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/pnpm-workspace.yaml create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/scripts/rename-to-esm-files.js create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/BaseClient.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/Client.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/UpdateUser.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/AnyAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/BearerAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/HeaderAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BasicAuth.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BearerToken.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/NoOpAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/base64.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/APIResponse.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/BinaryResponse.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointMetadata.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointSupplier.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Fetcher.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Headers.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/HttpResponsePromise.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/RawResponse.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Supplier.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/createRequestUrl.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getErrorResponseBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getFetchFn.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getHeader.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getRequestBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getResponseBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makeRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/redactUrl.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/requestWithRetries.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/signals.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/headers.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/json.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/logger.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/requestBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/runtime.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/QueryStringBuilder.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/encodePathParam.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/join.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/qs.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthError.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/handleNonStatusCodeError.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/version.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/custom.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServer.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServerPool.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/mockEndpointBuilder.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/randomBaseUrl.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/setup.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withFormUrlEncoded.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withHeaders.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withJson.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/setup.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/tsconfig.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BasicAuth.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BearerToken.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/base64.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/Fetcher.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/HttpResponsePromise.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/RawResponse.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/createRequestUrl.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getRequestBody.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getResponseBody.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/logging.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makeRequest.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/redacting.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/requestWithRetries.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/signals.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/test-file.txt create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/logging/logger.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/QueryStringBuilder.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/join.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/qs.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/.gitkeep create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.base.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.cjs.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.esm.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/vitest.config.mts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/metadata.json create mode 100755 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/verify.sh create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.github/workflows/ci.yml create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.gitignore create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/CONTRIBUTING.md create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/biome.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/package.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/pnpm-workspace.yaml create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/scripts/rename-to-esm-files.js create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/BaseClient.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/Client.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/UpdateUser.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/AnyAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/BearerAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/HeaderAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BasicAuth.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BearerToken.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/NoOpAuthProvider.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/base64.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/APIResponse.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/BinaryResponse.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointMetadata.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointSupplier.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Fetcher.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Headers.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/HttpResponsePromise.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/RawResponse.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Supplier.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/createRequestUrl.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getErrorResponseBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getFetchFn.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getHeader.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getRequestBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getResponseBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makeRequest.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/redactUrl.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/requestWithRetries.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/signals.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/headers.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/json.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/logger.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/requestBody.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/runtime.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/QueryStringBuilder.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/encodePathParam.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/join.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/qs.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthError.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/handleNonStatusCodeError.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/exports.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/index.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/version.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/custom.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServer.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServerPool.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/mockEndpointBuilder.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/randomBaseUrl.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/setup.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withFormUrlEncoded.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withHeaders.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withJson.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/setup.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/tsconfig.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BasicAuth.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BearerToken.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/base64.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/Fetcher.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/HttpResponsePromise.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/RawResponse.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/createRequestUrl.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getRequestBody.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getResponseBody.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/logging.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makeRequest.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/redacting.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/requestWithRetries.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/signals.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/test-file.txt create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/logging/logger.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/QueryStringBuilder.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/join.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/qs.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/.gitkeep create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.base.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.cjs.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.esm.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.json create mode 100644 seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/vitest.config.mts diff --git a/seed/ts-sdk/seed.yml b/seed/ts-sdk/seed.yml index b8409fca01f9..b48e32b30969 100644 --- a/seed/ts-sdk/seed.yml +++ b/seed/ts-sdk/seed.yml @@ -623,6 +623,12 @@ fixtures: namespaceExport: SeedErrors naming: client: SeedErrors + ts-flatten-request-any-auth: + - outputFolder: no-custom-config + customConfig: null + - outputFolder: flatten-request-parameters + customConfig: + flattenRequestParameters: true ts-react-query: - outputFolder: . customConfig: diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/metadata.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/metadata.json new file mode 100644 index 000000000000..cca1b61d33f2 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/metadata.json @@ -0,0 +1,12 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-typescript-sdk", + "generatorVersion": "latest", + "generatorConfig": { + "flattenRequestParameters": true + }, + "originGitCommit": "DUMMY", + "invokedBy": "manual", + "requestedVersion": "0.0.1", + "sdkVersion": "0.0.1" +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/verify.sh b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/verify.sh new file mode 100755 index 000000000000..a224ac815887 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.fern/verify.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +pnpm install +pnpm build +pnpm test diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.github/workflows/ci.yml b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.github/workflows/ci.yml new file mode 100644 index 000000000000..93fba226cb67 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up node + uses: actions/setup-node@v6 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Compile + run: pnpm build + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up node + uses: actions/setup-node@v6 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test + run: pnpm test diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.gitignore b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.gitignore new file mode 100644 index 000000000000..72271e049c02 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +/dist \ No newline at end of file diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/CONTRIBUTING.md b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/CONTRIBUTING.md new file mode 100644 index 000000000000..fe5bc2f77e0b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Node.js 20 or higher +- pnpm package manager + +### Installation + +Install the project dependencies: + +```bash +pnpm install +``` + +### Building + +Build the project: + +```bash +pnpm build +``` + +### Testing + +Run the test suite: + +```bash +pnpm test +``` + +Run specific test types: +- `pnpm test:unit` - Run unit tests +- `pnpm test:wire` - Run wire/integration tests + +### Linting and Formatting + +Check code style: + +```bash +pnpm run lint +pnpm run format:check +``` + +Fix code style issues: + +```bash +pnpm run lint:fix +pnpm run format:fix +``` + +Or use the combined check command: + +```bash +pnpm run check:fix +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/api/` - API client classes and types +- `src/serialization/` - Serialization/deserialization logic +- Most TypeScript files in `src/` + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The TypeScript SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/typescript/sdk/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `pnpm test` +4. Run linting and formatting: `pnpm run check:fix` +5. Build the project: `pnpm build` +6. Commit your changes with a clear commit message +7. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting and linting. Run `pnpm run check:fix` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md new file mode 100644 index 000000000000..03a80754ae1b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md @@ -0,0 +1,298 @@ +# Seed TypeScript Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FTypeScript) +[![npm shield](https://img.shields.io/npm/v/@fern/ts-flatten-request-any-auth)](https://www.npmjs.com/package/@fern/ts-flatten-request-any-auth) + +The Seed TypeScript library provides convenient access to the Seed APIs from TypeScript. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Request and Response Types](#request-and-response-types) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Subpackage Exports](#subpackage-exports) + - [Additional Headers](#additional-headers) + - [Additional Query String Parameters](#additional-query-string-parameters) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Aborting Requests](#aborting-requests) + - [Access Raw Response Data](#access-raw-response-data) + - [Logging](#logging) + - [Custom Fetch](#custom-fetch) + - [Runtime Compatibility](#runtime-compatibility) +- [Contributing](#contributing) + +## Installation + +```sh +npm i -s @fern/ts-flatten-request-any-auth +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```typescript +import { SeedTsFlattenRequestAnyAuthClient } from "@fern/ts-flatten-request-any-auth"; + +const client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: "YOUR_BASE_URL", token: "YOUR_TOKEN", apiKey: "YOUR_API_KEY" }); +await client.users.updateUser({ + id: "body-id", + name: "Ada" +}); +``` + +## Request and Response Types + +The SDK exports all request and response types as TypeScript interfaces. Simply import them with the +following namespace: + +```typescript +import { SeedTsFlattenRequestAnyAuth } from "@fern/ts-flatten-request-any-auth"; + +const request: SeedTsFlattenRequestAnyAuth.UpdateUserRequest = { + ... +}; +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```typescript +import { SeedTsFlattenRequestAnyAuthError } from "@fern/ts-flatten-request-any-auth"; + +try { + await client.users.updateUser(...); +} catch (err) { + if (err instanceof SeedTsFlattenRequestAnyAuthError) { + console.log(err.statusCode); + console.log(err.message); + console.log(err.body); + console.log(err.rawResponse); + } +} +``` + +## Advanced + +### Subpackage Exports + +This SDK supports direct imports of subpackage clients, which allows JavaScript bundlers to tree-shake and include only the imported subpackage code. This results in much smaller bundle sizes. + +```typescript +import { UsersClient } from '@fern/ts-flatten-request-any-auth/users'; + +const client = new UsersClient({...}); +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `headers` request option. + +```typescript +import { SeedTsFlattenRequestAnyAuthClient } from "@fern/ts-flatten-request-any-auth"; + +const client = new SeedTsFlattenRequestAnyAuthClient({ + ... + headers: { + 'X-Custom-Header': 'custom value' + } +}); + +const response = await client.users.updateUser(..., { + headers: { + 'X-Custom-Header': 'custom value' + } +}); +``` + +### Additional Query String Parameters + +If you would like to send additional query string parameters as part of the request, use the `queryParams` request option. + +```typescript +const response = await client.users.updateUser(..., { + queryParams: { + 'customQueryParamKey': 'custom query param value' + } +}); +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +Which status codes are retried depends on the `retryStatusCodes` generator configuration: + +**`legacy`** (current default): retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) + +**`recommended`**: retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `maxRetries` request option to configure this behavior. + +```typescript +const response = await client.users.updateUser(..., { + maxRetries: 0 // override maxRetries at the request level +}); +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. Use the `timeoutInSeconds` option to configure this behavior. + +```typescript +const response = await client.users.updateUser(..., { + timeoutInSeconds: 30 // override timeout to 30s +}); +``` + +### Aborting Requests + +The SDK allows users to abort requests at any point by passing in an abort signal. + +```typescript +const controller = new AbortController(); +const response = await client.users.updateUser(..., { + abortSignal: controller.signal +}); +controller.abort(); // aborts the request +``` + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.withRawResponse()` method. +The `.withRawResponse()` method returns a promise that results to an object with a `data` and a `rawResponse` property. + +```typescript +const { data, rawResponse } = await client.users.updateUser(...).withRawResponse(); + +console.log(data); +console.log(rawResponse.headers['X-My-Header']); +``` + +### Logging + +The SDK supports logging. You can configure the logger by passing in a `logging` object to the client options. + +```typescript +import { SeedTsFlattenRequestAnyAuthClient, logging } from "@fern/ts-flatten-request-any-auth"; + +const client = new SeedTsFlattenRequestAnyAuthClient({ + ... + logging: { + level: logging.LogLevel.Debug, // defaults to logging.LogLevel.Info + logger: new logging.ConsoleLogger(), // defaults to ConsoleLogger + silent: false, // defaults to true, set to false to enable logging + } +}); +``` +The `logging` object can have the following properties: +- `level`: The log level to use. Defaults to `logging.LogLevel.Info`. +- `logger`: The logger to use. Defaults to a `logging.ConsoleLogger`. +- `silent`: Whether to silence the logger. Defaults to `true`. + +The `level` property can be one of the following values: +- `logging.LogLevel.Debug` +- `logging.LogLevel.Info` +- `logging.LogLevel.Warn` +- `logging.LogLevel.Error` + +To provide a custom logger, you can pass in an object that implements the `logging.ILogger` interface. + +
+Custom logger examples + +Here's an example using the popular `winston` logging library. +```ts +import winston from 'winston'; + +const winstonLogger = winston.createLogger({...}); + +const logger: logging.ILogger = { + debug: (msg, ...args) => winstonLogger.debug(msg, ...args), + info: (msg, ...args) => winstonLogger.info(msg, ...args), + warn: (msg, ...args) => winstonLogger.warn(msg, ...args), + error: (msg, ...args) => winstonLogger.error(msg, ...args), +}; +``` + +Here's an example using the popular `pino` logging library. + +```ts +import pino from 'pino'; + +const pinoLogger = pino({...}); + +const logger: logging.ILogger = { + debug: (msg, ...args) => pinoLogger.debug(args, msg), + info: (msg, ...args) => pinoLogger.info(args, msg), + warn: (msg, ...args) => pinoLogger.warn(args, msg), + error: (msg, ...args) => pinoLogger.error(args, msg), +}; +``` +
+ + +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + +### Runtime Compatibility + + +The SDK works in the following runtimes: + + + +- Node.js 18+ +- Vercel +- Cloudflare Workers +- Deno v1.25+ +- Bun 1.0+ +- React Native + + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/biome.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/biome.json new file mode 100644 index 000000000000..6b89164f9f99 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/biome.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "root": true, + "vcs": { + "enabled": false + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!!dist", + "!!**/dist", + "!!lib", + "!!**/lib", + "!!_tmp_*", + "!!**/_tmp_*", + "!!*.tmp", + "!!**/*.tmp", + "!!.tmp/", + "!!**/.tmp/", + "!!*.log", + "!!**/*.log", + "!!**/.DS_Store", + "!!**/Thumbs.db" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 120 + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "rules": { + "style": { + "useNodejsImportProtocol": "off" + }, + "suspicious": { + "noAssignInExpressions": "warn", + "noUselessEscapeInString": { + "level": "warn", + "fix": "none", + "options": {} + }, + "noThenProperty": "warn", + "useIterableCallbackReturn": "warn", + "noShadowRestrictedNames": "warn", + "noTsIgnore": { + "level": "warn", + "fix": "none", + "options": {} + }, + "noConfusingVoidType": { + "level": "warn", + "fix": "none", + "options": {} + } + } + } + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/package.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/package.json new file mode 100644 index 000000000000..ec7d3e8ee315 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/package.json @@ -0,0 +1,80 @@ +{ + "name": "@fern/ts-flatten-request-any-auth", + "version": "0.0.1", + "private": false, + "repository": { + "type": "git", + "url": "git+https://github.com/ts-flatten-request-any-auth/fern.git" + }, + "type": "commonjs", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + }, + "default": "./dist/cjs/index.js" + }, + "./users": { + "import": { + "types": "./dist/esm/api/resources/users/exports.d.mts", + "default": "./dist/esm/api/resources/users/exports.mjs" + }, + "require": { + "types": "./dist/cjs/api/resources/users/exports.d.ts", + "default": "./dist/cjs/api/resources/users/exports.js" + }, + "default": "./dist/cjs/api/resources/users/exports.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "reference.md", + "README.md", + "LICENSE" + ], + "scripts": { + "format": "biome format --write --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "format:check": "biome format --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "lint": "biome lint --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "lint:fix": "biome lint --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "check": "biome check --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "check:fix": "biome check --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "build": "pnpm build:cjs && pnpm build:esm", + "build:cjs": "tsc --project ./tsconfig.cjs.json", + "build:esm": "tsc --project ./tsconfig.esm.json && node scripts/rename-to-esm-files.js dist/esm", + "test": "vitest", + "test:unit": "vitest --project unit", + "test:wire": "vitest --project wire" + }, + "dependencies": {}, + "devDependencies": { + "webpack": "^5.105.4", + "ts-loader": "^9.5.4", + "vitest": "^4.1.1", + "msw": "2.11.2", + "@types/node": "^20.0.0", + "typescript": "~5.9.3", + "@biomejs/biome": "2.4.10" + }, + "browser": { + "fs": false, + "os": false, + "path": false, + "stream": false, + "crypto": false + }, + "packageManager": "pnpm@10.33.0", + "engines": { + "node": ">=18.0.0" + }, + "sideEffects": false +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/pnpm-workspace.yaml b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/pnpm-workspace.yaml new file mode 100644 index 000000000000..6e4c395107df --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/pnpm-workspace.yaml @@ -0,0 +1 @@ +packages: ['.'] \ No newline at end of file diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md new file mode 100644 index 000000000000..436da7e6fa20 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md @@ -0,0 +1,114 @@ +# Reference +## Users +
client.users.updateUser({ ...params }) -> SeedTsFlattenRequestAnyAuth.UpdateUser +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.users.updateUser({ + id: "body-id", + name: "Ada" +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `SeedTsFlattenRequestAnyAuth.UpdateUserRequest` + +
+
+ +
+
+ +**requestOptions:** `UsersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.users.updateUserProfile(id, { ...params }) -> SeedTsFlattenRequestAnyAuth.UpdateUser +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.users.updateUserProfile("path-id", { + id: "body-id", + name: "Ada" +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**request:** `SeedTsFlattenRequestAnyAuth.UpdateUser` + +
+
+ +
+
+ +**requestOptions:** `UsersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/scripts/rename-to-esm-files.js b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/scripts/rename-to-esm-files.js new file mode 100644 index 000000000000..0a03de13782f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/scripts/rename-to-esm-files.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +const fs = require("fs").promises; +const fsSync = require("fs"); +const path = require("path"); + +const extensionMap = { + ".js": ".mjs", + ".d.ts": ".d.mts", +}; +const oldExtensions = Object.keys(extensionMap); + +async function findFiles(rootPath) { + const files = []; + + async function scan(directory) { + const entries = await fs.readdir(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + if (entry.name !== "node_modules" && !entry.name.startsWith(".")) { + await scan(fullPath); + } + } else if (entry.isFile()) { + if (oldExtensions.some((ext) => entry.name.endsWith(ext))) { + files.push(fullPath); + } + } + } + } + + await scan(rootPath); + return files; +} + +async function updateFiles(files) { + const updatedFiles = []; + for (const file of files) { + const updated = await updateFileContents(file); + updatedFiles.push(updated); + } + + console.log(`Updated imports in ${updatedFiles.length} files.`); +} + +const KNOWN_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".jsx", ".json", ".ts", ".mts", ".cts", ".tsx", ".node"]); + +function hasFileExtension(importPath) { + const basename = path.basename(importPath); + const dotIndex = basename.lastIndexOf("."); + if (dotIndex <= 0) { + return false; + } + return KNOWN_EXTENSIONS.has(basename.slice(dotIndex).toLowerCase()); +} + +function resolveExtensionlessImport(dir, importPath) { + const resolvedPath = path.resolve(dir, importPath); + if (fsSync.existsSync(`${resolvedPath}.js`)) { + return `${importPath}.mjs`; + } + const indexPath = path.join(resolvedPath, "index.js"); + if (fsSync.existsSync(indexPath)) { + return `${importPath}/index.mjs`; + } + return null; +} + +async function updateFileContents(file) { + const content = await fs.readFile(file, "utf8"); + const dir = path.dirname(file); + + let newContent = content; + // Update each extension type defined in the map + for (const [oldExt, newExt] of Object.entries(extensionMap)) { + // Handle static imports/exports + const staticRegex = new RegExp(`(import|export)(.+from\\s+['"])(\\.\\.?\\/[^'"]+)(\\${oldExt})(['"])`, "g"); + newContent = newContent.replace(staticRegex, `$1$2$3${newExt}$5`); + + // Handle dynamic imports (yield import, await import, regular import()) + const dynamicRegex = new RegExp( + `(yield\\s+import|await\\s+import|import)\\s*\\(\\s*['"](\\.\\.\?\\/[^'"]+)(\\${oldExt})['"]\\s*\\)`, + "g", + ); + newContent = newContent.replace(dynamicRegex, `$1("$2${newExt}")`); + } + + // Handle extensionless relative imports (e.g. from "./oauth" or from "../utils"). + // These violate the ESM spec and break Node's ESM loader and Turbopack. + const staticExtensionless = /(import|export)(.+from\s+['"])(\.\.?\/[^'"]+?)(['"])/g; + const staticReplacements = []; + let match; + while ((match = staticExtensionless.exec(newContent)) !== null) { + const importPath = match[3]; + if (hasFileExtension(importPath)) continue; + const resolved = resolveExtensionlessImport(dir, importPath); + if (resolved != null) { + staticReplacements.push({ + start: match.index, + end: match.index + match[0].length, + replacement: `${match[1]}${match[2]}${resolved}${match[4]}`, + }); + } + } + for (const { start, end, replacement } of staticReplacements.reverse()) { + newContent = newContent.slice(0, start) + replacement + newContent.slice(end); + } + + // Handle extensionless dynamic imports + const dynamicExtensionless = /(yield\s+import|await\s+import|import)\s*\(\s*['"](\.\.?\/[^'"]+?)['"]\s*\)/g; + const dynamicReplacements = []; + while ((match = dynamicExtensionless.exec(newContent)) !== null) { + const importPath = match[2]; + if (hasFileExtension(importPath)) continue; + const resolved = resolveExtensionlessImport(dir, importPath); + if (resolved != null) { + dynamicReplacements.push({ + start: match.index, + end: match.index + match[0].length, + replacement: `${match[1]}("${resolved}")`, + }); + } + } + for (const { start, end, replacement } of dynamicReplacements.reverse()) { + newContent = newContent.slice(0, start) + replacement + newContent.slice(end); + } + + if (content !== newContent) { + await fs.writeFile(file, newContent, "utf8"); + return true; + } + return false; +} + +async function renameFiles(files) { + let counter = 0; + for (const file of files) { + const ext = oldExtensions.find((ext) => file.endsWith(ext)); + const newExt = extensionMap[ext]; + + if (newExt) { + const newPath = file.slice(0, -ext.length) + newExt; + await fs.rename(file, newPath); + counter++; + } + } + + console.log(`Renamed ${counter} files.`); +} + +async function main() { + try { + const targetDir = process.argv[2]; + if (!targetDir) { + console.error("Please provide a target directory"); + process.exit(1); + } + + const targetPath = path.resolve(targetDir); + const targetStats = await fs.stat(targetPath); + + if (!targetStats.isDirectory()) { + console.error("The provided path is not a directory"); + process.exit(1); + } + + console.log(`Scanning directory: ${targetDir}`); + + const files = await findFiles(targetDir); + + if (files.length === 0) { + console.log("No matching files found."); + process.exit(0); + } + + console.log(`Found ${files.length} files.`); + await updateFiles(files); + await renameFiles(files); + console.log("\nDone!"); + } catch (error) { + console.error("An error occurred:", error.message); + process.exit(1); + } +} + +main(); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json new file mode 100644 index 000000000000..f060dc83ec42 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json @@ -0,0 +1,27 @@ +{ + "endpoints": [ + { + "id": { + "path": "/users/{id}", + "method": "PUT", + "identifier_override": "endpoint_users.updateUser" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUser({\n id: \"body-id\",\n name: \"Ada\"\n});\n" + } + }, + { + "id": { + "path": "/users/{id}/profile", + "method": "PUT", + "identifier_override": "endpoint_users.updateUserProfile" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUserProfile(\"path-id\", {\n id: \"body-id\",\n name: \"Ada\"\n});\n" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/BaseClient.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/BaseClient.ts new file mode 100644 index 000000000000..5846bc819c78 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/BaseClient.ts @@ -0,0 +1,120 @@ +// This file was auto-generated by Fern from our API Definition. + +import { AnyAuthProvider } from "./auth/AnyAuthProvider.js"; +import { BearerAuthProvider } from "./auth/BearerAuthProvider.js"; +import { HeaderAuthProvider } from "./auth/HeaderAuthProvider.js"; +import { mergeHeaders } from "./core/headers.js"; +import * as core from "./core/index.js"; + +export type AuthOption = + | false + | core.AuthProvider["getAuthRequest"] + | core.AuthProvider + | AnyAuthProvider.AuthOptions<[BearerAuthProvider.AuthOptions, HeaderAuthProvider.AuthOptions]>; + +export type BaseClientOptions = { + environment: core.Supplier; + /** Specify a custom URL to connect the client to. */ + baseUrl?: core.Supplier; + /** Additional headers to include in requests. */ + headers?: Record | null | undefined>; + /** The default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** Provide a custom fetch implementation. Useful for platforms that don't have a built-in fetch or need a custom implementation. */ + fetch?: typeof fetch; + /** Configure logging for the client. */ + logging?: core.logging.LogConfig | core.logging.Logger; + /** Default options for SSE stream reconnection behavior. Has no effect on non-resumable endpoints. */ + stream?: { reconnectionEnabled?: boolean; maxReconnectionAttempts?: number }; + /** Override auth. Pass false to disable, a function returning auth headers, an AuthProvider, or auth options. */ + auth?: AuthOption; +} & AnyAuthProvider.AuthOptions<[BearerAuthProvider.AuthOptions, HeaderAuthProvider.AuthOptions]>; + +export interface BaseRequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Additional query string parameters to include in the request. */ + queryParams?: Record; + /** A dictionary containing additional parameters to spread into the request's body. */ + additionalBodyParameters?: Record; + /** Additional headers to include in the request. */ + headers?: Record | null | undefined>; + /** Options for SSE stream reconnection behavior. Has no effect on non-resumable endpoints. */ + stream?: { reconnectionEnabled?: boolean; maxReconnectionAttempts?: number }; +} + +export type NormalizedClientOptions = T & { + logging: core.logging.Logger; + authProvider?: core.AuthProvider; +}; + +export type NormalizedClientOptionsWithAuth = + NormalizedClientOptions & { + authProvider: core.AuthProvider; + }; + +export function normalizeClientOptions( + options: T, +): NormalizedClientOptions { + const headers = mergeHeaders( + { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fern/ts-flatten-request-any-auth", + "X-Fern-SDK-Version": "0.0.1", + "User-Agent": "@fern/ts-flatten-request-any-auth/0.0.1", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + options?.headers, + ); + + return { + ...options, + logging: core.logging.createLogger(options?.logging), + headers, + } as NormalizedClientOptions; +} + +export function normalizeClientOptionsWithAuth( + options: T, +): NormalizedClientOptionsWithAuth { + const normalized = normalizeClientOptions(options) as NormalizedClientOptionsWithAuth; + + if (options.auth === false) { + normalized.authProvider = new core.NoOpAuthProvider(); + return normalized; + } + if (options.auth != null) { + if (typeof options.auth === "function") { + normalized.authProvider = { getAuthRequest: options.auth }; + return normalized; + } + if (core.isAuthProvider(options.auth)) { + normalized.authProvider = options.auth; + return normalized; + } + Object.assign(normalized, options.auth); + } + + const normalizedWithNoOpAuthProvider = withNoOpAuthProvider(normalized); + normalized.authProvider ??= AnyAuthProvider.createInstance(normalizedWithNoOpAuthProvider, [ + BearerAuthProvider, + HeaderAuthProvider, + ]); + return normalized; +} + +function withNoOpAuthProvider( + options: NormalizedClientOptions, +): NormalizedClientOptionsWithAuth { + return { + ...options, + authProvider: new core.NoOpAuthProvider(), + }; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/Client.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/Client.ts new file mode 100644 index 000000000000..04f3e719463d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/Client.ts @@ -0,0 +1,56 @@ +// This file was auto-generated by Fern from our API Definition. + +import { UsersClient } from "./api/resources/users/client/Client.js"; +import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; + +export declare namespace SeedTsFlattenRequestAnyAuthClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class SeedTsFlattenRequestAnyAuthClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + protected _users: UsersClient | undefined; + + constructor(options: SeedTsFlattenRequestAnyAuthClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + public get users(): UsersClient { + return (this._users ??= new UsersClient(this._options)); + } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + baseUrl: this._options.baseUrl ?? this._options.environment, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/index.ts new file mode 100644 index 000000000000..e445af0d831e --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/index.ts @@ -0,0 +1 @@ +export * from "./resources/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/index.ts new file mode 100644 index 000000000000..eede1737b98f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/index.ts @@ -0,0 +1,3 @@ +export * from "./users/client/requests/index.js"; +export * as users from "./users/index.js"; +export * from "./users/types/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts new file mode 100644 index 000000000000..5d553d7fc7eb --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts @@ -0,0 +1,159 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import { mergeAdditionalBodyParameters } from "../../../../core/requestBody.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import type * as SeedTsFlattenRequestAnyAuth from "../../../index.js"; + +export declare namespace UsersClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class UsersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + + constructor(options: UsersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + /** + * @param {SeedTsFlattenRequestAnyAuth.UpdateUserRequest} request + * @param {UsersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SeedTsFlattenRequestAnyAuthError} + * @throws {@link errors.SeedTsFlattenRequestAnyAuthTimeoutError} + * + * @example + * await client.users.updateUser({ + * id: "body-id", + * name: "Ada" + * }) + */ + public updateUser( + request: SeedTsFlattenRequestAnyAuth.UpdateUserRequest, + requestOptions?: UsersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__updateUser(request, requestOptions)); + } + + private async __updateUser( + request: SeedTsFlattenRequestAnyAuth.UpdateUserRequest, + requestOptions?: UsersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `users/${core.url.encodePathParam(request.id)}`, + ), + method: "PUT", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters(request, requestOptions?.additionalBodyParameters), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as SeedTsFlattenRequestAnyAuth.UpdateUser, + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/users/{id}"); + } + + /** + * @param {string} id + * @param {SeedTsFlattenRequestAnyAuth.UpdateUser} request + * @param {UsersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SeedTsFlattenRequestAnyAuthError} + * @throws {@link errors.SeedTsFlattenRequestAnyAuthTimeoutError} + * + * @example + * await client.users.updateUserProfile("path-id", { + * id: "body-id", + * name: "Ada" + * }) + */ + public updateUserProfile( + id: string, + request: SeedTsFlattenRequestAnyAuth.UpdateUser, + requestOptions?: UsersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__updateUserProfile(id, request, requestOptions)); + } + + private async __updateUserProfile( + id: string, + request: SeedTsFlattenRequestAnyAuth.UpdateUser, + requestOptions?: UsersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `users/${core.url.encodePathParam(id)}/profile`, + ), + method: "PUT", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters(request, requestOptions?.additionalBodyParameters), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as SeedTsFlattenRequestAnyAuth.UpdateUser, + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/users/{id}/profile"); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/index.ts new file mode 100644 index 000000000000..195f9aa8a846 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts new file mode 100644 index 000000000000..9947ae2f2317 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * { + * id: "body-id", + * name: "Ada" + * } + */ +export interface UpdateUserRequest { + id: string; + name: string; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/index.ts new file mode 100644 index 000000000000..2292f5395065 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/index.ts @@ -0,0 +1 @@ +export type { UpdateUserRequest } from "./UpdateUserRequest.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/exports.ts new file mode 100644 index 000000000000..788add4edbfc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { UsersClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/index.ts new file mode 100644 index 000000000000..d9adb1af9a93 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/UpdateUser.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/UpdateUser.ts new file mode 100644 index 000000000000..c377e06cd091 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/UpdateUser.ts @@ -0,0 +1,6 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface UpdateUser { + id: string; + name: string; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/index.ts new file mode 100644 index 000000000000..d6915ffe9774 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/types/index.ts @@ -0,0 +1 @@ +export * from "./UpdateUser.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/AnyAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/AnyAuthProvider.ts new file mode 100644 index 000000000000..e97994fb2fe6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/AnyAuthProvider.ts @@ -0,0 +1,59 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { NormalizedClientOptions } from "../BaseClient.js"; +import type * as core from "../core/index.js"; + +export class AnyAuthProvider implements core.AuthProvider { + private readonly authProviders: core.AuthProvider[]; + + constructor(authProviders: core.AuthProvider[]) { + this.authProviders = authProviders; + } + + public async getAuthRequest(arg?: { endpointMetadata?: core.EndpointMetadata }): Promise { + const availableProviders = this.authProviders; + + for (const provider of availableProviders) { + try { + const authRequest = await provider.getAuthRequest(arg); + if (authRequest.headers.Authorization != null || Object.keys(authRequest.headers).length > 0) { + return authRequest; + } + } catch (_e) { + // Continue to next auth provider + } + } + + // No auth credentials found + throw new Error( + "No authentication credentials provided. Please provide one of the supported authentication methods.", + ); + } +} + +export namespace AnyAuthProvider { + type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + + type AtLeastOneOf = { + [K in keyof T]: T[K] & Partial>>; + }[number]; + + export type AuthOptions = AtLeastOneOf; + export type Options = Partial>; + + type InstantiatableAuthProvider = { + canCreate: (opts: NormalizedClientOptions) => boolean; + createInstance: (opts: NormalizedClientOptions) => core.AuthProvider; + }; + + export function createInstance( + options: NormalizedClientOptions, + authProviderClasses: InstantiatableAuthProvider[], + ): core.AuthProvider { + const authProviders: core.AuthProvider[] = authProviderClasses + .filter((providerClass) => providerClass.canCreate(options)) + .map((providerClass) => providerClass.createInstance(options)); + + return new AnyAuthProvider(authProviders); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/BearerAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/BearerAuthProvider.ts new file mode 100644 index 000000000000..2aa01c47ca50 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/BearerAuthProvider.ts @@ -0,0 +1,52 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as core from "../core/index.js"; +import * as errors from "../errors/index.js"; + +const WRAPPER_PROPERTY = "bearerAuth" as const; +const TOKEN_PARAM = "token" as const; +const ENV_TOKEN = "MY_TOKEN" as const; + +export class BearerAuthProvider implements core.AuthProvider { + private readonly options: BearerAuthProvider.Options; + + constructor(options: BearerAuthProvider.Options) { + this.options = options; + } + + public static canCreate(options: Partial): boolean { + return options?.[WRAPPER_PROPERTY]?.[TOKEN_PARAM] != null || process.env?.[ENV_TOKEN] != null; + } + + public async getAuthRequest({ + endpointMetadata, + }: { + endpointMetadata?: core.EndpointMetadata; + } = {}): Promise { + const token = + (await core.Supplier.get(this.options[WRAPPER_PROPERTY]?.[TOKEN_PARAM])) ?? process.env?.[ENV_TOKEN]; + if (token == null) { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: BearerAuthProvider.AUTH_CONFIG_ERROR_MESSAGE, + }); + } + + return { + headers: { Authorization: `Bearer ${token}` }, + }; + } +} + +export namespace BearerAuthProvider { + export const AUTH_SCHEME = "BearerAuth" as const; + export const AUTH_CONFIG_ERROR_MESSAGE: string = + `Please provide '${TOKEN_PARAM}' when initializing the client, or set the '${ENV_TOKEN}' environment variable` as const; + export type Options = AuthOptions; + export type AuthOptions = { + [WRAPPER_PROPERTY]?: { [TOKEN_PARAM]?: core.Supplier | undefined }; + }; + + export function createInstance(options: Options): core.AuthProvider { + return new BearerAuthProvider(options); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/HeaderAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/HeaderAuthProvider.ts new file mode 100644 index 000000000000..74e84b14a6c0 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/HeaderAuthProvider.ts @@ -0,0 +1,53 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as core from "../core/index.js"; +import * as errors from "../errors/index.js"; + +const WRAPPER_PROPERTY = "apiKey" as const; +const PARAM_KEY = "apiKey" as const; +const ENV_HEADER_KEY = "MY_API_KEY" as const; +const HEADER_NAME = "X-API-Key" as const; + +export class HeaderAuthProvider implements core.AuthProvider { + private readonly options: HeaderAuthProvider.Options; + + constructor(options: HeaderAuthProvider.Options) { + this.options = options; + } + + public static canCreate(options: Partial): boolean { + return options?.[WRAPPER_PROPERTY]?.[PARAM_KEY] != null || process.env?.[ENV_HEADER_KEY] != null; + } + + public async getAuthRequest({ + endpointMetadata, + }: { + endpointMetadata?: core.EndpointMetadata; + } = {}): Promise { + const headerValue = + (await core.Supplier.get(this.options[WRAPPER_PROPERTY]?.[PARAM_KEY])) ?? process.env?.[ENV_HEADER_KEY]; + if (headerValue == null) { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: HeaderAuthProvider.AUTH_CONFIG_ERROR_MESSAGE, + }); + } + + return { + headers: { [HEADER_NAME]: headerValue }, + }; + } +} + +export namespace HeaderAuthProvider { + export const AUTH_SCHEME = "ApiKey" as const; + export const AUTH_CONFIG_ERROR_MESSAGE: string = + `Please provide '${PARAM_KEY}' when initializing the client, or set the '${ENV_HEADER_KEY}' environment variable` as const; + export type Options = AuthOptions; + export type AuthOptions = { + [WRAPPER_PROPERTY]?: { [PARAM_KEY]?: core.Supplier | undefined }; + }; + + export function createInstance(options: Options): core.AuthProvider { + return new HeaderAuthProvider(options); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/index.ts new file mode 100644 index 000000000000..cfdbd6f9a9e5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/auth/index.ts @@ -0,0 +1,3 @@ +export { AnyAuthProvider } from "./AnyAuthProvider.js"; +export { BearerAuthProvider } from "./BearerAuthProvider.js"; +export { HeaderAuthProvider } from "./HeaderAuthProvider.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthProvider.ts new file mode 100644 index 000000000000..c9478669fb89 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthProvider.ts @@ -0,0 +1,15 @@ +import type { EndpointMetadata } from "../fetcher/EndpointMetadata.js"; +import type { AuthRequest } from "./AuthRequest.js"; + +export interface AuthProvider { + getAuthRequest(arg?: { endpointMetadata?: EndpointMetadata }): Promise; +} + +export function isAuthProvider(value: unknown): value is AuthProvider { + return ( + typeof value === "object" && + value !== null && + "getAuthRequest" in value && + typeof value.getAuthRequest === "function" + ); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthRequest.ts new file mode 100644 index 000000000000..f6218b42211e --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/AuthRequest.ts @@ -0,0 +1,9 @@ +/** + * Request parameters for authentication requests. + */ +export interface AuthRequest { + /** + * The headers to be included in the request. + */ + headers: Record; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BasicAuth.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BasicAuth.ts new file mode 100644 index 000000000000..f34fca5cc4dd --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BasicAuth.ts @@ -0,0 +1,37 @@ +import { base64Decode, base64Encode } from "../base64.js"; + +export interface BasicAuth { + username?: string; + password?: string; +} + +const BASIC_AUTH_HEADER_PREFIX = /^Basic /i; + +export const BasicAuth = { + toAuthorizationHeader: (basicAuth: BasicAuth | undefined): string | undefined => { + if (basicAuth == null) { + return undefined; + } + const username = basicAuth.username ?? ""; + const password = basicAuth.password ?? ""; + if (username === "" && password === "") { + return undefined; + } + const token = base64Encode(`${username}:${password}`); + return `Basic ${token}`; + }, + fromAuthorizationHeader: (header: string): BasicAuth => { + const credentials = header.replace(BASIC_AUTH_HEADER_PREFIX, ""); + const decoded = base64Decode(credentials); + const [username, ...passwordParts] = decoded.split(":"); + const password = passwordParts.length > 0 ? passwordParts.join(":") : undefined; + + if (username == null || password == null) { + throw new Error("Invalid basic auth"); + } + return { + username, + password, + }; + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BearerToken.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BearerToken.ts new file mode 100644 index 000000000000..c44a06c38f06 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/BearerToken.ts @@ -0,0 +1,20 @@ +export type BearerToken = string; + +const BEARER_AUTH_HEADER_PREFIX = /^Bearer /i; + +function toAuthorizationHeader(token: string | undefined): string | undefined { + if (token == null) { + return undefined; + } + return `Bearer ${token}`; +} + +export const BearerToken: { + toAuthorizationHeader: typeof toAuthorizationHeader; + fromAuthorizationHeader: (header: string) => BearerToken; +} = { + toAuthorizationHeader: toAuthorizationHeader, + fromAuthorizationHeader: (header: string): BearerToken => { + return header.replace(BEARER_AUTH_HEADER_PREFIX, "").trim() as BearerToken; + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/NoOpAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/NoOpAuthProvider.ts new file mode 100644 index 000000000000..5b7acfd2bd8b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/NoOpAuthProvider.ts @@ -0,0 +1,8 @@ +import type { AuthProvider } from "./AuthProvider.js"; +import type { AuthRequest } from "./AuthRequest.js"; + +export class NoOpAuthProvider implements AuthProvider { + public getAuthRequest(): Promise { + return Promise.resolve({ headers: {} }); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/index.ts new file mode 100644 index 000000000000..77effd090dce --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/auth/index.ts @@ -0,0 +1,5 @@ +export { type AuthProvider, isAuthProvider } from "./AuthProvider.js"; +export type { AuthRequest } from "./AuthRequest.js"; +export { BasicAuth } from "./BasicAuth.js"; +export { BearerToken } from "./BearerToken.js"; +export { NoOpAuthProvider } from "./NoOpAuthProvider.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/base64.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/base64.ts new file mode 100644 index 000000000000..448a0db638a6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/base64.ts @@ -0,0 +1,27 @@ +function base64ToBytes(base64: string): Uint8Array { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)!); +} + +function bytesToBase64(bytes: Uint8Array): string { + const binString = String.fromCodePoint(...bytes); + return btoa(binString); +} + +export function base64Encode(input: string): string { + if (typeof Buffer !== "undefined") { + return Buffer.from(input, "utf8").toString("base64"); + } + + const bytes = new TextEncoder().encode(input); + return bytesToBase64(bytes); +} + +export function base64Decode(input: string): string { + if (typeof Buffer !== "undefined") { + return Buffer.from(input, "base64").toString("utf8"); + } + + const bytes = base64ToBytes(input); + return new TextDecoder().decode(bytes); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/exports.ts new file mode 100644 index 000000000000..69296d7100d6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/exports.ts @@ -0,0 +1 @@ +export * from "./logging/exports.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/APIResponse.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/APIResponse.ts new file mode 100644 index 000000000000..97ab83c2b195 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/APIResponse.ts @@ -0,0 +1,23 @@ +import type { RawResponse } from "./RawResponse.js"; + +/** + * The response of an API call. + * It is a successful response or a failed response. + */ +export type APIResponse = SuccessfulResponse | FailedResponse; + +export interface SuccessfulResponse { + ok: true; + body: T; + /** + * @deprecated Use `rawResponse` instead + */ + headers?: Record; + rawResponse: RawResponse; +} + +export interface FailedResponse { + ok: false; + error: T; + rawResponse: RawResponse; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/BinaryResponse.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/BinaryResponse.ts new file mode 100644 index 000000000000..b9e40fb62cc4 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/BinaryResponse.ts @@ -0,0 +1,34 @@ +export type BinaryResponse = { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + bodyUsed: Response["bodyUsed"]; + /** + * Returns a ReadableStream of the response body. + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) + */ + stream: () => Response["body"]; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer: () => ReturnType; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob: () => ReturnType; + /** + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) + * Some versions of the Fetch API may not support this method. + */ + bytes?(): Promise; +}; + +export function getBinaryResponse(response: Response): BinaryResponse { + const binaryResponse: BinaryResponse = { + get bodyUsed() { + return response.bodyUsed; + }, + stream: () => response.body, + arrayBuffer: response.arrayBuffer.bind(response), + blob: response.blob.bind(response), + }; + if ("bytes" in response && typeof response.bytes === "function") { + binaryResponse.bytes = response.bytes.bind(response); + } + + return binaryResponse; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointMetadata.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointMetadata.ts new file mode 100644 index 000000000000..998d68f5c20c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointMetadata.ts @@ -0,0 +1,13 @@ +export type SecuritySchemeKey = string; +/** + * A collection of security schemes, where the key is the name of the security scheme and the value is the list of scopes required for that scheme. + * All schemes in the collection must be satisfied for authentication to be successful. + */ +export type SecuritySchemeCollection = Record; +export type AuthScope = string; +export type EndpointMetadata = { + /** + * An array of security scheme collections. Each collection represents an alternative way to authenticate. + */ + security?: SecuritySchemeCollection[]; +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointSupplier.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointSupplier.ts new file mode 100644 index 000000000000..aad81f0d9040 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/EndpointSupplier.ts @@ -0,0 +1,14 @@ +import type { EndpointMetadata } from "./EndpointMetadata.js"; +import type { Supplier } from "./Supplier.js"; + +type EndpointSupplierFn = (arg: { endpointMetadata?: EndpointMetadata }) => T | Promise; +export type EndpointSupplier = Supplier | EndpointSupplierFn; +export const EndpointSupplier = { + get: async (supplier: EndpointSupplier, arg: { endpointMetadata?: EndpointMetadata }): Promise => { + if (typeof supplier === "function") { + return (supplier as EndpointSupplierFn)(arg); + } else { + return supplier; + } + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Fetcher.ts new file mode 100644 index 000000000000..cd5c5793d670 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Fetcher.ts @@ -0,0 +1,311 @@ +import { toJson } from "../json.js"; +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import type { APIResponse } from "./APIResponse.js"; +import { createRequestUrl } from "./createRequestUrl.js"; +import type { EndpointMetadata } from "./EndpointMetadata.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getErrorResponseBody } from "./getErrorResponseBody.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { getRequestBody } from "./getRequestBody.js"; +import { getResponseBody } from "./getResponseBody.js"; +import { Headers } from "./Headers.js"; +import { makeRequest } from "./makeRequest.js"; +import { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; +import { redactUrl, SENSITIVE_QUERY_PARAMS } from "./redactUrl.js"; +import { requestWithRetries } from "./requestWithRetries.js"; + +export type FetchFunction = (args: Fetcher.Args) => Promise>; + +export declare namespace Fetcher { + export interface Args { + url: string; + method: string; + contentType?: string; + headers?: Record; + /** + * @deprecated Prefer `queryString` (produced by `core.url.queryBuilder()`). + * Retained for backwards compatibility with custom fetchers and callers that + * still construct request args with a query-parameter object. + */ + queryParameters?: Record; + queryString?: string; + body?: unknown; + timeoutMs?: number; + maxRetries?: number; + withCredentials?: boolean; + abortSignal?: AbortSignal; + requestType?: "json" | "file" | "bytes" | "form" | "other"; + responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response"; + duplex?: "half"; + endpointMetadata?: EndpointMetadata; + fetchFn?: typeof fetch; + logging?: LogConfig | Logger; + } + + export type Error = FailedStatusCodeError | NonJsonError | BodyIsNullError | TimeoutError | UnknownError; + + export interface FailedStatusCodeError { + reason: "status-code"; + statusCode: number; + body: unknown; + } + + export interface NonJsonError { + reason: "non-json"; + statusCode: number; + rawBody: string; + } + + export interface BodyIsNullError { + reason: "body-is-null"; + statusCode: number; + } + + export interface TimeoutError { + reason: "timeout"; + cause?: unknown; + } + + export interface UnknownError { + reason: "unknown"; + errorMessage: string; + cause?: unknown; + } +} + +const SENSITIVE_HEADERS = new Set([ + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", +]); + +function redactHeaders(headers: Headers | Record): Record { + const filtered: Record = {}; + for (const [key, value] of headers instanceof Headers ? headers.entries() : Object.entries(headers)) { + if (SENSITIVE_HEADERS.has(key.toLowerCase())) { + filtered[key] = "[REDACTED]"; + } else { + filtered[key] = value; + } + } + return filtered; +} + +function redactQueryParameters( + queryParameters: Record | undefined, +): Record | undefined { + if (queryParameters == null) { + return undefined; + } + const redacted: Record = {}; + for (const [key, value] of Object.entries(queryParameters)) { + redacted[key] = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()) ? "[REDACTED]" : value; + } + return redacted; +} + +async function getHeaders(args: Fetcher.Args): Promise { + const newHeaders: Headers = new Headers(); + + newHeaders.set( + "Accept", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", + ); + if (args.body !== undefined && args.contentType != null) { + newHeaders.set("Content-Type", args.contentType); + } + + if (args.headers == null) { + return newHeaders; + } + + for (const [key, value] of Object.entries(args.headers)) { + const result = await EndpointSupplier.get(value, { endpointMetadata: args.endpointMetadata ?? {} }); + if (typeof result === "string") { + newHeaders.set(key, result); + continue; + } + if (result == null) { + continue; + } + newHeaders.set(key, `${result}`); + } + return newHeaders; +} + +export async function fetcherImpl(args: Fetcher.Args): Promise> { + let url = args.url; + if (args.queryString != null && args.queryString.length > 0) { + url = `${url}?${args.queryString}`; + } else { + url = createRequestUrl(args.url, args.queryParameters); + } + const requestBody: BodyInit | undefined = await getRequestBody({ + body: args.body, + type: args.requestType ?? "other", + }); + const fetchFn = args.fetchFn ?? (await getFetchFn()); + const headers = await getHeaders(args); + const logger = createLogger(args.logging); + + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + headers: redactHeaders(headers), + queryParameters: redactQueryParameters(args.queryParameters), + hasBody: requestBody != null, + }; + logger.debug("Making HTTP request", metadata); + } + + try { + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + url, + args.method, + headers, + requestBody, + args.timeoutMs, + args.abortSignal, + args.withCredentials, + args.duplex, + args.responseType === "streaming" || args.responseType === "sse", + ), + args.maxRetries, + ); + + if (response.status >= 200 && response.status < 400) { + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(response.headers), + }; + logger.debug("HTTP request succeeded", metadata); + } + const body = await getResponseBody(response, args.responseType); + return { + ok: true, + body: body as R, + headers: response.headers, + rawResponse: toRawResponse(response), + }; + } else { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())), + }; + logger.error("HTTP request failed with error status", metadata); + } + return { + ok: false, + error: { + reason: "status-code", + statusCode: response.status, + body: await getErrorResponseBody(response), + }, + rawResponse: toRawResponse(response), + }; + } + } catch (error) { + if (args.abortSignal?.aborted) { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + }; + logger.error("HTTP request was aborted", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: "The user aborted a request", + cause: error, + }, + rawResponse: abortRawResponse, + }; + } else if (error instanceof Error && error.name === "AbortError") { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + timeoutMs: args.timeoutMs, + }; + logger.error("HTTP request timed out", metadata); + } + return { + ok: false, + error: { + reason: "timeout", + cause: error, + }, + rawResponse: abortRawResponse, + }; + } else if (error instanceof Error) { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + errorMessage: error.message, + }; + logger.error("HTTP request failed with error", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: error.message, + cause: error, + }, + rawResponse: unknownRawResponse, + }; + } + + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + error: toJson(error), + }; + logger.error("HTTP request failed with unknown error", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: toJson(error), + cause: error, + }, + rawResponse: unknownRawResponse, + }; + } +} + +export const fetcher: FetchFunction = fetcherImpl; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Headers.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Headers.ts new file mode 100644 index 000000000000..f02246c50757 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Headers.ts @@ -0,0 +1,93 @@ +let Headers: typeof globalThis.Headers; + +if (typeof globalThis.Headers !== "undefined") { + Headers = globalThis.Headers; +} else { + Headers = class Headers implements Headers { + private headers: Map; + + constructor(init?: HeadersInit) { + this.headers = new Map(); + + if (init) { + if (init instanceof Headers) { + init.forEach((value, key) => this.append(key, value)); + } else if (Array.isArray(init)) { + for (const [key, value] of init) { + if (typeof key === "string" && typeof value === "string") { + this.append(key, value); + } else { + throw new TypeError("Each header entry must be a [string, string] tuple"); + } + } + } else { + for (const [key, value] of Object.entries(init)) { + if (typeof value === "string") { + this.append(key, value); + } else { + throw new TypeError("Header values must be strings"); + } + } + } + } + } + + append(name: string, value: string): void { + const key = name.toLowerCase(); + const existing = this.headers.get(key) || []; + this.headers.set(key, [...existing, value]); + } + + delete(name: string): void { + const key = name.toLowerCase(); + this.headers.delete(key); + } + + get(name: string): string | null { + const key = name.toLowerCase(); + const values = this.headers.get(key); + return values ? values.join(", ") : null; + } + + has(name: string): boolean { + const key = name.toLowerCase(); + return this.headers.has(key); + } + + set(name: string, value: string): void { + const key = name.toLowerCase(); + this.headers.set(key, [value]); + } + + forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void { + const boundCallback = thisArg ? callbackfn.bind(thisArg) : callbackfn; + this.headers.forEach((values, key) => boundCallback(values.join(", "), key, this)); + } + + getSetCookie(): string[] { + return this.headers.get("set-cookie") || []; + } + + *entries(): IterableIterator<[string, string]> { + for (const [key, values] of this.headers.entries()) { + yield [key, values.join(", ")]; + } + } + + *keys(): IterableIterator { + yield* this.headers.keys(); + } + + *values(): IterableIterator { + for (const values of this.headers.values()) { + yield values.join(", "); + } + } + + [Symbol.iterator](): IterableIterator<[string, string]> { + return this.entries(); + } + }; +} + +export { Headers }; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/HttpResponsePromise.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/HttpResponsePromise.ts new file mode 100644 index 000000000000..692ca7d795f0 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/HttpResponsePromise.ts @@ -0,0 +1,116 @@ +import type { WithRawResponse } from "./RawResponse.js"; + +/** + * A promise that returns the parsed response and lets you retrieve the raw response too. + */ +export class HttpResponsePromise extends Promise { + private innerPromise: Promise>; + private unwrappedPromise: Promise | undefined; + + private constructor(promise: Promise>) { + // Initialize with a no-op to avoid premature parsing + super((resolve) => { + resolve(undefined as unknown as T); + }); + this.innerPromise = promise; + } + + /** + * Creates an `HttpResponsePromise` from a function that returns a promise. + * + * @param fn - A function that returns a promise resolving to a `WithRawResponse` object. + * @param args - Arguments to pass to the function. + * @returns An `HttpResponsePromise` instance. + */ + public static fromFunction Promise>, T>( + fn: F, + ...args: Parameters + ): HttpResponsePromise { + return new HttpResponsePromise(fn(...args)); + } + + /** + * Creates a function that returns an `HttpResponsePromise` from a function that returns a promise. + * + * @param fn - A function that returns a promise resolving to a `WithRawResponse` object. + * @returns A function that returns an `HttpResponsePromise` instance. + */ + public static interceptFunction< + F extends (...args: never[]) => Promise>, + T = Awaited>["data"], + >(fn: F): (...args: Parameters) => HttpResponsePromise { + return (...args: Parameters): HttpResponsePromise => { + return HttpResponsePromise.fromPromise(fn(...args)); + }; + } + + /** + * Creates an `HttpResponsePromise` from an existing promise. + * + * @param promise - A promise resolving to a `WithRawResponse` object. + * @returns An `HttpResponsePromise` instance. + */ + public static fromPromise(promise: Promise>): HttpResponsePromise { + return new HttpResponsePromise(promise); + } + + /** + * Creates an `HttpResponsePromise` from an executor function. + * + * @param executor - A function that takes resolve and reject callbacks to create a promise. + * @returns An `HttpResponsePromise` instance. + */ + public static fromExecutor( + executor: (resolve: (value: WithRawResponse) => void, reject: (reason?: unknown) => void) => void, + ): HttpResponsePromise { + const promise = new Promise>(executor); + return new HttpResponsePromise(promise); + } + + /** + * Creates an `HttpResponsePromise` from a resolved result. + * + * @param result - A `WithRawResponse` object to resolve immediately. + * @returns An `HttpResponsePromise` instance. + */ + public static fromResult(result: WithRawResponse): HttpResponsePromise { + const promise = Promise.resolve(result); + return new HttpResponsePromise(promise); + } + + private unwrap(): Promise { + if (!this.unwrappedPromise) { + this.unwrappedPromise = this.innerPromise.then(({ data }) => data); + } + return this.unwrappedPromise; + } + + /** @inheritdoc */ + public override then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ): Promise { + return this.unwrap().then(onfulfilled, onrejected); + } + + /** @inheritdoc */ + public override catch( + onrejected?: ((reason: unknown) => TResult | PromiseLike) | null, + ): Promise { + return this.unwrap().catch(onrejected); + } + + /** @inheritdoc */ + public override finally(onfinally?: (() => void) | null): Promise { + return this.unwrap().finally(onfinally); + } + + /** + * Retrieves the data and raw response. + * + * @returns A promise resolving to a `WithRawResponse` object. + */ + public async withRawResponse(): Promise> { + return await this.innerPromise; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/RawResponse.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/RawResponse.ts new file mode 100644 index 000000000000..37fb44e2aa99 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/RawResponse.ts @@ -0,0 +1,61 @@ +import { Headers } from "./Headers.js"; + +/** + * The raw response from the fetch call excluding the body. + */ +export type RawResponse = Omit< + { + [K in keyof Response as Response[K] extends Function ? never : K]: Response[K]; // strips out functions + }, + "ok" | "body" | "bodyUsed" +>; // strips out body and bodyUsed + +/** + * A raw response indicating that the request was aborted. + */ +export const abortRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 499, + statusText: "Client Closed Request", + type: "error", + url: "", +} as const; + +/** + * A raw response indicating an unknown error. + */ +export const unknownRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 0, + statusText: "Unknown Error", + type: "error", + url: "", +} as const; + +/** + * Converts a `RawResponse` object into a `RawResponse` by extracting its properties, + * excluding the `body` and `bodyUsed` fields. + * + * @param response - The `RawResponse` object to convert. + * @returns A `RawResponse` object containing the extracted properties of the input response. + */ +export function toRawResponse(response: Response): RawResponse { + return { + headers: response.headers, + redirected: response.redirected, + status: response.status, + statusText: response.statusText, + type: response.type, + url: response.url, + }; +} + +/** + * Creates a `RawResponse` from a standard `Response` object. + */ +export interface WithRawResponse { + readonly data: T; + readonly rawResponse: RawResponse; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Supplier.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Supplier.ts new file mode 100644 index 000000000000..867c931c02f4 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/Supplier.ts @@ -0,0 +1,11 @@ +export type Supplier = T | Promise | (() => T | Promise); + +export const Supplier = { + get: async (supplier: Supplier): Promise => { + if (typeof supplier === "function") { + return (supplier as () => T)(); + } else { + return supplier; + } + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/createRequestUrl.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/createRequestUrl.ts new file mode 100644 index 000000000000..88e13265e112 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/createRequestUrl.ts @@ -0,0 +1,6 @@ +import { toQueryString } from "../url/qs.js"; + +export function createRequestUrl(baseUrl: string, queryParameters?: Record): string { + const queryString = toQueryString(queryParameters, { arrayFormat: "repeat" }); + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getErrorResponseBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getErrorResponseBody.ts new file mode 100644 index 000000000000..7cf4e623c2f5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getErrorResponseBody.ts @@ -0,0 +1,33 @@ +import { fromJson } from "../json.js"; +import { getResponseBody } from "./getResponseBody.js"; + +export async function getErrorResponseBody(response: Response): Promise { + let contentType = response.headers.get("Content-Type")?.toLowerCase(); + if (contentType == null || contentType.length === 0) { + return getResponseBody(response); + } + + if (contentType.indexOf(";") !== -1) { + contentType = contentType.split(";")[0]?.trim() ?? ""; + } + switch (contentType) { + case "application/hal+json": + case "application/json": + case "application/ld+json": + case "application/problem+json": + case "application/vnd.api+json": + case "text/json": { + const text = await response.text(); + return text.length > 0 ? fromJson(text) : undefined; + } + default: + if (contentType.startsWith("application/vnd.") && contentType.endsWith("+json")) { + const text = await response.text(); + return text.length > 0 ? fromJson(text) : undefined; + } + + // Fallback to plain text if content type is not recognized + // Even if no body is present, the response will be an empty string + return await response.text(); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getFetchFn.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getFetchFn.ts new file mode 100644 index 000000000000..9f845b956392 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getFetchFn.ts @@ -0,0 +1,3 @@ +export async function getFetchFn(): Promise { + return fetch; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getHeader.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getHeader.ts new file mode 100644 index 000000000000..50f922b0e87f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getHeader.ts @@ -0,0 +1,8 @@ +export function getHeader(headers: Record, header: string): string | undefined { + for (const [headerKey, headerValue] of Object.entries(headers)) { + if (headerKey.toLowerCase() === header.toLowerCase()) { + return headerValue; + } + } + return undefined; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getRequestBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getRequestBody.ts new file mode 100644 index 000000000000..91d9d81f50e5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getRequestBody.ts @@ -0,0 +1,20 @@ +import { toJson } from "../json.js"; +import { toQueryString } from "../url/qs.js"; + +export declare namespace GetRequestBody { + interface Args { + body: unknown; + type: "json" | "file" | "bytes" | "form" | "other"; + } +} + +export async function getRequestBody({ body, type }: GetRequestBody.Args): Promise { + if (type === "form") { + return toQueryString(body, { arrayFormat: "repeat", encode: true }); + } + if (type.includes("json")) { + return toJson(body); + } else { + return body as BodyInit; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getResponseBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getResponseBody.ts new file mode 100644 index 000000000000..2e831e4956e8 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/getResponseBody.ts @@ -0,0 +1,70 @@ +import { fromJson } from "../json.js"; +import { getBinaryResponse } from "./BinaryResponse.js"; + +// Pins the upstream Response so undici's FinalizationRegistry can't GC it and cancel the body stream. +function retainResponse(target: object, response: Response): void { + Object.defineProperty(target, "__fern_response_ref", { + value: response, + enumerable: false, + configurable: true, + writable: false, + }); +} + +export async function getResponseBody(response: Response, responseType?: string): Promise { + switch (responseType) { + case "binary-response": + return getBinaryResponse(response); + case "blob": + return await response.blob(); + case "arrayBuffer": + return await response.arrayBuffer(); + case "sse": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + retainResponse(response.body, response); + return response.body; + case "streaming": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + + retainResponse(response.body, response); + return response.body; + + case "text": + return await response.text(); + } + + // if responseType is "json" or not specified, try to parse as JSON + const text = await response.text(); + if (text.length > 0) { + try { + const responseBody = fromJson(text); + return responseBody; + } catch (_err) { + return { + ok: false, + error: { + reason: "non-json", + statusCode: response.status, + rawBody: text, + }, + }; + } + } + return undefined; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/index.ts new file mode 100644 index 000000000000..bd5db362c778 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/index.ts @@ -0,0 +1,13 @@ +export type { APIResponse } from "./APIResponse.js"; +export type { BinaryResponse } from "./BinaryResponse.js"; +export type { EndpointMetadata } from "./EndpointMetadata.js"; +export { EndpointSupplier } from "./EndpointSupplier.js"; +export type { Fetcher, FetchFunction } from "./Fetcher.js"; +export { fetcher } from "./Fetcher.js"; +export { getHeader } from "./getHeader.js"; +export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; +export type { RawResponse, WithRawResponse } from "./RawResponse.js"; +export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; +export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..e8dceda9383d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,211 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { redactUrl } from "./redactUrl.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers, but only when the resolved URL targets the configured base URL. + // This prevents the SDK's credentials from leaking to an unrelated host when a caller + // passes an absolute cross-origin URL into the passthrough fetch escape hatch. + if (clientOptions.getAuthHeaders != null && targetsBaseUrl(fullUrl, baseUrl)) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: redactUrl(fullUrl), + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: redactUrl(fullUrl), + statusCode: response.status, + }); + } + + return response; +} + +/** + * Returns true when the resolved request URL points at the same origin as the + * configured base URL. Relative paths are always joined onto the base URL, so + * they resolve to the base origin and return true. Absolute URLs only match when + * their origin equals the base origin. When there is no base URL to compare + * against, or either value is not a parseable absolute URL, this returns false so + * auth headers are not attached. + */ +function targetsBaseUrl(fullUrl: string, baseUrl: string | undefined): boolean { + if (baseUrl == null) { + return false; + } + try { + return new URL(fullUrl).origin === new URL(baseUrl).origin; + } catch { + return false; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makeRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makeRequest.ts new file mode 100644 index 000000000000..360a86df40ad --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/makeRequest.ts @@ -0,0 +1,70 @@ +import { anySignal, getTimeoutSignal } from "./signals.js"; + +/** + * Cached result of checking whether the current runtime supports + * the `cache` option in `Request`. Some runtimes (e.g. Cloudflare Workers) + * throw a TypeError when this option is used. + */ +let _cacheNoStoreSupported: boolean | undefined; +export function isCacheNoStoreSupported(): boolean { + if (_cacheNoStoreSupported != null) { + return _cacheNoStoreSupported; + } + try { + new Request("http://localhost", { cache: "no-store" }); + _cacheNoStoreSupported = true; + } catch { + _cacheNoStoreSupported = false; + } + return _cacheNoStoreSupported; +} + +/** + * Reset the cached result of `isCacheNoStoreSupported`. Exposed for testing only. + */ +export function resetCacheNoStoreSupported(): void { + _cacheNoStoreSupported = undefined; +} + +export const makeRequest = async ( + fetchFn: (url: string, init: RequestInit) => Promise, + url: string, + method: string, + headers: Headers | Record, + requestBody: BodyInit | undefined, + timeoutMs?: number, + abortSignal?: AbortSignal, + withCredentials?: boolean, + duplex?: "half", + disableCache?: boolean, +): Promise => { + const signals: AbortSignal[] = []; + + let timeoutAbortId: ReturnType | undefined; + if (timeoutMs != null) { + const { signal, abortId } = getTimeoutSignal(timeoutMs); + timeoutAbortId = abortId; + signals.push(signal); + } + + if (abortSignal != null) { + signals.push(abortSignal); + } + const newSignals = anySignal(signals); + const response = await fetchFn(url, { + method: method, + headers, + body: requestBody, + signal: newSignals, + credentials: withCredentials ? "include" : undefined, + // @ts-ignore + duplex, + ...(disableCache && isCacheNoStoreSupported() ? { cache: "no-store" as RequestCache } : {}), + }); + + if (timeoutAbortId != null) { + clearTimeout(timeoutAbortId); + } + + return response; +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/redactUrl.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/redactUrl.ts new file mode 100644 index 000000000000..3c2e897a619c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/redactUrl.ts @@ -0,0 +1,102 @@ +export const SENSITIVE_QUERY_PARAMS: Set = new Set([ + "api_key", + "api-key", + "apikey", + "token", + "access_token", + "access-token", + "auth_token", + "auth-token", + "password", + "passwd", + "secret", + "api_secret", + "api-secret", + "apisecret", + "key", + "session", + "session_id", + "session-id", +]); + +export function redactUrl(url: string): string { + const protocolIndex = url.indexOf("://"); + if (protocolIndex === -1) return url; + + const afterProtocol = protocolIndex + 3; + + // Find the first delimiter that marks the end of the authority section + const pathStart = url.indexOf("/", afterProtocol); + let queryStart = url.indexOf("?", afterProtocol); + let fragmentStart = url.indexOf("#", afterProtocol); + + const firstDelimiter = Math.min( + pathStart === -1 ? url.length : pathStart, + queryStart === -1 ? url.length : queryStart, + fragmentStart === -1 ? url.length : fragmentStart, + ); + + // Find the LAST @ before the delimiter (handles multiple @ in credentials) + let atIndex = -1; + for (let i = afterProtocol; i < firstDelimiter; i++) { + if (url[i] === "@") { + atIndex = i; + } + } + + if (atIndex !== -1) { + url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`; + } + + // Recalculate queryStart since url might have changed + queryStart = url.indexOf("?"); + if (queryStart === -1) return url; + + fragmentStart = url.indexOf("#", queryStart); + const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length; + const queryString = url.slice(queryStart + 1, queryEnd); + + if (queryString.length === 0) return url; + + // FAST PATH: Quick check if any sensitive keywords present + // Using indexOf is faster than regex for simple substring matching + const lower = queryString.toLowerCase(); + const hasSensitive = + lower.includes("token") || + lower.includes("key") || + lower.includes("password") || + lower.includes("passwd") || + lower.includes("secret") || + lower.includes("session") || + lower.includes("auth"); + + if (!hasSensitive) { + return url; + } + + // SLOW PATH: Parse and redact + const redactedParams: string[] = []; + const params = queryString.split("&"); + + for (const param of params) { + const equalIndex = param.indexOf("="); + if (equalIndex === -1) { + redactedParams.push(param); + continue; + } + + const key = param.slice(0, equalIndex); + let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()); + + if (!shouldRedact && key.includes("%")) { + try { + const decodedKey = decodeURIComponent(key); + shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase()); + } catch {} + } + + redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param); + } + + return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/requestWithRetries.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/requestWithRetries.ts new file mode 100644 index 000000000000..5e66b9330e51 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/requestWithRetries.ts @@ -0,0 +1,68 @@ +const INITIAL_RETRY_DELAY = 1000; // in milliseconds +const MAX_RETRY_DELAY = 60000; // in milliseconds +const DEFAULT_MAX_RETRIES = 2; +const JITTER_FACTOR = 0.2; // 20% random jitter + +function isRetryableStatusCode(statusCode: number): boolean { + return [408, 429].includes(statusCode) || statusCode >= 500; +} + +function addPositiveJitter(delay: number): number { + const jitterMultiplier = 1 + Math.random() * JITTER_FACTOR; + return delay * jitterMultiplier; +} + +function addSymmetricJitter(delay: number): number { + const jitterMultiplier = 1 + (Math.random() - 0.5) * JITTER_FACTOR; + return delay * jitterMultiplier; +} + +function getRetryDelayFromHeaders(response: Response, retryAttempt: number): number { + const retryAfter = response.headers.get("Retry-After"); + if (retryAfter) { + const retryAfterSeconds = parseInt(retryAfter, 10); + if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) { + return Math.min(retryAfterSeconds * 1000, MAX_RETRY_DELAY); + } + + const retryAfterDate = new Date(retryAfter); + if (!Number.isNaN(retryAfterDate.getTime())) { + const delay = retryAfterDate.getTime() - Date.now(); + if (delay > 0) { + return Math.min(Math.max(delay, 0), MAX_RETRY_DELAY); + } + } + } + + const rateLimitReset = response.headers.get("X-RateLimit-Reset"); + if (rateLimitReset) { + const resetTime = parseInt(rateLimitReset, 10); + if (!Number.isNaN(resetTime)) { + const delay = resetTime * 1000 - Date.now(); + if (delay > 0) { + return addPositiveJitter(Math.min(delay, MAX_RETRY_DELAY)); + } + } + } + + return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** retryAttempt, MAX_RETRY_DELAY)); +} + +export async function requestWithRetries( + requestFn: () => Promise, + maxRetries: number = DEFAULT_MAX_RETRIES, +): Promise { + let response: Response = await requestFn(); + + for (let i = 0; i < maxRetries; ++i) { + if (isRetryableStatusCode(response.status)) { + const delay = getRetryDelayFromHeaders(response, i); + + await new Promise((resolve) => setTimeout(resolve, delay)); + response = await requestFn(); + } else { + break; + } + } + return response!; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/signals.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/signals.ts new file mode 100644 index 000000000000..ba74c4d02be6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/fetcher/signals.ts @@ -0,0 +1,35 @@ +const TIMEOUT = "timeout"; + +export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: ReturnType } { + const controller = new AbortController(); + const abortId = setTimeout(() => controller.abort(TIMEOUT), timeoutMs); + return { signal: controller.signal, abortId }; +} + +export function anySignal(...args: AbortSignal[] | [AbortSignal[]]): AbortSignal { + const signals = (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as AbortSignal[]; + + const controller = new AbortController(); + + for (const signal of signals) { + if (signal.aborted) { + controller.abort((signal as any)?.reason); + return controller.signal; + } + + signal.addEventListener("abort", () => controller.abort((signal as any)?.reason), { + signal: controller.signal, + }); + + // Re-check after adding listener: the signal may have aborted + // between the initial `signal.aborted` check and the `addEventListener` + // call above. If it did, the abort event was already dispatched and + // the listener will never fire — we must manually abort. + if (signal.aborted) { + controller.abort((signal as any)?.reason); + return controller.signal; + } + } + + return controller.signal; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/headers.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/headers.ts new file mode 100644 index 000000000000..be45c4552a35 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/headers.ts @@ -0,0 +1,33 @@ +export function mergeHeaders(...headersArray: (Record | null | undefined)[]): Record { + const result: Record = {}; + + for (const [key, value] of headersArray + .filter((headers) => headers != null) + .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); + if (value != null) { + result[insensitiveKey] = value; + } else if (insensitiveKey in result) { + delete result[insensitiveKey]; + } + } + + return result; +} + +export function mergeOnlyDefinedHeaders( + ...headersArray: (Record | null | undefined)[] +): Record { + const result: Record = {}; + + for (const [key, value] of headersArray + .filter((headers) => headers != null) + .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); + if (value != null) { + result[insensitiveKey] = value; + } + } + + return result; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/index.ts new file mode 100644 index 000000000000..92290bfadcac --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/index.ts @@ -0,0 +1,6 @@ +export * from "./auth/index.js"; +export * from "./base64.js"; +export * from "./fetcher/index.js"; +export * as logging from "./logging/index.js"; +export * from "./runtime/index.js"; +export * as url from "./url/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/json.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/json.ts new file mode 100644 index 000000000000..c052f3249f4f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/json.ts @@ -0,0 +1,27 @@ +/** + * Serialize a value to JSON + * @param value A JavaScript value, usually an object or array, to be converted. + * @param replacer A function that transforms the results. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + * @returns JSON string + */ +export const toJson = ( + value: unknown, + replacer?: (this: unknown, key: string, value: unknown) => unknown, + space?: string | number, +): string => { + return JSON.stringify(value, replacer, space); +}; + +/** + * Parse JSON string to object, array, or other type + * @param text A valid JSON string. + * @param reviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. + * @returns Parsed object, array, or other type + */ +export function fromJson( + text: string, + reviver?: (this: unknown, key: string, value: unknown) => unknown, +): T { + return JSON.parse(text, reviver); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/exports.ts new file mode 100644 index 000000000000..88f6c00db0cf --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/exports.ts @@ -0,0 +1,19 @@ +import * as logger from "./logger.js"; + +export namespace logging { + /** + * Configuration for logger instances. + */ + export type LogConfig = logger.LogConfig; + export type LogLevel = logger.LogLevel; + export const LogLevel: typeof logger.LogLevel = logger.LogLevel; + export type ILogger = logger.ILogger; + /** + * Console logger implementation that outputs to the console. + */ + export type ConsoleLogger = logger.ConsoleLogger; + /** + * Console logger implementation that outputs to the console. + */ + export const ConsoleLogger: typeof logger.ConsoleLogger = logger.ConsoleLogger; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/index.ts new file mode 100644 index 000000000000..d81cc32c40f9 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/index.ts @@ -0,0 +1 @@ +export * from "./logger.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/logger.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/logger.ts new file mode 100644 index 000000000000..a3f3673cda93 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/logging/logger.ts @@ -0,0 +1,203 @@ +export const LogLevel = { + Debug: "debug", + Info: "info", + Warn: "warn", + Error: "error", +} as const; +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +const logLevelMap: Record = { + [LogLevel.Debug]: 1, + [LogLevel.Info]: 2, + [LogLevel.Warn]: 3, + [LogLevel.Error]: 4, +}; + +export interface ILogger { + /** + * Logs a debug message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + debug(message: string, ...args: unknown[]): void; + /** + * Logs an info message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + info(message: string, ...args: unknown[]): void; + /** + * Logs a warning message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + warn(message: string, ...args: unknown[]): void; + /** + * Logs an error message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + error(message: string, ...args: unknown[]): void; +} + +/** + * Configuration for logger initialization. + */ +export interface LogConfig { + /** + * Minimum log level to output. + * @default LogLevel.Info + */ + level?: LogLevel; + /** + * Logger implementation to use. + * @default new ConsoleLogger() + */ + logger?: ILogger; + /** + * Whether logging should be silenced. + * @default true + */ + silent?: boolean; +} + +/** + * Default console-based logger implementation. + */ +export class ConsoleLogger implements ILogger { + debug(message: string, ...args: unknown[]): void { + console.debug(message, ...args); + } + info(message: string, ...args: unknown[]): void { + console.info(message, ...args); + } + warn(message: string, ...args: unknown[]): void { + console.warn(message, ...args); + } + error(message: string, ...args: unknown[]): void { + console.error(message, ...args); + } +} + +/** + * Logger class that provides level-based logging functionality. + */ +export class Logger { + private readonly level: number; + private readonly logger: ILogger; + private readonly silent: boolean; + + /** + * Creates a new logger instance. + * @param config - Logger configuration + */ + constructor(config: Required) { + this.level = logLevelMap[config.level]; + this.logger = config.logger; + this.silent = config.silent; + } + + /** + * Checks if a log level should be output based on configuration. + * @param level - The log level to check + * @returns True if the level should be logged + */ + public shouldLog(level: LogLevel): boolean { + return !this.silent && this.level <= logLevelMap[level]; + } + + /** + * Checks if debug logging is enabled. + * @returns True if debug logs should be output + */ + public isDebug(): boolean { + return this.shouldLog(LogLevel.Debug); + } + + /** + * Logs a debug message if debug logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public debug(message: string, ...args: unknown[]): void { + if (this.isDebug()) { + this.logger.debug(message, ...args); + } + } + + /** + * Checks if info logging is enabled. + * @returns True if info logs should be output + */ + public isInfo(): boolean { + return this.shouldLog(LogLevel.Info); + } + + /** + * Logs an info message if info logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public info(message: string, ...args: unknown[]): void { + if (this.isInfo()) { + this.logger.info(message, ...args); + } + } + + /** + * Checks if warning logging is enabled. + * @returns True if warning logs should be output + */ + public isWarn(): boolean { + return this.shouldLog(LogLevel.Warn); + } + + /** + * Logs a warning message if warning logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public warn(message: string, ...args: unknown[]): void { + if (this.isWarn()) { + this.logger.warn(message, ...args); + } + } + + /** + * Checks if error logging is enabled. + * @returns True if error logs should be output + */ + public isError(): boolean { + return this.shouldLog(LogLevel.Error); + } + + /** + * Logs an error message if error logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public error(message: string, ...args: unknown[]): void { + if (this.isError()) { + this.logger.error(message, ...args); + } + } +} + +export function createLogger(config?: LogConfig | Logger): Logger { + if (config == null) { + return defaultLogger; + } + if (config instanceof Logger) { + return config; + } + config = config ?? {}; + config.level ??= LogLevel.Info; + config.logger ??= new ConsoleLogger(); + config.silent ??= true; + return new Logger(config as Required); +} + +const defaultLogger: Logger = new Logger({ + level: LogLevel.Info, + logger: new ConsoleLogger(), + silent: true, +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/requestBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/requestBody.ts new file mode 100644 index 000000000000..d58ef590cffc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/requestBody.ts @@ -0,0 +1,26 @@ +/** + * Spreads caller-supplied `additionalBodyParameters` (from `requestOptions.additionalBodyParameters`) + * on top of the request body. Caller-supplied properties win over the endpoint body. When no + * additional body parameters are provided, the original body is returned unchanged so serialization + * is unaffected. + * + * The merge only applies to plain-object (JSON object) bodies. When the body is `null`/`undefined` + * the additional parameters become the body; when the body is an array or a primitive JSON value it + * is returned unchanged, since object properties cannot be spread into it. This mirrors the Python + * SDK, which only merges additional body parameters into mapping bodies. + */ +export function mergeAdditionalBodyParameters( + body: unknown, + additionalBodyParameters: Record | undefined, +): unknown { + if (additionalBodyParameters == null) { + return body; + } + if (body == null) { + return { ...additionalBodyParameters }; + } + if (typeof body === "object" && !Array.isArray(body)) { + return { ...body, ...additionalBodyParameters }; + } + return body; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/index.ts new file mode 100644 index 000000000000..85a327200031 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/index.ts @@ -0,0 +1 @@ +export { getUserAgent, RUNTIME } from "./runtime.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/runtime.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/runtime.ts new file mode 100644 index 000000000000..d367ce6b9041 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/runtime/runtime.ts @@ -0,0 +1,231 @@ +interface DenoGlobal { + version: { + deno: string; + }; + build?: { + os?: string; + arch?: string; + }; +} + +interface BunGlobal { + version: string; +} + +declare const Deno: DenoGlobal | undefined; +declare const Bun: BunGlobal | undefined; +declare const EdgeRuntime: string | undefined; +declare const self: typeof globalThis.self & { + importScripts?: unknown; +}; + +/** + * A constant that indicates which environment and version the SDK is running in. + */ +export const RUNTIME: Runtime = evaluateRuntime(); + +export interface Runtime { + type: "browser" | "web-worker" | "deno" | "bun" | "node" | "react-native" | "unknown" | "workerd" | "edge-runtime"; + version?: string; + parsedVersion?: number; + /** + * The operating system the SDK is running on, when it can be determined + * (e.g. "linux", "darwin", "win32" on server runtimes). Undefined in + * environments where the OS is not observable (e.g. browsers). + */ + os?: string; + /** + * The CPU architecture the SDK is running on, when it can be determined + * (e.g. "x64", "arm64" on server runtimes). Undefined in environments where + * the architecture is not observable (e.g. browsers). + */ + arch?: string; +} + +function evaluateRuntime(): Runtime { + /** + * A constant that indicates whether the environment the code is running is a Web Browser. + */ + const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; + if (isBrowser) { + return { + type: "browser", + version: window.navigator.userAgent, + }; + } + + /** + * A constant that indicates whether the environment the code is running is Cloudflare. + * https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent + */ + const isCloudflare = typeof globalThis !== "undefined" && globalThis?.navigator?.userAgent === "Cloudflare-Workers"; + if (isCloudflare) { + return { + type: "workerd", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Edge Runtime. + * https://vercel.com/docs/functions/runtimes/edge-runtime#check-if-you're-running-on-the-edge-runtime + */ + const isEdgeRuntime = typeof EdgeRuntime === "string"; + if (isEdgeRuntime) { + return { + type: "edge-runtime", + }; + } + + /** + * A constant that indicates whether the environment the code is running is a Web Worker. + */ + const isWebWorker = + typeof self === "object" && + typeof self?.importScripts === "function" && + (self.constructor?.name === "DedicatedWorkerGlobalScope" || + self.constructor?.name === "ServiceWorkerGlobalScope" || + self.constructor?.name === "SharedWorkerGlobalScope"); + if (isWebWorker) { + return { + type: "web-worker", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Deno. + * FYI Deno spoofs process.versions.node, see https://deno.land/std@0.177.0/node/process.ts?s=versions + */ + const isDeno = + typeof Deno !== "undefined" && typeof Deno.version !== "undefined" && typeof Deno.version.deno !== "undefined"; + if (isDeno) { + return { + type: "deno", + version: Deno.version.deno, + os: Deno.build?.os, + arch: Deno.build?.arch, + }; + } + + /** + * A constant that indicates whether the environment the code is running is Bun.sh. + */ + const isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined"; + if (isBun) { + return { + type: "bun", + version: Bun.version, + os: typeof process !== "undefined" ? process.platform : undefined, + arch: typeof process !== "undefined" ? process.arch : undefined, + }; + } + + /** + * A constant that indicates whether the environment the code is running is in React-Native. + * This check should come before Node.js detection since React Native may have a process polyfill. + * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js + */ + const isReactNative = typeof navigator !== "undefined" && navigator?.product === "ReactNative"; + if (isReactNative) { + return { + type: "react-native", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Node.JS. + * + * We assign `process` to a local variable first to avoid being flagged by + * bundlers that perform static analysis on `process.versions` (e.g. Next.js + * Edge Runtime warns about Node.js APIs even when they are guarded). + */ + const _process = typeof process !== "undefined" ? process : undefined; + const isNode = typeof _process !== "undefined" && typeof _process.versions?.node === "string"; + if (isNode) { + return { + type: "node", + version: _process.versions.node, + parsedVersion: Number(_process.versions.node.split(".")[0]), + os: _process.platform, + arch: _process.arch, + }; + } + + return { + type: "unknown", + }; +} + +/** + * Display names for the language runtimes whose version is meaningful to encode + * in a User-Agent. Environments where a version string is not useful (e.g. + * browsers, where `version` is the full navigator UA) are intentionally mapped + * to `undefined` so they are omitted from the User-Agent. + */ +const RUNTIME_DISPLAY_NAMES: Record = { + node: "Node", + deno: "Deno", + bun: "Bun", + browser: undefined, + "web-worker": undefined, + "react-native": undefined, + workerd: undefined, + "edge-runtime": undefined, + unknown: undefined, +}; + +/** + * CPU architecture aliases that all refer to 64-bit x86. They are normalized to + * the single canonical token `x86_64` so the User-Agent architecture label is + * consistent regardless of which runtime reports it (Node reports `x64`, others + * report `amd64` or `x86_64`). + */ +const X86_64_ARCH_ALIASES = new Set(["x64", "amd64", "x86_64"]); + +/** + * Normalizes a CPU architecture token, collapsing the 64-bit x86 aliases + * (`x64`, `amd64`, `x86_64`) to `x86_64`. Other architectures are returned + * unchanged. + */ +function normalizeArch(arch: string | undefined): string | undefined { + if (arch == null) { + return arch; + } + return X86_64_ARCH_ALIASES.has(arch.toLowerCase()) ? "x86_64" : arch; +} + +/** + * Percent-encodes the `@` and `/` characters in an npm package name so the + * User-Agent product token stays within the RFC 7230 token grammar. The + * original scoped package name can be recovered by URL-decoding (e.g. + * `@dummy/sdk` becomes `%40dummy%2Fsdk`). + */ +function encodeProductName(sdkName: string): string { + return sdkName.replace(/@/g, "%40").replace(/\//g, "%2F"); +} + +/** + * Builds a structured User-Agent string of the form + * `{sdkName}/{sdkVersion} ({os}; {arch}) {runtime}/{runtimeVersion}` + * where the platform group and runtime segment are omitted gracefully when the + * underlying values cannot be determined (e.g. in a browser). + */ +export function getUserAgent(sdkName: string, sdkVersion: string): string { + let userAgent = `${encodeProductName(sdkName)}/${sdkVersion}`; + + const platform = [RUNTIME.os, normalizeArch(RUNTIME.arch)].filter( + (part): part is string => part != null && part.length > 0, + ); + if (platform.length > 0) { + userAgent += ` (${platform.join("; ")})`; + } + + const runtimeName = RUNTIME_DISPLAY_NAMES[RUNTIME.type]; + if (runtimeName != null) { + userAgent += ` ${runtimeName}`; + if (RUNTIME.version != null && RUNTIME.version.length > 0) { + userAgent += `/${RUNTIME.version}`; + } + } + + return userAgent; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/QueryStringBuilder.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/QueryStringBuilder.ts new file mode 100644 index 000000000000..b045221c381c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/QueryStringBuilder.ts @@ -0,0 +1,87 @@ +import { toQueryString } from "./qs.js"; + +/** + * Creates a fluent builder for constructing URL query strings. + * + * Each `.add()` call serializes its value immediately (like C#'s builder), + * so no format tracking is needed — the style is applied at add-time. + * + * Usage (generated code): + * + * const qs = core.url.queryBuilder() + * .add("limit", limit) + * .add("tags", tags, { style: "comma" }) // explode: false + * .mergeAdditional(requestOptions?.queryParams) + * .build(); + */ +export function queryBuilder(): QueryStringBuilder { + return new QueryStringBuilder(); +} + +class QueryStringBuilder { + private parts: Map = new Map(); + + /** + * Adds a query parameter, serializing it immediately. + * + * By default arrays use "repeat" format (`key=a&key=b`). + * Pass `{ style: "comma" }` for OpenAPI `explode: false` parameters + * to get comma-separated values (`key=a,b,c`). + * + * Null / undefined values are silently skipped. + */ + add(key: string, value: unknown, options?: { style?: "comma" }): this { + if (value === undefined || value === null) { + return this; + } + const serialized = toQueryString( + { [key]: value }, + { arrayFormat: options?.style === "comma" ? "comma" : "repeat" }, + ); + if (serialized.length > 0) { + this.parts.set(key, serialized); + } + return this; + } + + /** + * Adds multiple query parameters at once from a record. + * All parameters use the default "repeat" array format. + * Null / undefined values are silently skipped. + */ + addMany(params: Record): this { + if (params != null) { + for (const [key, value] of Object.entries(params)) { + this.add(key, value); + } + } + return this; + } + + /** + * Merges additional query parameters supplied at call-time via + * `requestOptions.queryParams`. Overrides existing keys (last-write-wins). + */ + mergeAdditional(additionalParams?: Record): this { + if (additionalParams != null) { + for (const [key, value] of Object.entries(additionalParams)) { + if (value === undefined || value === null) { + continue; + } + const serialized = toQueryString({ [key]: value }, { arrayFormat: "repeat" }); + if (serialized.length > 0) { + this.parts.set(key, serialized); + } + } + } + return this; + } + + /** + * Returns the assembled query string (without the leading `?`). + * Returns an empty string when no parameters were added. + */ + build(): string { + return [...this.parts.values()].join("&"); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/encodePathParam.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/encodePathParam.ts new file mode 100644 index 000000000000..19b901244218 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/encodePathParam.ts @@ -0,0 +1,18 @@ +export function encodePathParam(param: unknown): string { + if (param === null) { + return "null"; + } + const typeofParam = typeof param; + switch (typeofParam) { + case "undefined": + return "undefined"; + case "string": + case "number": + case "boolean": + break; + default: + param = String(param); + break; + } + return encodeURIComponent(param as string | number | boolean); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/index.ts new file mode 100644 index 000000000000..ca9d4fbd1eb6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/index.ts @@ -0,0 +1,4 @@ +export { encodePathParam } from "./encodePathParam.js"; +export { join } from "./join.js"; +export { queryBuilder } from "./QueryStringBuilder.js"; +export { toQueryString } from "./qs.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/join.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/join.ts new file mode 100644 index 000000000000..7ca7daef094d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/join.ts @@ -0,0 +1,79 @@ +export function join(base: string, ...segments: string[]): string { + if (!base) { + return ""; + } + + if (segments.length === 0) { + return base; + } + + if (base.includes("://")) { + let url: URL; + try { + url = new URL(base); + } catch { + return joinPath(base, ...segments); + } + + const lastSegment = segments[segments.length - 1]; + const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + + for (const segment of segments) { + const cleanSegment = trimSlashes(segment); + if (cleanSegment) { + url.pathname = joinPathSegments(url.pathname, cleanSegment); + } + } + + if (shouldPreserveTrailingSlash && !url.pathname.endsWith("/")) { + url.pathname += "/"; + } + + return url.toString(); + } + + return joinPath(base, ...segments); +} + +function joinPath(base: string, ...segments: string[]): string { + if (segments.length === 0) { + return base; + } + + let result = base; + + const lastSegment = segments[segments.length - 1]; + const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + + for (const segment of segments) { + const cleanSegment = trimSlashes(segment); + if (cleanSegment) { + result = joinPathSegments(result, cleanSegment); + } + } + + if (shouldPreserveTrailingSlash && !result.endsWith("/")) { + result += "/"; + } + + return result; +} + +function joinPathSegments(left: string, right: string): string { + if (left.endsWith("/")) { + return left + right; + } + return `${left}/${right}`; +} + +function trimSlashes(str: string): string { + if (!str) return str; + + let start = 0; + let end = str.length; + + if (str.startsWith("/")) start = 1; + if (str.endsWith("/")) end = str.length - 1; + + return start === 0 && end === str.length ? str : str.slice(start, end); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/qs.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/qs.ts new file mode 100644 index 000000000000..aebb95a38bd4 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/core/url/qs.ts @@ -0,0 +1,87 @@ +type ArrayFormat = "indices" | "repeat" | "comma"; + +interface QueryStringOptions { + arrayFormat?: ArrayFormat; + encode?: boolean; +} + +const defaultQsOptions: Required = { + arrayFormat: "indices", + encode: true, +} as const; + +function encodeValue(value: unknown, shouldEncode: boolean): string { + if (value === undefined) { + return ""; + } + if (value === null) { + return ""; + } + const stringValue = String(value); + return shouldEncode ? encodeURIComponent(stringValue) : stringValue; +} + +function stringifyObject(obj: Record, prefix = "", options: Required): string[] { + const parts: string[] = []; + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}[${key}]` : key; + + if (value == null) { + continue; + } + + if (Array.isArray(value)) { + if (value.length === 0) { + continue; + } + const effectiveFormat = options.arrayFormat; + if (effectiveFormat === "comma") { + const encodedKey = options.encode ? encodeURIComponent(fullKey) : fullKey; + const encodedValues = value + .filter((item) => item !== undefined && item !== null) + .map((item) => encodeValue(item, options.encode)); + if (encodedValues.length > 0) { + parts.push(`${encodedKey}=${encodedValues.join(",")}`); + } + } else { + for (let i = 0; i < value.length; i++) { + const item = value[i]; + if (item == null) { + continue; + } + if (typeof item === "object" && !Array.isArray(item) && item !== null) { + const arrayKey = effectiveFormat === "indices" ? `${fullKey}[${i}]` : fullKey; + parts.push(...stringifyObject(item as Record, arrayKey, options)); + } else { + const arrayKey = effectiveFormat === "indices" ? `${fullKey}[${i}]` : fullKey; + const encodedKey = options.encode ? encodeURIComponent(arrayKey) : arrayKey; + parts.push(`${encodedKey}=${encodeValue(item, options.encode)}`); + } + } + } + } else if (typeof value === "object" && value !== null) { + if (Object.keys(value as Record).length === 0) { + continue; + } + parts.push(...stringifyObject(value as Record, fullKey, options)); + } else { + const encodedKey = options.encode ? encodeURIComponent(fullKey) : fullKey; + parts.push(`${encodedKey}=${encodeValue(value, options.encode)}`); + } + } + + return parts; +} + +export function toQueryString(obj: unknown, options?: QueryStringOptions): string { + if (obj == null || typeof obj !== "object") { + return ""; + } + + const parts = stringifyObject(obj as Record, "", { + ...defaultQsOptions, + ...options, + }); + return parts.join("&"); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthError.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthError.ts new file mode 100644 index 000000000000..3c9be0ee12e6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthError.ts @@ -0,0 +1,68 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import { toJson } from "../core/json.js"; + +export class SeedTsFlattenRequestAnyAuthError extends Error { + public readonly statusCode?: number; + public readonly body?: unknown; + public readonly rawResponse?: core.RawResponse; + public readonly cause?: unknown; + + constructor({ + message, + statusCode, + body, + rawResponse, + cause, + }: { + message?: string; + statusCode?: number; + body?: unknown; + rawResponse?: core.RawResponse; + cause?: unknown; + }) { + super(buildMessage({ message, statusCode, body })); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "SeedTsFlattenRequestAnyAuthError"; + this.statusCode = statusCode; + this.body = body; + this.rawResponse = rawResponse; + if (cause != null) { + this.cause = cause; + } + } + + public get requestId(): string | undefined { + return this.rawResponse?.headers?.get("x-request-id") ?? undefined; + } +} + +function buildMessage({ + message, + statusCode, + body, +}: { + message: string | undefined; + statusCode: number | undefined; + body: unknown | undefined; +}): string { + const lines: string[] = []; + if (message != null) { + lines.push(message); + } + + if (statusCode != null) { + lines.push(`Status code: ${statusCode.toString()}`); + } + + if (body != null) { + lines.push(`Body: ${toJson(body, undefined, 2)}`); + } + + return lines.join("\n"); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts new file mode 100644 index 000000000000..51256ce1a8a9 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as errors from "./index.js"; + +export class SeedTsFlattenRequestAnyAuthTimeoutError extends errors.SeedTsFlattenRequestAnyAuthError { + constructor(message: string, opts?: { cause?: unknown }) { + super({ + message: message, + cause: opts?.cause, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "SeedTsFlattenRequestAnyAuthTimeoutError"; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/handleNonStatusCodeError.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/handleNonStatusCodeError.ts new file mode 100644 index 000000000000..43d16dac85fc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/handleNonStatusCodeError.ts @@ -0,0 +1,43 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import * as errors from "./index.js"; + +export function handleNonStatusCodeError( + error: core.Fetcher.Error, + rawResponse: core.RawResponse, + method: string, + path: string, +): never { + switch (error.reason) { + case "non-json": + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: error.statusCode, + body: error.rawBody, + rawResponse: rawResponse, + }); + case "body-is-null": + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: error.statusCode, + rawResponse: rawResponse, + }); + case "timeout": + throw new errors.SeedTsFlattenRequestAnyAuthTimeoutError( + `Timeout exceeded when calling ${method} ${path}.`, + { + cause: error.cause, + }, + ); + case "unknown": + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: error.errorMessage, + rawResponse: rawResponse, + cause: error.cause, + }); + default: + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: "Unknown error", + rawResponse: rawResponse, + }); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/index.ts new file mode 100644 index 000000000000..cd36e0da75f7 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/errors/index.ts @@ -0,0 +1,2 @@ +export { SeedTsFlattenRequestAnyAuthError } from "./SeedTsFlattenRequestAnyAuthError.js"; +export { SeedTsFlattenRequestAnyAuthTimeoutError } from "./SeedTsFlattenRequestAnyAuthTimeoutError.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/exports.ts new file mode 100644 index 000000000000..7b70ee14fc02 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/exports.ts @@ -0,0 +1 @@ +export * from "./core/exports.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/index.ts new file mode 100644 index 000000000000..e7b7c7e7db0a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/index.ts @@ -0,0 +1,5 @@ +export * as SeedTsFlattenRequestAnyAuth from "./api/index.js"; +export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; +export { SeedTsFlattenRequestAnyAuthClient } from "./Client.js"; +export { SeedTsFlattenRequestAnyAuthError, SeedTsFlattenRequestAnyAuthTimeoutError } from "./errors/index.js"; +export * from "./exports.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/version.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/version.ts new file mode 100644 index 000000000000..b643a3e3ea27 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/version.ts @@ -0,0 +1 @@ +export const SDK_VERSION = "0.0.1"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/custom.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/custom.test.ts new file mode 100644 index 000000000000..7f5e031c8396 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/custom.test.ts @@ -0,0 +1,13 @@ +/** + * This is a custom test file, if you wish to add more tests + * to your SDK. + * Be sure to mark this file in `.fernignore`. + * + * If you include example requests/responses in your fern definition, + * you will have tests automatically generated for you. + */ +describe("test", () => { + it("default", () => { + expect(true).toBe(true); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServer.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServer.ts new file mode 100644 index 000000000000..954872157d52 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServer.ts @@ -0,0 +1,29 @@ +import type { RequestHandlerOptions } from "msw"; +import type { SetupServer } from "msw/node"; + +import { mockEndpointBuilder } from "./mockEndpointBuilder"; + +export interface MockServerOptions { + baseUrl: string; + server: SetupServer; +} + +export class MockServer { + private readonly server: SetupServer; + public readonly baseUrl: string; + + constructor({ baseUrl, server }: MockServerOptions) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + this.server = server; + } + + public mockEndpoint(options?: RequestHandlerOptions): ReturnType { + const builder = mockEndpointBuilder({ + once: options?.once ?? true, + onBuild: (handler) => { + this.server.use(handler); + }, + }).baseUrl(this.baseUrl); + return builder; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServerPool.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServerPool.ts new file mode 100644 index 000000000000..d7d891a2d80b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/MockServerPool.ts @@ -0,0 +1,106 @@ +import { setupServer } from "msw/node"; + +import { fromJson, toJson } from "../../src/core/json"; +import { MockServer } from "./MockServer"; +import { randomBaseUrl } from "./randomBaseUrl"; + +const mswServer = setupServer(); +interface MockServerOptions { + baseUrl?: string; +} + +async function formatHttpRequest(request: Request, id?: string): Promise { + try { + const clone = request.clone(); + const headers = [...clone.headers.entries()].map(([k, v]) => `${k}: ${v}`).join("\n"); + + let body = ""; + try { + const contentType = clone.headers.get("content-type"); + if (contentType?.includes("application/json")) { + body = toJson(fromJson(await clone.text()), undefined, 2); + } else if (clone.body) { + body = await clone.text(); + } + } catch (_e) { + body = "(unable to parse body)"; + } + + const title = id ? `### Request ${id} ###\n` : ""; + const firstLine = `${title}${request.method} ${request.url.toString()} HTTP/1.1`; + + return `\n${firstLine}\n${headers}\n\n${body || "(no body)"}\n`; + } catch (e) { + return `Error formatting request: ${e}`; + } +} + +async function formatHttpResponse(response: Response, id?: string): Promise { + try { + const clone = response.clone(); + const headers = [...clone.headers.entries()].map(([k, v]) => `${k}: ${v}`).join("\n"); + + let body = ""; + try { + const contentType = clone.headers.get("content-type"); + if (contentType?.includes("application/json")) { + body = toJson(fromJson(await clone.text()), undefined, 2); + } else if (clone.body) { + body = await clone.text(); + } + } catch (_e) { + body = "(unable to parse body)"; + } + + const title = id ? `### Response for ${id} ###\n` : ""; + const firstLine = `${title}HTTP/1.1 ${response.status} ${response.statusText}`; + + return `\n${firstLine}\n${headers}\n\n${body || "(no body)"}\n`; + } catch (e) { + return `Error formatting response: ${e}`; + } +} + +class MockServerPool { + private servers: MockServer[] = []; + + public createServer(options?: Partial): MockServer { + const baseUrl = options?.baseUrl || randomBaseUrl(); + const server = new MockServer({ baseUrl, server: mswServer }); + this.servers.push(server); + return server; + } + + public getServers(): MockServer[] { + return [...this.servers]; + } + + public listen(): void { + const onUnhandledRequest = process.env.LOG_LEVEL === "debug" ? "warn" : "bypass"; + mswServer.listen({ onUnhandledRequest }); + + if (process.env.LOG_LEVEL === "debug") { + mswServer.events.on("request:start", async ({ request, requestId }) => { + const formattedRequest = await formatHttpRequest(request, requestId); + console.debug(`request:start\n${formattedRequest}`); + }); + + mswServer.events.on("request:unhandled", async ({ request, requestId }) => { + const formattedRequest = await formatHttpRequest(request, requestId); + console.debug(`request:unhandled\n${formattedRequest}`); + }); + + mswServer.events.on("response:mocked", async ({ request, response, requestId }) => { + const formattedResponse = await formatHttpResponse(response, requestId); + console.debug(`response:mocked\n${formattedResponse}`); + }); + } + } + + public close(): void { + this.servers = []; + mswServer.close(); + } +} + +export const mockServerPool: MockServerPool = new MockServerPool(); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/mockEndpointBuilder.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/mockEndpointBuilder.ts new file mode 100644 index 000000000000..3e8540a3ba5a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/mockEndpointBuilder.ts @@ -0,0 +1,234 @@ +import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponseResolver, http } from "msw"; + +import { url } from "../../src/core"; +import { toJson } from "../../src/core/json"; +import { type WithFormUrlEncodedOptions, withFormUrlEncoded } from "./withFormUrlEncoded"; +import { withHeaders } from "./withHeaders"; +import { type WithJsonOptions, withJson } from "./withJson"; + +type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; + +interface MethodStage { + baseUrl(baseUrl: string): MethodStage; + all(path: string): RequestHeadersStage; + get(path: string): RequestHeadersStage; + post(path: string): RequestHeadersStage; + put(path: string): RequestHeadersStage; + delete(path: string): RequestHeadersStage; + patch(path: string): RequestHeadersStage; + options(path: string): RequestHeadersStage; + head(path: string): RequestHeadersStage; +} + +interface RequestHeadersStage extends RequestBodyStage, ResponseStage { + header(name: string, value: string): RequestHeadersStage; + headers(headers: Record): RequestBodyStage; +} + +interface RequestBodyStage extends ResponseStage { + jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage; + formUrlEncodedBody(body: unknown, options?: WithFormUrlEncodedOptions): ResponseStage; +} + +interface ResponseStage { + respondWith(): ResponseStatusStage; +} +interface ResponseStatusStage { + statusCode(statusCode: number): ResponseHeaderStage; +} + +interface ResponseHeaderStage extends ResponseBodyStage, BuildStage { + header(name: string, value: string): ResponseHeaderStage; + headers(headers: Record): ResponseHeaderStage; +} + +interface ResponseBodyStage { + jsonBody(body: unknown): BuildStage; + sseBody(body: string): BuildStage; +} + +interface BuildStage { + build(): HttpHandler; +} + +export interface HttpHandlerBuilderOptions { + onBuild?: (handler: HttpHandler) => void; + once?: boolean; +} + +class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodyStage, ResponseStage { + private method: HttpMethod = "get"; + private _baseUrl: string = ""; + private path: string = "/"; + private readonly predicates: ((resolver: HttpResponseResolver) => HttpResponseResolver)[] = []; + private readonly handlerOptions?: HttpHandlerBuilderOptions; + + constructor(options?: HttpHandlerBuilderOptions) { + this.handlerOptions = options; + } + + baseUrl(baseUrl: string): MethodStage { + this._baseUrl = baseUrl; + return this; + } + + all(path: string): RequestHeadersStage { + this.method = "all"; + this.path = path; + return this; + } + + get(path: string): RequestHeadersStage { + this.method = "get"; + this.path = path; + return this; + } + + post(path: string): RequestHeadersStage { + this.method = "post"; + this.path = path; + return this; + } + + put(path: string): RequestHeadersStage { + this.method = "put"; + this.path = path; + return this; + } + + delete(path: string): RequestHeadersStage { + this.method = "delete"; + this.path = path; + return this; + } + + patch(path: string): RequestHeadersStage { + this.method = "patch"; + this.path = path; + return this; + } + + options(path: string): RequestHeadersStage { + this.method = "options"; + this.path = path; + return this; + } + + head(path: string): RequestHeadersStage { + this.method = "head"; + this.path = path; + return this; + } + + header(name: string, value: string): RequestHeadersStage { + this.predicates.push((resolver) => withHeaders({ [name]: value }, resolver)); + return this; + } + + headers(headers: Record): RequestBodyStage { + this.predicates.push((resolver) => withHeaders(headers, resolver)); + return this; + } + + jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage { + if (body === undefined) { + throw new Error("Undefined is not valid JSON. Do not call jsonBody if you want an empty body."); + } + this.predicates.push((resolver) => withJson(body, resolver, options)); + return this; + } + + formUrlEncodedBody(body: unknown, options?: WithFormUrlEncodedOptions): ResponseStage { + if (body === undefined) { + throw new Error( + "Undefined is not valid for form-urlencoded. Do not call formUrlEncodedBody if you want an empty body.", + ); + } + this.predicates.push((resolver) => withFormUrlEncoded(body, resolver, options)); + return this; + } + + respondWith(): ResponseStatusStage { + return new ResponseBuilder(this.method, this.buildUrl(), this.predicates, this.handlerOptions); + } + + private buildUrl(): string { + return url.join(this._baseUrl, this.path); + } +} + +class ResponseBuilder implements ResponseStatusStage, ResponseHeaderStage, ResponseBodyStage, BuildStage { + private readonly method: HttpMethod; + private readonly url: string; + private readonly requestPredicates: ((resolver: HttpResponseResolver) => HttpResponseResolver)[]; + private readonly handlerOptions?: HttpHandlerBuilderOptions; + + private responseStatusCode: number = 200; + private responseHeaders: Record = {}; + private responseBody: DefaultBodyType = undefined; + + constructor( + method: HttpMethod, + url: string, + requestPredicates: ((resolver: HttpResponseResolver) => HttpResponseResolver)[], + options?: HttpHandlerBuilderOptions, + ) { + this.method = method; + this.url = url; + this.requestPredicates = requestPredicates; + this.handlerOptions = options; + } + + public statusCode(code: number): ResponseHeaderStage { + this.responseStatusCode = code; + return this; + } + + public header(name: string, value: string): ResponseHeaderStage { + this.responseHeaders[name] = value; + return this; + } + + public headers(headers: Record): ResponseHeaderStage { + this.responseHeaders = { ...this.responseHeaders, ...headers }; + return this; + } + + public jsonBody(body: unknown): BuildStage { + if (body === undefined) { + throw new Error("Undefined is not valid JSON. Do not call jsonBody if you expect an empty body."); + } + this.responseBody = toJson(body); + return this; + } + + public sseBody(body: string): BuildStage { + this.responseHeaders["Content-Type"] = "text/event-stream"; + this.responseBody = body; + return this; + } + + public build(): HttpHandler { + const responseResolver: HttpResponseResolver = () => { + const response = new HttpResponse(this.responseBody, { + status: this.responseStatusCode, + headers: this.responseHeaders, + }); + // if no Content-Type header is set, delete the default text content type that is set + if (Object.keys(this.responseHeaders).some((key) => key.toLowerCase() === "content-type") === false) { + response.headers.delete("Content-Type"); + } + return response; + }; + + const finalResolver = this.requestPredicates.reduceRight((acc, predicate) => predicate(acc), responseResolver); + + const handler = http[this.method](this.url, finalResolver, this.handlerOptions); + this.handlerOptions?.onBuild?.(handler); + return handler; + } +} + +export function mockEndpointBuilder(options?: HttpHandlerBuilderOptions): MethodStage { + return new RequestBuilder(options); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/randomBaseUrl.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/randomBaseUrl.ts new file mode 100644 index 000000000000..031aa6408aca --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/randomBaseUrl.ts @@ -0,0 +1,4 @@ +export function randomBaseUrl(): string { + const randomString = Math.random().toString(36).substring(2, 15); + return `http://${randomString}.localhost`; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/setup.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/setup.ts new file mode 100644 index 000000000000..aeb3a95af7dc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/setup.ts @@ -0,0 +1,10 @@ +import { afterAll, beforeAll } from "vitest"; + +import { mockServerPool } from "./MockServerPool"; + +beforeAll(() => { + mockServerPool.listen(); +}); +afterAll(() => { + mockServerPool.close(); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withFormUrlEncoded.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withFormUrlEncoded.ts new file mode 100644 index 000000000000..2b23448e3102 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withFormUrlEncoded.ts @@ -0,0 +1,104 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +import { toJson } from "../../src/core/json"; + +export interface WithFormUrlEncodedOptions { + /** + * List of field names to ignore when comparing request bodies. + * This is useful for pagination cursor fields that change between requests. + */ + ignoredFields?: string[]; +} + +/** + * Creates a request matcher that validates if the request form-urlencoded body exactly matches the expected object + * @param expectedBody - The exact body object to match against + * @param resolver - Response resolver to execute if body matches + * @param options - Optional configuration including fields to ignore + */ +export function withFormUrlEncoded( + expectedBody: unknown, + resolver: HttpResponseResolver, + options?: WithFormUrlEncodedOptions, +): HttpResponseResolver { + const ignoredFields = options?.ignoredFields ?? []; + return async (args) => { + const { request } = args; + + let clonedRequest: Request; + let bodyText: string | undefined; + let actualBody: Record; + try { + clonedRequest = request.clone(); + bodyText = await clonedRequest.text(); + if (bodyText === "") { + // Empty body is valid if expected body is also empty + const isExpectedEmpty = + expectedBody != null && + typeof expectedBody === "object" && + Object.keys(expectedBody as Record).length === 0; + if (!isExpectedEmpty) { + console.error("Request body is empty, expected a form-urlencoded body."); + return passthrough(); + } + actualBody = {}; + } else { + const params = new URLSearchParams(bodyText); + actualBody = {}; + for (const [key, value] of params.entries()) { + actualBody[key] = value; + } + } + } catch (error) { + console.error(`Error processing form-urlencoded request body:\n\tError: ${error}\n\tBody: ${bodyText}`); + return passthrough(); + } + + const mismatches = findMismatches(actualBody, expectedBody); + const filteredMismatches = Object.keys(mismatches).filter((key) => !ignoredFields.includes(key)); + if (filteredMismatches.length > 0) { + console.error("Form-urlencoded body mismatch:", toJson(mismatches, undefined, 2)); + return passthrough(); + } + + return resolver(args); + }; +} + +function findMismatches(actual: any, expected: any): Record { + const mismatches: Record = {}; + + if (typeof actual !== typeof expected) { + return { value: { actual, expected } }; + } + + if (typeof actual !== "object" || actual === null || expected === null) { + if (actual !== expected) { + return { value: { actual, expected } }; + } + return {}; + } + + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + + const allKeys = new Set([...actualKeys, ...expectedKeys]); + + for (const key of allKeys) { + if (!expectedKeys.includes(key)) { + if (actual[key] === undefined) { + continue; + } + mismatches[key] = { actual: actual[key], expected: undefined }; + } else if (!actualKeys.includes(key)) { + if (expected[key] === undefined) { + continue; + } + mismatches[key] = { actual: undefined, expected: expected[key] }; + } else if (actual[key] !== expected[key]) { + mismatches[key] = { actual: actual[key], expected: expected[key] }; + } + } + + return mismatches; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withHeaders.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withHeaders.ts new file mode 100644 index 000000000000..6599d2b4a92d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withHeaders.ts @@ -0,0 +1,70 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +/** + * Creates a request matcher that validates if request headers match specified criteria + * @param expectedHeaders - Headers to match against + * @param resolver - Response resolver to execute if headers match + */ +export function withHeaders( + expectedHeaders: Record boolean)>, + resolver: HttpResponseResolver, +): HttpResponseResolver { + return (args) => { + const { request } = args; + const { headers } = request; + + const mismatches: Record< + string, + { actual: string | null; expected: string | RegExp | ((value: string) => boolean) } + > = {}; + + for (const [key, expectedValue] of Object.entries(expectedHeaders)) { + const actualValue = headers.get(key); + + if (actualValue === null) { + mismatches[key] = { actual: null, expected: expectedValue }; + continue; + } + + if (typeof expectedValue === "function") { + if (!expectedValue(actualValue)) { + mismatches[key] = { actual: actualValue, expected: expectedValue }; + } + } else if (expectedValue instanceof RegExp) { + if (!expectedValue.test(actualValue)) { + mismatches[key] = { actual: actualValue, expected: expectedValue }; + } + } else if (expectedValue !== actualValue) { + mismatches[key] = { actual: actualValue, expected: expectedValue }; + } + } + + if (Object.keys(mismatches).length > 0) { + const formattedMismatches = formatHeaderMismatches(mismatches); + console.error("Header mismatch:", formattedMismatches); + return passthrough(); + } + + return resolver(args); + }; +} + +function formatHeaderMismatches( + mismatches: Record boolean) }>, +): Record { + const formatted: Record = {}; + + for (const [key, { actual, expected }] of Object.entries(mismatches)) { + formatted[key] = { + actual, + expected: + expected instanceof RegExp + ? expected.toString() + : typeof expected === "function" + ? "[Function]" + : expected, + }; + } + + return formatted; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withJson.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withJson.ts new file mode 100644 index 000000000000..3e8800a0c374 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/mock-server/withJson.ts @@ -0,0 +1,173 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +import { fromJson, toJson } from "../../src/core/json"; + +export interface WithJsonOptions { + /** + * List of field names to ignore when comparing request bodies. + * This is useful for pagination cursor fields that change between requests. + */ + ignoredFields?: string[]; +} + +/** + * Creates a request matcher that validates if the request JSON body exactly matches the expected object + * @param expectedBody - The exact body object to match against + * @param resolver - Response resolver to execute if body matches + * @param options - Optional configuration including fields to ignore + */ +export function withJson( + expectedBody: unknown, + resolver: HttpResponseResolver, + options?: WithJsonOptions, +): HttpResponseResolver { + const ignoredFields = options?.ignoredFields ?? []; + return async (args) => { + const { request } = args; + + let clonedRequest: Request; + let bodyText: string | undefined; + let actualBody: unknown; + try { + clonedRequest = request.clone(); + bodyText = await clonedRequest.text(); + if (bodyText === "") { + console.error("Request body is empty, expected a JSON object."); + return passthrough(); + } + actualBody = fromJson(bodyText); + } catch (error) { + console.error(`Error processing request body:\n\tError: ${error}\n\tBody: ${bodyText}`); + return passthrough(); + } + + const mismatches = findMismatches(actualBody, expectedBody); + const filteredMismatches = Object.keys(mismatches).filter((key) => !ignoredFields.includes(key)); + if (filteredMismatches.length > 0) { + console.error("JSON body mismatch:", toJson(mismatches, undefined, 2)); + return passthrough(); + } + + return resolver(args); + }; +} + +function findMismatches(actual: any, expected: any): Record { + const mismatches: Record = {}; + + if (typeof actual !== typeof expected) { + if (areEquivalent(actual, expected)) { + return {}; + } + return { value: { actual, expected } }; + } + + if (typeof actual !== "object" || actual === null || expected === null) { + if (actual !== expected) { + if (areEquivalent(actual, expected)) { + return {}; + } + return { value: { actual, expected } }; + } + return {}; + } + + if (Array.isArray(actual) && Array.isArray(expected)) { + if (actual.length !== expected.length) { + return { length: { actual: actual.length, expected: expected.length } }; + } + + const arrayMismatches: Record = {}; + for (let i = 0; i < actual.length; i++) { + const itemMismatches = findMismatches(actual[i], expected[i]); + if (Object.keys(itemMismatches).length > 0) { + for (const [mismatchKey, mismatchValue] of Object.entries(itemMismatches)) { + arrayMismatches[`[${i}]${mismatchKey === "value" ? "" : `.${mismatchKey}`}`] = mismatchValue; + } + } + } + return arrayMismatches; + } + + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + + const allKeys = new Set([...actualKeys, ...expectedKeys]); + + for (const key of allKeys) { + if (!expectedKeys.includes(key)) { + if (actual[key] === undefined) { + continue; // Skip undefined values in actual + } + mismatches[key] = { actual: actual[key], expected: undefined }; + } else if (!actualKeys.includes(key)) { + if (expected[key] === undefined) { + continue; // Skip undefined values in expected + } + mismatches[key] = { actual: undefined, expected: expected[key] }; + } else if ( + typeof actual[key] === "object" && + actual[key] !== null && + typeof expected[key] === "object" && + expected[key] !== null + ) { + const nestedMismatches = findMismatches(actual[key], expected[key]); + if (Object.keys(nestedMismatches).length > 0) { + for (const [nestedKey, nestedValue] of Object.entries(nestedMismatches)) { + mismatches[`${key}${nestedKey === "value" ? "" : `.${nestedKey}`}`] = nestedValue; + } + } + } else if (actual[key] !== expected[key]) { + if (areEquivalent(actual[key], expected[key])) { + continue; + } + mismatches[key] = { actual: actual[key], expected: expected[key] }; + } + } + + return mismatches; +} + +function areEquivalent(actual: unknown, expected: unknown): boolean { + if (actual === expected) { + return true; + } + if (isEquivalentBigInt(actual, expected)) { + return true; + } + if (isEquivalentDatetime(actual, expected)) { + return true; + } + return false; +} + +function isEquivalentBigInt(actual: unknown, expected: unknown) { + if (typeof actual === "number") { + actual = BigInt(actual); + } + if (typeof expected === "number") { + expected = BigInt(expected); + } + if (typeof actual === "bigint" && typeof expected === "bigint") { + return actual === expected; + } + return false; +} + +function isEquivalentDatetime(str1: unknown, str2: unknown): boolean { + if (typeof str1 !== "string" || typeof str2 !== "string") { + return false; + } + const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/; + if (!isoDatePattern.test(str1) || !isoDatePattern.test(str2)) { + return false; + } + + try { + const date1 = new Date(str1).getTime(); + const date2 = new Date(str2).getTime(); + return date1 === date2; + } catch { + return false; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/setup.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/setup.ts new file mode 100644 index 000000000000..a5651f81ba10 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/setup.ts @@ -0,0 +1,80 @@ +import { expect } from "vitest"; + +interface CustomMatchers { + toContainHeaders(expectedHeaders: Record): R; +} + +declare module "vitest" { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + +expect.extend({ + toContainHeaders(actual: unknown, expectedHeaders: Record) { + const isHeaders = actual instanceof Headers; + const isPlainObject = typeof actual === "object" && actual !== null && !Array.isArray(actual); + + if (!isHeaders && !isPlainObject) { + throw new TypeError("Received value must be an instance of Headers or a plain object!"); + } + + if (typeof expectedHeaders !== "object" || expectedHeaders === null || Array.isArray(expectedHeaders)) { + throw new TypeError("Expected headers must be a plain object!"); + } + + const missingHeaders: string[] = []; + const mismatchedHeaders: Array<{ key: string; expected: string; actual: string | null }> = []; + + for (const [key, value] of Object.entries(expectedHeaders)) { + let actualValue: string | null = null; + + if (isHeaders) { + // Headers.get() is already case-insensitive + actualValue = (actual as Headers).get(key); + } else { + // For plain objects, do case-insensitive lookup + const actualObj = actual as Record; + const lowerKey = key.toLowerCase(); + const foundKey = Object.keys(actualObj).find((k) => k.toLowerCase() === lowerKey); + actualValue = foundKey ? actualObj[foundKey] : null; + } + + if (actualValue === null || actualValue === undefined) { + missingHeaders.push(key); + } else if (actualValue !== value) { + mismatchedHeaders.push({ key, expected: value, actual: actualValue }); + } + } + + const pass = missingHeaders.length === 0 && mismatchedHeaders.length === 0; + + const actualType = isHeaders ? "Headers" : "object"; + + if (pass) { + return { + message: () => `expected ${actualType} not to contain ${this.utils.printExpected(expectedHeaders)}`, + pass: true, + }; + } else { + const messages: string[] = []; + + if (missingHeaders.length > 0) { + messages.push(`Missing headers: ${this.utils.printExpected(missingHeaders.join(", "))}`); + } + + if (mismatchedHeaders.length > 0) { + const mismatches = mismatchedHeaders.map( + ({ key, expected, actual }) => + `${key}: expected ${this.utils.printExpected(expected)} but got ${this.utils.printReceived(actual)}`, + ); + messages.push(mismatches.join("\n")); + } + + return { + message: () => + `expected ${actualType} to contain ${this.utils.printExpected(expectedHeaders)}\n\n${messages.join("\n")}`, + pass: false, + }; + } + }, +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/tsconfig.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/tsconfig.json new file mode 100644 index 000000000000..ac39744de7b2 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": null, + "rootDir": "..", + "types": ["vitest/globals"] + }, + "include": ["../src", "../tests"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BasicAuth.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BasicAuth.test.ts new file mode 100644 index 000000000000..8c82c1b723db --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BasicAuth.test.ts @@ -0,0 +1,112 @@ +import { BasicAuth } from "../../../src/core/auth/BasicAuth"; + +describe("BasicAuth", () => { + interface ToHeaderTestCase { + description: string; + input: { username?: string; password?: string }; + expected: string | undefined; + } + + interface FromHeaderTestCase { + description: string; + input: string; + expected: { username: string; password: string }; + } + + interface ErrorTestCase { + description: string; + input: string; + expectedError: string; + } + + describe("toAuthorizationHeader", () => { + const toHeaderTests: ToHeaderTestCase[] = [ + { + description: "correctly converts to header with both username and password", + input: { username: "username", password: "password" }, + expected: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + }, + { + description: "encodes username only with trailing colon", + input: { username: "username" }, + expected: "Basic dXNlcm5hbWU6", + }, + { + description: "encodes password only with leading colon", + input: { password: "password" }, + expected: "Basic OnBhc3N3b3Jk", + }, + { + description: "returns undefined when neither provided", + input: {}, + expected: undefined, + }, + { + description: "returns undefined when both are empty strings", + input: { username: "", password: "" }, + expected: undefined, + }, + ]; + + toHeaderTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(BasicAuth.toAuthorizationHeader(input)).toBe(expected); + }); + }); + }); + + describe("fromAuthorizationHeader", () => { + const fromHeaderTests: FromHeaderTestCase[] = [ + { + description: "correctly parses header", + input: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + expected: { username: "username", password: "password" }, + }, + { + description: "handles password with colons", + input: "Basic dXNlcjpwYXNzOndvcmQ=", + expected: { username: "user", password: "pass:word" }, + }, + { + description: "handles empty username and password (just colon)", + input: "Basic Og==", + expected: { username: "", password: "" }, + }, + { + description: "handles empty username", + input: "Basic OnBhc3N3b3Jk", + expected: { username: "", password: "password" }, + }, + { + description: "handles empty password", + input: "Basic dXNlcm5hbWU6", + expected: { username: "username", password: "" }, + }, + ]; + + fromHeaderTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(BasicAuth.fromAuthorizationHeader(input)).toEqual(expected); + }); + }); + + const errorTests: ErrorTestCase[] = [ + { + description: "throws error for completely empty credentials", + input: "Basic ", + expectedError: "Invalid basic auth", + }, + { + description: "throws error for credentials without colon", + input: "Basic dXNlcm5hbWU=", + expectedError: "Invalid basic auth", + }, + ]; + + errorTests.forEach(({ description, input, expectedError }) => { + it(description, () => { + expect(() => BasicAuth.fromAuthorizationHeader(input)).toThrow(expectedError); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BearerToken.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BearerToken.test.ts new file mode 100644 index 000000000000..7757b87cb97e --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/auth/BearerToken.test.ts @@ -0,0 +1,14 @@ +import { BearerToken } from "../../../src/core/auth/BearerToken"; + +describe("BearerToken", () => { + describe("toAuthorizationHeader", () => { + it("correctly converts to header", () => { + expect(BearerToken.toAuthorizationHeader("my-token")).toBe("Bearer my-token"); + }); + }); + describe("fromAuthorizationHeader", () => { + it("correctly parses header", () => { + expect(BearerToken.fromAuthorizationHeader("Bearer my-token")).toBe("my-token"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/base64.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/base64.test.ts new file mode 100644 index 000000000000..939594ca277b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/base64.test.ts @@ -0,0 +1,53 @@ +import { base64Decode, base64Encode } from "../../src/core/base64"; + +describe("base64", () => { + describe("base64Encode", () => { + it("should encode ASCII strings", () => { + expect(base64Encode("hello")).toBe("aGVsbG8="); + expect(base64Encode("")).toBe(""); + }); + + it("should encode UTF-8 strings", () => { + expect(base64Encode("café")).toBe("Y2Fmw6k="); + expect(base64Encode("🎉")).toBe("8J+OiQ=="); + }); + + it("should handle basic auth credentials", () => { + expect(base64Encode("username:password")).toBe("dXNlcm5hbWU6cGFzc3dvcmQ="); + }); + }); + + describe("base64Decode", () => { + it("should decode ASCII strings", () => { + expect(base64Decode("aGVsbG8=")).toBe("hello"); + expect(base64Decode("")).toBe(""); + }); + + it("should decode UTF-8 strings", () => { + expect(base64Decode("Y2Fmw6k=")).toBe("café"); + expect(base64Decode("8J+OiQ==")).toBe("🎉"); + }); + + it("should handle basic auth credentials", () => { + expect(base64Decode("dXNlcm5hbWU6cGFzc3dvcmQ=")).toBe("username:password"); + }); + }); + + describe("round-trip encoding", () => { + const testStrings = [ + "hello world", + "test@example.com", + "café", + "username:password", + "user@domain.com:super$ecret123!", + ]; + + testStrings.forEach((testString) => { + it(`should round-trip encode/decode: "${testString}"`, () => { + const encoded = base64Encode(testString); + const decoded = base64Decode(encoded); + expect(decoded).toBe(testString); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/Fetcher.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/Fetcher.test.ts new file mode 100644 index 000000000000..6c17624228bb --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/Fetcher.test.ts @@ -0,0 +1,262 @@ +import fs from "fs"; +import { join } from "path"; +import stream from "stream"; +import type { BinaryResponse } from "../../../src/core"; +import { type Fetcher, fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +describe("Test fetcherImpl", () => { + it("should handle successful request", async () => { + const mockArgs: Fetcher.Args = { + url: "https://httpbin.org/post", + method: "POST", + headers: { "X-Test": "x-test-header" }, + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + maxRetries: 0, + responseType: "json", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + }), + ); + + const result = await fetcherImpl(mockArgs); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.body).toEqual({ data: "test" }); + } + + expect(global.fetch).toHaveBeenCalledWith( + "https://httpbin.org/post", + expect.objectContaining({ + method: "POST", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + body: JSON.stringify({ data: "test" }), + }), + ); + }); + + it("should send octet stream", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "POST", + headers: { "X-Test": "x-test-header" }, + contentType: "application/octet-stream", + requestType: "bytes", + maxRetries: 0, + responseType: "json", + body: fs.createReadStream(join(__dirname, "test-file.txt")), + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + }), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "POST", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + body: expect.any(fs.ReadStream), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.body).toEqual({ data: "test" }); + } + }); + + it("should receive file as stream", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.stream).toBe("function"); + const stream = body.stream(); + expect(stream).toBeInstanceOf(ReadableStream); + const readableStream = stream as ReadableStream; + const reader = readableStream.getReader(); + const { value } = await reader.read(); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(value); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); + + it("should receive file as blob", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.blob).toBe("function"); + const blob = await body.blob(); + expect(blob).toBeInstanceOf(Blob); + const reader = blob.stream().getReader(); + const { value } = await reader.read(); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(value); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); + + it("should receive file as arraybuffer", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.arrayBuffer).toBe("function"); + const arrayBuffer = await body.arrayBuffer(); + expect(arrayBuffer).toBeInstanceOf(ArrayBuffer); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(new Uint8Array(arrayBuffer)); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); + + it("should receive file as bytes", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.bytes).toBe("function"); + if (!body.bytes) { + return; + } + const bytes = await body.bytes(); + expect(bytes).toBeInstanceOf(Uint8Array); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(bytes); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/HttpResponsePromise.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/HttpResponsePromise.test.ts new file mode 100644 index 000000000000..2ec008e581d8 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/HttpResponsePromise.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { HttpResponsePromise } from "../../../src/core/fetcher/HttpResponsePromise"; +import type { RawResponse, WithRawResponse } from "../../../src/core/fetcher/RawResponse"; + +describe("HttpResponsePromise", () => { + const mockRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 200, + statusText: "OK", + type: "basic" as ResponseType, + url: "https://example.com", + }; + const mockData = { id: "123", name: "test" }; + const mockWithRawResponse: WithRawResponse = { + data: mockData, + rawResponse: mockRawResponse, + }; + + describe("fromFunction", () => { + it("should create an HttpResponsePromise from a function", async () => { + const mockFn = vi + .fn<(arg1: string, arg2: string) => Promise>>() + .mockResolvedValue(mockWithRawResponse); + + const responsePromise = HttpResponsePromise.fromFunction(mockFn, "arg1", "arg2"); + + const result = await responsePromise; + expect(result).toEqual(mockData); + expect(mockFn).toHaveBeenCalledWith("arg1", "arg2"); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("fromPromise", () => { + it("should create an HttpResponsePromise from a promise", async () => { + const promise = Promise.resolve(mockWithRawResponse); + + const responsePromise = HttpResponsePromise.fromPromise(promise); + + const result = await responsePromise; + expect(result).toEqual(mockData); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("fromExecutor", () => { + it("should create an HttpResponsePromise from an executor function", async () => { + const responsePromise = HttpResponsePromise.fromExecutor((resolve) => { + resolve(mockWithRawResponse); + }); + + const result = await responsePromise; + expect(result).toEqual(mockData); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("fromResult", () => { + it("should create an HttpResponsePromise from a result", async () => { + const responsePromise = HttpResponsePromise.fromResult(mockWithRawResponse); + + const result = await responsePromise; + expect(result).toEqual(mockData); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("Promise methods", () => { + let responsePromise: HttpResponsePromise; + + beforeEach(() => { + responsePromise = HttpResponsePromise.fromResult(mockWithRawResponse); + }); + + it("should support then() method", async () => { + const result = await responsePromise.then((data) => ({ + ...data, + modified: true, + })); + + expect(result).toEqual({ + ...mockData, + modified: true, + }); + }); + + it("should support catch() method", async () => { + const errorResponsePromise = HttpResponsePromise.fromExecutor((_, reject) => { + reject(new Error("Test error")); + }); + + const catchSpy = vi.fn(); + await errorResponsePromise.catch(catchSpy); + + expect(catchSpy).toHaveBeenCalled(); + const error = catchSpy.mock.calls[0]?.[0]; + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toBe("Test error"); + }); + + it("should support finally() method", async () => { + const finallySpy = vi.fn(); + await responsePromise.finally(finallySpy); + + expect(finallySpy).toHaveBeenCalled(); + }); + }); + + describe("withRawResponse", () => { + it("should return both data and raw response", async () => { + const responsePromise = HttpResponsePromise.fromResult(mockWithRawResponse); + + const result = await responsePromise.withRawResponse(); + + expect(result).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/RawResponse.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/RawResponse.test.ts new file mode 100644 index 000000000000..375ee3f38064 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/RawResponse.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { toRawResponse } from "../../../src/core/fetcher/RawResponse"; + +describe("RawResponse", () => { + describe("toRawResponse", () => { + it("should convert Response to RawResponse by removing body, bodyUsed, and ok properties", () => { + const mockHeaders = new Headers({ "content-type": "application/json" }); + const mockResponse = { + body: "test body", + bodyUsed: false, + ok: true, + headers: mockHeaders, + redirected: false, + status: 200, + statusText: "OK", + type: "basic" as ResponseType, + url: "https://example.com", + }; + + const result = toRawResponse(mockResponse as unknown as Response); + + expect("body" in result).toBe(false); + expect("bodyUsed" in result).toBe(false); + expect("ok" in result).toBe(false); + expect(result.headers).toBe(mockHeaders); + expect(result.redirected).toBe(false); + expect(result.status).toBe(200); + expect(result.statusText).toBe("OK"); + expect(result.type).toBe("basic"); + expect(result.url).toBe("https://example.com"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/createRequestUrl.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/createRequestUrl.test.ts new file mode 100644 index 000000000000..7787e5530fcd --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/createRequestUrl.test.ts @@ -0,0 +1,167 @@ +import { createRequestUrl } from "../../../src/core/fetcher/createRequestUrl"; + +describe("Test createRequestUrl", () => { + const BASE_URL = "https://api.example.com"; + + interface TestCase { + description: string; + baseUrl: string; + queryParams?: Record; + expected: string; + } + + const testCases: TestCase[] = [ + { + description: "should return the base URL when no query parameters are provided", + baseUrl: BASE_URL, + expected: BASE_URL, + }, + { + description: "should append simple query parameters", + baseUrl: BASE_URL, + queryParams: { key: "value", another: "param" }, + expected: "https://api.example.com?key=value&another=param", + }, + { + description: "should handle array query parameters", + baseUrl: BASE_URL, + queryParams: { items: ["a", "b", "c"] }, + expected: "https://api.example.com?items=a&items=b&items=c", + }, + { + description: "should handle object query parameters", + baseUrl: BASE_URL, + queryParams: { filter: { name: "John", age: 30 } }, + expected: "https://api.example.com?filter%5Bname%5D=John&filter%5Bage%5D=30", + }, + { + description: "should handle mixed types of query parameters", + baseUrl: BASE_URL, + queryParams: { + simple: "value", + array: ["x", "y"], + object: { key: "value" }, + }, + expected: "https://api.example.com?simple=value&array=x&array=y&object%5Bkey%5D=value", + }, + { + description: "should handle empty query parameters object", + baseUrl: BASE_URL, + queryParams: {}, + expected: BASE_URL, + }, + { + description: "should encode special characters in query parameters", + baseUrl: BASE_URL, + queryParams: { special: "a&b=c d" }, + expected: "https://api.example.com?special=a%26b%3Dc%20d", + }, + { + description: "should handle numeric values", + baseUrl: BASE_URL, + queryParams: { count: 42, price: 19.99, active: 1, inactive: 0 }, + expected: "https://api.example.com?count=42&price=19.99&active=1&inactive=0", + }, + { + description: "should handle boolean values", + baseUrl: BASE_URL, + queryParams: { enabled: true, disabled: false }, + expected: "https://api.example.com?enabled=true&disabled=false", + }, + { + description: "should handle null and undefined values", + baseUrl: BASE_URL, + queryParams: { + valid: "value", + nullValue: null, + undefinedValue: undefined, + emptyString: "", + }, + expected: "https://api.example.com?valid=value&emptyString=", + }, + { + description: "should handle deeply nested objects", + baseUrl: BASE_URL, + queryParams: { + user: { + profile: { + name: "John", + settings: { theme: "dark" }, + }, + }, + }, + expected: + "https://api.example.com?user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", + }, + { + description: "should handle arrays of objects", + baseUrl: BASE_URL, + queryParams: { + users: [ + { name: "John", age: 30 }, + { name: "Jane", age: 25 }, + ], + }, + expected: + "https://api.example.com?users%5Bname%5D=John&users%5Bage%5D=30&users%5Bname%5D=Jane&users%5Bage%5D=25", + }, + { + description: "should handle mixed arrays", + baseUrl: BASE_URL, + queryParams: { + mixed: ["string", 42, true, { key: "value" }], + }, + expected: "https://api.example.com?mixed=string&mixed=42&mixed=true&mixed%5Bkey%5D=value", + }, + { + description: "should handle empty arrays", + baseUrl: BASE_URL, + queryParams: { emptyArray: [] }, + expected: BASE_URL, + }, + { + description: "should handle empty objects", + baseUrl: BASE_URL, + queryParams: { emptyObject: {} }, + expected: BASE_URL, + }, + { + description: "should handle special characters in keys", + baseUrl: BASE_URL, + queryParams: { "key with spaces": "value", "key[with]brackets": "value" }, + expected: "https://api.example.com?key%20with%20spaces=value&key%5Bwith%5Dbrackets=value", + }, + { + description: "should handle URL with existing query parameters", + baseUrl: "https://api.example.com?existing=param", + queryParams: { new: "value" }, + expected: "https://api.example.com?existing=param?new=value", + }, + { + description: "should handle complex nested structures", + baseUrl: BASE_URL, + queryParams: { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], + }, + }, + sort: { field: "name", direction: "asc" }, + }, + expected: + "https://api.example.com?filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + }, + ]; + + testCases.forEach(({ description, baseUrl, queryParams, expected }) => { + it(description, () => { + expect(createRequestUrl(baseUrl, queryParams)).toBe(expected); + }); + }); + + it("should default to repeat format for arrays", () => { + expect(createRequestUrl(BASE_URL, { items: ["a", "b"] })).toBe("https://api.example.com?items=a&items=b"); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getRequestBody.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getRequestBody.test.ts new file mode 100644 index 000000000000..8a6c3a57e211 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getRequestBody.test.ts @@ -0,0 +1,129 @@ +import { getRequestBody } from "../../../src/core/fetcher/getRequestBody"; +import { RUNTIME } from "../../../src/core/runtime"; + +describe("Test getRequestBody", () => { + interface TestCase { + description: string; + input: any; + type: "json" | "form" | "file" | "bytes" | "other"; + expected: any; + skipCondition?: () => boolean; + } + + const testCases: TestCase[] = [ + { + description: "should stringify body if not FormData in Node environment", + input: { key: "value" }, + type: "json", + expected: '{"key":"value"}', + skipCondition: () => RUNTIME.type !== "node", + }, + { + description: "should stringify body if not FormData in browser environment", + input: { key: "value" }, + type: "json", + expected: '{"key":"value"}', + skipCondition: () => RUNTIME.type !== "browser", + }, + { + description: "should return the Uint8Array", + input: new Uint8Array([1, 2, 3]), + type: "bytes", + expected: new Uint8Array([1, 2, 3]), + }, + { + description: "should serialize objects for form-urlencoded content type", + input: { username: "johndoe", email: "john@example.com" }, + type: "form", + expected: "username=johndoe&email=john%40example.com", + }, + { + description: "should serialize complex nested objects and arrays for form-urlencoded content type", + input: { + user: { + profile: { + name: "John Doe", + settings: { + theme: "dark", + notifications: true, + }, + }, + tags: ["admin", "user"], + contacts: [ + { type: "email", value: "john@example.com" }, + { type: "phone", value: "+1234567890" }, + ], + }, + filters: { + status: ["active", "pending"], + metadata: { + created: "2024-01-01", + categories: ["electronics", "books"], + }, + }, + preferences: ["notifications", "updates"], + }, + type: "form", + expected: + "user%5Bprofile%5D%5Bname%5D=John%20Doe&" + + "user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark&" + + "user%5Bprofile%5D%5Bsettings%5D%5Bnotifications%5D=true&" + + "user%5Btags%5D=admin&" + + "user%5Btags%5D=user&" + + "user%5Bcontacts%5D%5Btype%5D=email&" + + "user%5Bcontacts%5D%5Bvalue%5D=john%40example.com&" + + "user%5Bcontacts%5D%5Btype%5D=phone&" + + "user%5Bcontacts%5D%5Bvalue%5D=%2B1234567890&" + + "filters%5Bstatus%5D=active&" + + "filters%5Bstatus%5D=pending&" + + "filters%5Bmetadata%5D%5Bcreated%5D=2024-01-01&" + + "filters%5Bmetadata%5D%5Bcategories%5D=electronics&" + + "filters%5Bmetadata%5D%5Bcategories%5D=books&" + + "preferences=notifications&" + + "preferences=updates", + }, + { + description: "should return the input for pre-serialized form-urlencoded strings", + input: "key=value&another=param", + type: "other", + expected: "key=value&another=param", + }, + { + description: "should JSON stringify objects", + input: { key: "value" }, + type: "json", + expected: '{"key":"value"}', + }, + ]; + + testCases.forEach(({ description, input, type, expected, skipCondition }) => { + it(description, async () => { + if (skipCondition?.()) { + return; + } + + const result = await getRequestBody({ + body: input, + type, + }); + + if (input instanceof Uint8Array) { + expect(result).toBe(input); + } else { + expect(result).toBe(expected); + } + }); + }); + + it("should return FormData in browser environment", async () => { + if (RUNTIME.type === "browser") { + const formData = new FormData(); + formData.append("key", "value"); + const result = await getRequestBody({ + body: formData, + type: "file", + }); + expect(result).toBe(formData); + } + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getResponseBody.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getResponseBody.test.ts new file mode 100644 index 000000000000..64ed7461ec3a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/getResponseBody.test.ts @@ -0,0 +1,123 @@ +import { getResponseBody } from "../../../src/core/fetcher/getResponseBody"; + +import { RUNTIME } from "../../../src/core/runtime"; + +describe("Test getResponseBody", () => { + interface SimpleTestCase { + description: string; + responseData: string | Record; + responseType?: "blob" | "sse" | "streaming" | "text"; + expected: any; + skipCondition?: () => boolean; + } + + const simpleTestCases: SimpleTestCase[] = [ + { + description: "should handle text response type", + responseData: "test text", + responseType: "text", + expected: "test text", + }, + { + description: "should handle JSON response", + responseData: { key: "value" }, + expected: { key: "value" }, + }, + { + description: "should handle empty response", + responseData: "", + expected: undefined, + }, + { + description: "should handle non-JSON response", + responseData: "invalid json", + expected: { + ok: false, + error: { + reason: "non-json", + statusCode: 200, + rawBody: "invalid json", + }, + }, + }, + ]; + + simpleTestCases.forEach(({ description, responseData, responseType, expected, skipCondition }) => { + it(description, async () => { + if (skipCondition?.()) { + return; + } + + const mockResponse = new Response( + typeof responseData === "string" ? responseData : JSON.stringify(responseData), + ); + const result = await getResponseBody(mockResponse, responseType); + expect(result).toEqual(expected); + }); + }); + + it("should handle blob response type", async () => { + const mockBlob = new Blob(["test"], { type: "text/plain" }); + const mockResponse = new Response(mockBlob); + const result = await getResponseBody(mockResponse, "blob"); + // @ts-expect-error + expect(result.constructor.name).toBe("Blob"); + }); + + it("should handle sse response type", async () => { + if (RUNTIME.type === "node") { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = await getResponseBody(mockResponse, "sse"); + expect(result).toBe(mockStream); + } + }); + + it("should retain a reference to the parent Response for sse responses", async () => { + if (RUNTIME.type === "node") { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = (await getResponseBody(mockResponse, "sse")) as ReadableStream & { + __fern_response_ref?: Response; + }; + // Pins the parent Response so undici's FinalizationRegistry can't GC it and cancel the stream. + expect(result.__fern_response_ref).toBe(mockResponse); + // The pin must be non-enumerable so it does not leak through JSON.stringify or Object.keys. + const descriptor = Object.getOwnPropertyDescriptor(result, "__fern_response_ref"); + expect(descriptor?.enumerable).toBe(false); + } + }); + + it("should handle streaming response type", async () => { + const encoder = new TextEncoder(); + const testData = "test stream data"; + const mockStream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(testData)); + controller.close(); + }, + }); + + const mockResponse = new Response(mockStream); + const result = (await getResponseBody(mockResponse, "streaming")) as ReadableStream; + + expect(result).toBeInstanceOf(ReadableStream); + + const reader = result.getReader(); + const decoder = new TextDecoder(); + const { value } = await reader.read(); + const streamContent = decoder.decode(value); + expect(streamContent).toBe(testData); + }); + + it("should retain a reference to the parent Response for streaming responses", async () => { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = (await getResponseBody(mockResponse, "streaming")) as ReadableStream & { + __fern_response_ref?: Response; + }; + expect(result.__fern_response_ref).toBe(mockResponse); + const descriptor = Object.getOwnPropertyDescriptor(result, "__fern_response_ref"); + expect(descriptor?.enumerable).toBe(false); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/logging.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/logging.test.ts new file mode 100644 index 000000000000..366c9b6ced61 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/logging.test.ts @@ -0,0 +1,517 @@ +import { fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +function createMockLogger() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +function mockSuccessResponse(data: unknown = { data: "test" }, status = 200, statusText = "OK") { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(data), { + status, + statusText, + }), + ); +} + +function mockErrorResponse(data: unknown = { error: "Error" }, status = 404, statusText = "Not Found") { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(data), { + status, + statusText, + }), + ); +} + +describe("Fetcher Logging Integration", () => { + describe("Request Logging", () => { + it("should log successful request at debug level", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + headers: { "Content-Type": "application/json" }, + body: { test: "data" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "POST", + url: "https://example.com/api", + headers: expect.toContainHeaders({ + "Content-Type": "application/json", + }), + hasBody: true, + }), + ); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + method: "POST", + url: "https://example.com/api", + statusCode: 200, + }), + ); + }); + + it("should not log debug messages at info level for successful requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "info", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + }); + + it("should log request with body flag", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + hasBody: true, + }), + ); + }); + + it("should log request without body flag", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + hasBody: false, + }), + ); + }); + + it("should not log when silent mode is enabled", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: true, + }, + }); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it("should not log when no logging config is provided", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + }); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + }); + }); + + describe("Error Logging", () => { + it("should log 4xx errors at error level", async () => { + const mockLogger = createMockLogger(); + mockErrorResponse({ error: "Not found" }, 404, "Not Found"); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + statusCode: 404, + }), + ); + }); + + it("should log 5xx errors at error level", async () => { + const mockLogger = createMockLogger(); + mockErrorResponse({ error: "Internal error" }, 500, "Internal Server Error"); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + statusCode: 500, + }), + ); + }); + + it("should log aborted request errors", async () => { + const mockLogger = createMockLogger(); + + const abortController = new AbortController(); + abortController.abort(); + + global.fetch = vi.fn().mockRejectedValue(new Error("Aborted")); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + abortSignal: abortController.signal, + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request was aborted", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + }), + ); + }); + + it("should log timeout errors", async () => { + const mockLogger = createMockLogger(); + + const timeoutError = new Error("Request timeout"); + timeoutError.name = "AbortError"; + + global.fetch = vi.fn().mockRejectedValue(timeoutError); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request timed out", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + timeoutMs: undefined, + }), + ); + }); + + it("should log unknown errors", async () => { + const mockLogger = createMockLogger(); + + const unknownError = new Error("Unknown error"); + + global.fetch = vi.fn().mockRejectedValue(unknownError); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + errorMessage: "Unknown error", + }), + ); + }); + }); + + describe("Logging with Redaction", () => { + it("should redact sensitive data in error logs", async () => { + const mockLogger = createMockLogger(); + mockErrorResponse({ error: "Unauthorized" }, 401, "Unauthorized"); + + await fetcherImpl({ + url: "https://example.com/api?api_key=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]", + }), + ); + }); + }); + + describe("Different HTTP Methods", () => { + it("should log GET requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "GET", + }), + ); + }); + + it("should log POST requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse({ data: "test" }, 201, "Created"); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "POST", + }), + ); + }); + + it("should log PUT requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "PUT", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "PUT", + }), + ); + }); + + it("should log DELETE requests", async () => { + const mockLogger = createMockLogger(); + global.fetch = vi.fn().mockResolvedValue( + new Response(null, { + status: 200, + statusText: "OK", + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "DELETE", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "DELETE", + }), + ); + }); + }); + + describe("Status Code Logging", () => { + it("should log 2xx success status codes", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse({ data: "test" }, 201, "Created"); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + statusCode: 201, + }), + ); + }); + + it("should log 3xx redirect status codes as success", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse({ data: "test" }, 301, "Moved Permanently"); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + statusCode: 301, + }), + ); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..07cb846739c2 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,504 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + baseUrl: "https://api.example.com", + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("auth header origin scoping", () => { + it("should attach auth headers to relative paths resolved against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should attach auth headers to same-origin absolute URLs", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should attach auth headers when the absolute URL matches the environment origin", async () => { + await makePassthroughRequest("https://env.example.com/v1/users", undefined, { + environment: "https://env.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should NOT attach auth headers to a cross-origin absolute URL", async () => { + await makePassthroughRequest("https://evil.example.com/steal", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should NOT attach auth headers to a cross-origin URL differing only by port", async () => { + await makePassthroughRequest("https://api.example.com:9999/steal", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should NOT attach auth headers when no baseUrl or environment is configured", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should still allow explicit init headers on cross-origin requests", async () => { + await makePassthroughRequest( + "https://evil.example.com/steal", + { headers: { "X-Custom": "keep-me" } }, + { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + expect(calledOptions.headers["x-custom"]).toBe("keep-me"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 502 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); + + describe("debug logging", () => { + it("should redact credentials in the logged request URL", async () => { + const mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + await makePassthroughRequest("https://user:password@api.example.com/v1/users?token=secret", undefined, { + fetch: mockFetch, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + const loggedUrls = mockLogger.debug.mock.calls.map(([, meta]) => (meta as { url: string }).url); + expect(loggedUrls.length).toBeGreaterThan(0); + for (const loggedUrl of loggedUrls) { + expect(loggedUrl).toBe("https://[REDACTED]@api.example.com/v1/users?token=[REDACTED]"); + expect(loggedUrl).not.toContain("password"); + expect(loggedUrl).not.toContain("secret"); + } + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makeRequest.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makeRequest.test.ts new file mode 100644 index 000000000000..bde194554dd8 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/makeRequest.test.ts @@ -0,0 +1,158 @@ +import type { Mock } from "vitest"; +import { + isCacheNoStoreSupported, + makeRequest, + resetCacheNoStoreSupported, +} from "../../../src/core/fetcher/makeRequest"; + +describe("Test makeRequest", () => { + const mockPostUrl = "https://httpbin.org/post"; + const mockGetUrl = "https://httpbin.org/get"; + const mockHeaders = { "Content-Type": "application/json" }; + const mockBody = JSON.stringify({ key: "value" }); + + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ test: "successful" }), { status: 200 })); + resetCacheNoStoreSupported(); + }); + + it("should handle POST request correctly", async () => { + const response = await makeRequest(mockFetch, mockPostUrl, "POST", mockHeaders, mockBody); + const responseBody = await response.json(); + expect(responseBody).toEqual({ test: "successful" }); + expect(mockFetch).toHaveBeenCalledTimes(1); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe(mockPostUrl); + expect(calledOptions).toEqual( + expect.objectContaining({ + method: "POST", + headers: mockHeaders, + body: mockBody, + credentials: undefined, + }), + ); + expect(calledOptions.signal).toBeDefined(); + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it("should handle GET request correctly", async () => { + const response = await makeRequest(mockFetch, mockGetUrl, "GET", mockHeaders, undefined); + const responseBody = await response.json(); + expect(responseBody).toEqual({ test: "successful" }); + expect(mockFetch).toHaveBeenCalledTimes(1); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe(mockGetUrl); + expect(calledOptions).toEqual( + expect.objectContaining({ + method: "GET", + headers: mockHeaders, + body: undefined, + credentials: undefined, + }), + ); + expect(calledOptions.signal).toBeDefined(); + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it("should not include cache option when disableCache is not set", async () => { + await makeRequest(mockFetch, mockGetUrl, "GET", mockHeaders, undefined); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBeUndefined(); + }); + + it("should not include cache option when disableCache is false", async () => { + await makeRequest( + mockFetch, + mockGetUrl, + "GET", + mockHeaders, + undefined, + undefined, + undefined, + undefined, + undefined, + false, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBeUndefined(); + }); + + it("should include cache: no-store when disableCache is true and runtime supports it", async () => { + // In Node.js test environment, Request supports the cache option + expect(isCacheNoStoreSupported()).toBe(true); + await makeRequest( + mockFetch, + mockGetUrl, + "GET", + mockHeaders, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBe("no-store"); + }); + + it("should cache the result of isCacheNoStoreSupported", () => { + const first = isCacheNoStoreSupported(); + const second = isCacheNoStoreSupported(); + expect(first).toBe(second); + }); + + it("should reset cache detection state with resetCacheNoStoreSupported", () => { + // First call caches the result + const first = isCacheNoStoreSupported(); + expect(first).toBe(true); + + // Reset clears the cache + resetCacheNoStoreSupported(); + + // After reset, it should re-detect (and still return true in Node.js) + const second = isCacheNoStoreSupported(); + expect(second).toBe(true); + }); + + it("should not include cache option when runtime does not support it (e.g. Cloudflare Workers)", async () => { + // Mock Request constructor to throw when cache option is passed, + // simulating runtimes like Cloudflare Workers + const OriginalRequest = globalThis.Request; + globalThis.Request = class MockRequest { + constructor(_url: string, init?: RequestInit) { + if (init?.cache != null) { + throw new TypeError("The 'cache' field on 'RequestInitializerDict' is not implemented."); + } + } + } as unknown as typeof Request; + + try { + // Reset so the detection runs fresh with the mocked Request + resetCacheNoStoreSupported(); + expect(isCacheNoStoreSupported()).toBe(false); + + await makeRequest( + mockFetch, + mockGetUrl, + "GET", + mockHeaders, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBeUndefined(); + } finally { + // Restore original Request + globalThis.Request = OriginalRequest; + resetCacheNoStoreSupported(); + } + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/redacting.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/redacting.test.ts new file mode 100644 index 000000000000..685f1ddafd40 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/redacting.test.ts @@ -0,0 +1,1221 @@ +import { fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +function createMockLogger() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +function mockSuccessResponse(data: unknown = { data: "test" }, status = 200, statusText = "OK") { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(data), { + status, + statusText, + }), + ); +} + +describe("Redacting Logic", () => { + describe("Header Redaction", () => { + it("should redact authorization header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { Authorization: "Bearer secret-token-12345" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + Authorization: "[REDACTED]", + }), + }), + ); + }); + + it("should redact api-key header (case-insensitive)", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "X-API-KEY": "secret-api-key" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "X-API-KEY": "[REDACTED]", + }), + }), + ); + }); + + it("should redact cookie header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { Cookie: "session=abc123; token=xyz789" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + Cookie: "[REDACTED]", + }), + }), + ); + }); + + it("should redact x-auth-token header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "x-auth-token": "auth-token-12345" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "x-auth-token": "[REDACTED]", + }), + }), + ); + }); + + it("should redact proxy-authorization header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "Proxy-Authorization": "Basic credentials" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "Proxy-Authorization": "[REDACTED]", + }), + }), + ); + }); + + it("should redact x-csrf-token header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "X-CSRF-Token": "csrf-token-abc" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "X-CSRF-Token": "[REDACTED]", + }), + }), + ); + }); + + it("should redact www-authenticate header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "WWW-Authenticate": "Bearer realm=example" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "WWW-Authenticate": "[REDACTED]", + }), + }), + ); + }); + + it("should redact x-session-token header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "X-Session-Token": "session-token-xyz" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "X-Session-Token": "[REDACTED]", + }), + }), + ); + }); + + it("should not redact non-sensitive headers", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { + "Content-Type": "application/json", + "User-Agent": "Test/1.0", + Accept: "application/json", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "Content-Type": "application/json", + "User-Agent": "Test/1.0", + Accept: "application/json", + }), + }), + ); + }); + + it("should redact multiple sensitive headers at once", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { + Authorization: "Bearer token", + "X-API-Key": "api-key", + Cookie: "session=123", + "Content-Type": "application/json", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + Authorization: "[REDACTED]", + "X-API-Key": "[REDACTED]", + Cookie: "[REDACTED]", + "Content-Type": "application/json", + }), + }), + ); + }); + }); + + describe("Response Header Redaction", () => { + it("should redact Set-Cookie in response headers", async () => { + const mockLogger = createMockLogger(); + + const mockHeaders = new Headers(); + mockHeaders.set("Set-Cookie", "session=abc123; HttpOnly; Secure"); + mockHeaders.set("Content-Type", "application/json"); + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + headers: mockHeaders, + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + responseHeaders: expect.toContainHeaders({ + "set-cookie": "[REDACTED]", + "content-type": "application/json", + }), + }), + ); + }); + + it("should redact authorization in response headers", async () => { + const mockLogger = createMockLogger(); + + const mockHeaders = new Headers(); + mockHeaders.set("Authorization", "Bearer token-123"); + mockHeaders.set("Content-Type", "application/json"); + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + headers: mockHeaders, + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + responseHeaders: expect.toContainHeaders({ + authorization: "[REDACTED]", + "content-type": "application/json", + }), + }), + ); + }); + + it("should redact response headers in error responses", async () => { + const mockLogger = createMockLogger(); + + const mockHeaders = new Headers(); + mockHeaders.set("WWW-Authenticate", "Bearer realm=example"); + mockHeaders.set("Content-Type", "application/json"); + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + statusText: "Unauthorized", + headers: mockHeaders, + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + responseHeaders: expect.toContainHeaders({ + "www-authenticate": "[REDACTED]", + "content-type": "application/json", + }), + }), + ); + }); + }); + + describe("Query Parameter Redaction", () => { + it("should redact api_key query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { api_key: "secret-key" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + api_key: "[REDACTED]", + }), + }), + ); + }); + + it("should redact token query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { token: "secret-token" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + token: "[REDACTED]", + }), + }), + ); + }); + + it("should redact access_token query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { access_token: "secret-access-token" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + access_token: "[REDACTED]", + }), + }), + ); + }); + + it("should redact password query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { password: "secret-password" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + password: "[REDACTED]", + }), + }), + ); + }); + + it("should redact secret query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { secret: "secret-value" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + secret: "[REDACTED]", + }), + }), + ); + }); + + it("should redact session_id query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { session_id: "session-123" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + session_id: "[REDACTED]", + }), + }), + ); + }); + + it("should not redact non-sensitive query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { + page: "1", + limit: "10", + sort: "name", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + page: "1", + limit: "10", + sort: "name", + }), + }), + ); + }); + + it("should not redact parameters containing 'auth' substring like 'author'", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { + author: "john", + authenticate: "false", + authorization_level: "user", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + author: "john", + authenticate: "false", + authorization_level: "user", + }), + }), + ); + }); + + it("should handle undefined query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: undefined, + }), + ); + }); + + it("should redact case-insensitive query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { API_KEY: "secret-key", Token: "secret-token" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + API_KEY: "[REDACTED]", + Token: "[REDACTED]", + }), + }), + ); + }); + }); + + describe("Query String Redaction", () => { + it("should redact api_key in queryString via URL", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "api_key=secret-key&page=1", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]&page=1", + }), + ); + }); + + it("should redact multiple sensitive params in queryString", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "token=t&password=p&page=1", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?token=[REDACTED]&password=[REDACTED]&page=1", + }), + ); + }); + + it("should not redact non-sensitive params in queryString", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "page=1&limit=10&sort=name", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1&limit=10&sort=name", + }), + ); + }); + + it("should prefer queryString over queryParameters when both provided", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "page=1", + queryParameters: { api_key: "secret-key" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1", + queryParameters: expect.objectContaining({ + api_key: "[REDACTED]", + }), + }), + ); + }); + }); + + describe("URL Redaction", () => { + it("should redact credentials in URL", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user:password@example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@example.com/api", + }), + ); + }); + + it("should redact api_key in query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?api_key=secret-key&page=1", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]&page=1", + }), + ); + }); + + it("should redact token in query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?token=secret-token", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?token=[REDACTED]", + }), + ); + }); + + it("should redact password in query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?username=user&password=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?username=user&password=[REDACTED]", + }), + ); + }); + + it("should not redact non-sensitive query strings", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?page=1&limit=10&sort=name", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1&limit=10&sort=name", + }), + ); + }); + + it("should not redact URL parameters containing 'auth' substring like 'author'", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?author=john&authenticate=false&page=1", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?author=john&authenticate=false&page=1", + }), + ); + }); + + it("should handle URL with fragment", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?token=secret#section", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?token=[REDACTED]#section", + }), + ); + }); + + it("should redact URL-encoded query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?api%5Fkey=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api%5Fkey=[REDACTED]", + }), + ); + }); + + it("should handle URL without query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api", + }), + ); + }); + + it("should handle empty query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?", + }), + ); + }); + + it("should redact multiple sensitive parameters in URL", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?api_key=secret1&token=secret2&page=1", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]&token=[REDACTED]&page=1", + }), + ); + }); + + it("should redact both credentials and query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user:pass@example.com/api?token=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@example.com/api?token=[REDACTED]", + }), + ); + }); + + it("should use fast path for URLs without sensitive keywords", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?page=1&limit=10&sort=name&filter=value", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1&limit=10&sort=name&filter=value", + }), + ); + }); + + it("should handle query parameter without value", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?flag&token=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?flag&token=[REDACTED]", + }), + ); + }); + + it("should handle URL with multiple @ symbols in credentials", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user@example.com:pass@host.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@host.com/api", + }), + ); + }); + + it("should handle URL with @ in query parameter but not in credentials", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?email=user@example.com", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?email=user@example.com", + }), + ); + }); + + it("should handle URL with both credentials and @ in path", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user:pass@example.com/users/@username", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@example.com/users/@username", + }), + ); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/requestWithRetries.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/requestWithRetries.test.ts new file mode 100644 index 000000000000..7c98c0abfad1 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/requestWithRetries.test.ts @@ -0,0 +1,282 @@ +import type { Mock, MockInstance } from "vitest"; +import { requestWithRetries } from "../../../src/core/fetcher/requestWithRetries"; + +describe("requestWithRetries", () => { + let mockFetch: Mock; + let originalMathRandom: typeof Math.random; + let setTimeoutSpy: MockInstance; + + beforeEach(() => { + mockFetch = vi.fn(); + originalMathRandom = Math.random; + + Math.random = vi.fn(() => 0.5); + + vi.useFakeTimers({ + toFake: [ + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "setImmediate", + "clearImmediate", + "Date", + "performance", + "requestAnimationFrame", + "cancelAnimationFrame", + "requestIdleCallback", + "cancelIdleCallback", + ], + }); + }); + + afterEach(() => { + Math.random = originalMathRandom; + vi.clearAllMocks(); + vi.clearAllTimers(); + }); + + it("should retry on retryable status codes (legacy mode)", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const retryableStatuses = [408, 429, 500, 501, 502, 503, 504, 505]; + let callCount = 0; + + mockFetch.mockImplementation(async () => { + if (callCount < retryableStatuses.length) { + return new Response("", { status: retryableStatuses[callCount++] }); + } + return new Response("", { status: 200 }); + }); + + const responsePromise = requestWithRetries(() => mockFetch(), retryableStatuses.length); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(retryableStatuses.length + 1); + expect(response.status).toBe(200); + }); + + it("should retry on 500 Internal Server Error in legacy mode", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce(new Response("", { status: 500 })) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(2); + expect(response.status).toBe(200); + }); + + it("should respect maxRetries limit", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const maxRetries = 2; + mockFetch.mockResolvedValue(new Response("", { status: 503 })); + + const responsePromise = requestWithRetries(() => mockFetch(), maxRetries); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); + expect(response.status).toBe(503); + }); + + it("should retry on status 599 (upper boundary of retryable 5xx in legacy mode)", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce(new Response("", { status: 599 })) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(2); + expect(response.status).toBe(200); + }); + + it("should not retry on non-retryable client error (400)", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch.mockResolvedValueOnce(new Response("", { status: 400 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(response.status).toBe(400); + }); + + it("should not retry on success status codes", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const successStatuses = [200, 201, 202]; + + for (const status of successStatuses) { + mockFetch.mockReset(); + setTimeoutSpy.mockClear(); + mockFetch.mockResolvedValueOnce(new Response("", { status })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(setTimeoutSpy).not.toHaveBeenCalled(); + } + }); + + interface RetryHeaderTestCase { + description: string; + headerName: string; + headerValue: string | (() => string); + expectedDelayMin: number; + expectedDelayMax: number; + } + + const retryHeaderTests: RetryHeaderTestCase[] = [ + { + description: "should respect retry-after header with seconds value", + headerName: "retry-after", + headerValue: "5", + expectedDelayMin: 4000, + expectedDelayMax: 6000, + }, + { + description: "should respect retry-after header with HTTP date value", + headerName: "retry-after", + headerValue: () => new Date(Date.now() + 3000).toUTCString(), + expectedDelayMin: 2000, + expectedDelayMax: 4000, + }, + { + description: "should respect x-ratelimit-reset header", + headerName: "x-ratelimit-reset", + headerValue: () => Math.floor((Date.now() + 4000) / 1000).toString(), + expectedDelayMin: 3000, + expectedDelayMax: 6000, + }, + ]; + + retryHeaderTests.forEach(({ description, headerName, headerValue, expectedDelayMin, expectedDelayMax }) => { + it(description, async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const value = typeof headerValue === "function" ? headerValue() : headerValue; + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ [headerName]: value }), + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expect.any(Number)); + const actualDelay = setTimeoutSpy.mock.calls[0][1]; + expect(actualDelay).toBeGreaterThan(expectedDelayMin); + expect(actualDelay).toBeLessThan(expectedDelayMax); + expect(response.status).toBe(200); + }); + }); + + it("should apply correct exponential backoff with jitter", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch.mockResolvedValue(new Response("", { status: 502 })); + const maxRetries = 3; + const expectedDelays = [1000, 2000, 4000]; + + const responsePromise = requestWithRetries(() => mockFetch(), maxRetries); + await vi.runAllTimersAsync(); + await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledTimes(expectedDelays.length); + + expectedDelays.forEach((delay, index) => { + expect(setTimeoutSpy).toHaveBeenNthCalledWith(index + 1, expect.any(Function), delay); + }); + + expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); + }); + + it("should handle concurrent retries independently", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce(new Response("", { status: 502 })) + .mockResolvedValueOnce(new Response("", { status: 502 })) + .mockResolvedValueOnce(new Response("", { status: 200 })) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const promise1 = requestWithRetries(() => mockFetch(), 1); + const promise2 = requestWithRetries(() => mockFetch(), 1); + + await vi.runAllTimersAsync(); + const [response1, response2] = await Promise.all([promise1, promise2]); + + expect(response1.status).toBe(200); + expect(response2.status).toBe(200); + }); + + it("should cap delay at MAX_RETRY_DELAY for large header values", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ "retry-after": "120" }), // 120 seconds = 120000ms > MAX_RETRY_DELAY (60000ms) + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60000); + expect(response.status).toBe(200); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/signals.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/signals.test.ts new file mode 100644 index 000000000000..c71761723bf6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/signals.test.ts @@ -0,0 +1,114 @@ +import { anySignal, getTimeoutSignal } from "../../../src/core/fetcher/signals"; + +describe("Test getTimeoutSignal", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should return an object with signal and abortId", () => { + const { signal, abortId } = getTimeoutSignal(1000); + + expect(signal).toBeDefined(); + expect(abortId).toBeDefined(); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(false); + }); + + it("should create a signal that aborts after the specified timeout", () => { + const timeoutMs = 5000; + const { signal } = getTimeoutSignal(timeoutMs); + + expect(signal.aborted).toBe(false); + + vi.advanceTimersByTime(timeoutMs - 1); + expect(signal.aborted).toBe(false); + + vi.advanceTimersByTime(1); + expect(signal.aborted).toBe(true); + }); +}); + +describe("Test anySignal", () => { + it("should return an AbortSignal", () => { + const signal = anySignal(new AbortController().signal); + expect(signal).toBeInstanceOf(AbortSignal); + }); + + it("should abort when any of the input signals is aborted", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = anySignal(controller1.signal, controller2.signal); + + expect(signal.aborted).toBe(false); + controller1.abort(); + expect(signal.aborted).toBe(true); + }); + + it("should handle an array of signals", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = anySignal([controller1.signal, controller2.signal]); + + expect(signal.aborted).toBe(false); + controller2.abort(); + expect(signal.aborted).toBe(true); + }); + + it("should abort immediately if one of the input signals is already aborted", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + controller1.abort(); + + const signal = anySignal(controller1.signal, controller2.signal); + expect(signal.aborted).toBe(true); + }); + + it("should detect a signal that aborts between the initial aborted check and the event listener registration", () => { + const ctrlA = new AbortController(); + const ctrlB = new AbortController(); + + const originalAddEventListener = ctrlA.signal.addEventListener.bind(ctrlA.signal); + const originalRemoveEventListener = ctrlA.signal.removeEventListener.bind(ctrlA.signal); + + let abortedAccessCount = 0; + const proxy = new Proxy(ctrlA.signal, { + get(target, prop, receiver) { + if (prop === "aborted") { + abortedAccessCount++; + if (abortedAccessCount === 1) return false; + return Reflect.get(target, prop, receiver); + } + if (prop === "addEventListener") { + return (...args: Parameters) => { + if (abortedAccessCount >= 1 && args[0] === "abort") { + ctrlA.abort("too-late"); + } + return originalAddEventListener(...args); + }; + } + if (prop === "removeEventListener") { + return originalRemoveEventListener; + } + return Reflect.get(target, prop, receiver); + }, + }); + + const combined = anySignal(proxy, ctrlB.signal); + + expect(combined.aborted).toBe(true); + expect(combined.reason).toBe("too-late"); + }); + + it("should forward the abort reason from a source signal", () => { + const controller = new AbortController(); + const combined = anySignal(controller.signal); + + controller.abort("test-reason"); + expect(combined.aborted).toBe(true); + expect(combined.reason).toBe("test-reason"); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/test-file.txt b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/test-file.txt new file mode 100644 index 000000000000..c66d471e359c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/fetcher/test-file.txt @@ -0,0 +1 @@ +This is a test file! diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/logging/logger.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/logging/logger.test.ts new file mode 100644 index 000000000000..2e0b5fe5040c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/logging/logger.test.ts @@ -0,0 +1,454 @@ +import { ConsoleLogger, createLogger, Logger, LogLevel } from "../../../src/core/logging/logger"; + +function createMockLogger() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +describe("Logger", () => { + describe("LogLevel", () => { + it("should have correct log levels", () => { + expect(LogLevel.Debug).toBe("debug"); + expect(LogLevel.Info).toBe("info"); + expect(LogLevel.Warn).toBe("warn"); + expect(LogLevel.Error).toBe("error"); + }); + }); + + describe("ConsoleLogger", () => { + let consoleLogger: ConsoleLogger; + let consoleSpy: { + debug: ReturnType; + info: ReturnType; + warn: ReturnType; + error: ReturnType; + }; + + beforeEach(() => { + consoleLogger = new ConsoleLogger(); + consoleSpy = { + debug: vi.spyOn(console, "debug").mockImplementation(() => {}), + info: vi.spyOn(console, "info").mockImplementation(() => {}), + warn: vi.spyOn(console, "warn").mockImplementation(() => {}), + error: vi.spyOn(console, "error").mockImplementation(() => {}), + }; + }); + + afterEach(() => { + consoleSpy.debug.mockRestore(); + consoleSpy.info.mockRestore(); + consoleSpy.warn.mockRestore(); + consoleSpy.error.mockRestore(); + }); + + it("should log debug messages", () => { + consoleLogger.debug("debug message", { data: "test" }); + expect(consoleSpy.debug).toHaveBeenCalledWith("debug message", { data: "test" }); + }); + + it("should log info messages", () => { + consoleLogger.info("info message", { data: "test" }); + expect(consoleSpy.info).toHaveBeenCalledWith("info message", { data: "test" }); + }); + + it("should log warn messages", () => { + consoleLogger.warn("warn message", { data: "test" }); + expect(consoleSpy.warn).toHaveBeenCalledWith("warn message", { data: "test" }); + }); + + it("should log error messages", () => { + consoleLogger.error("error message", { data: "test" }); + expect(consoleSpy.error).toHaveBeenCalledWith("error message", { data: "test" }); + }); + + it("should handle multiple arguments", () => { + consoleLogger.debug("message", "arg1", "arg2", { key: "value" }); + expect(consoleSpy.debug).toHaveBeenCalledWith("message", "arg1", "arg2", { key: "value" }); + }); + }); + + describe("Logger with level filtering", () => { + let mockLogger: { + debug: ReturnType; + info: ReturnType; + warn: ReturnType; + error: ReturnType; + }; + + beforeEach(() => { + mockLogger = createMockLogger(); + }); + + describe("Debug level", () => { + it("should log all levels when set to debug", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).toHaveBeenCalledWith("debug"); + expect(mockLogger.info).toHaveBeenCalledWith("info"); + expect(mockLogger.warn).toHaveBeenCalledWith("warn"); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(true); + expect(logger.isInfo()).toBe(true); + expect(logger.isWarn()).toBe(true); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Info level", () => { + it("should log info, warn, and error when set to info", () => { + const logger = new Logger({ + level: LogLevel.Info, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith("info"); + expect(mockLogger.warn).toHaveBeenCalledWith("warn"); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Info, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(true); + expect(logger.isWarn()).toBe(true); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Warn level", () => { + it("should log warn and error when set to warn", () => { + const logger = new Logger({ + level: LogLevel.Warn, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).toHaveBeenCalledWith("warn"); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Warn, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(false); + expect(logger.isWarn()).toBe(true); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Error level", () => { + it("should only log error when set to error", () => { + const logger = new Logger({ + level: LogLevel.Error, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Error, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(false); + expect(logger.isWarn()).toBe(false); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Silent mode", () => { + it("should not log anything when silent is true", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: true, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it("should report all level checks as false when silent", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: true, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(false); + expect(logger.isWarn()).toBe(false); + expect(logger.isError()).toBe(false); + }); + }); + + describe("shouldLog", () => { + it("should correctly determine if level should be logged", () => { + const logger = new Logger({ + level: LogLevel.Info, + logger: mockLogger, + silent: false, + }); + + expect(logger.shouldLog(LogLevel.Debug)).toBe(false); + expect(logger.shouldLog(LogLevel.Info)).toBe(true); + expect(logger.shouldLog(LogLevel.Warn)).toBe(true); + expect(logger.shouldLog(LogLevel.Error)).toBe(true); + }); + + it("should return false for all levels when silent", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: true, + }); + + expect(logger.shouldLog(LogLevel.Debug)).toBe(false); + expect(logger.shouldLog(LogLevel.Info)).toBe(false); + expect(logger.shouldLog(LogLevel.Warn)).toBe(false); + expect(logger.shouldLog(LogLevel.Error)).toBe(false); + }); + }); + + describe("Multiple arguments", () => { + it("should pass multiple arguments to logger", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("message", "arg1", { key: "value" }, 123); + expect(mockLogger.debug).toHaveBeenCalledWith("message", "arg1", { key: "value" }, 123); + }); + }); + }); + + describe("createLogger", () => { + it("should return default logger when no config provided", () => { + const logger = createLogger(); + expect(logger).toBeInstanceOf(Logger); + }); + + it("should return same logger instance when Logger is passed", () => { + const customLogger = new Logger({ + level: LogLevel.Debug, + logger: new ConsoleLogger(), + silent: false, + }); + + const result = createLogger(customLogger); + expect(result).toBe(customLogger); + }); + + it("should create logger with custom config", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + level: LogLevel.Warn, + logger: mockLogger, + silent: false, + }); + + expect(logger).toBeInstanceOf(Logger); + logger.warn("test"); + expect(mockLogger.warn).toHaveBeenCalledWith("test"); + }); + + it("should use default values for missing config", () => { + const logger = createLogger({}); + expect(logger).toBeInstanceOf(Logger); + }); + + it("should override default level", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("test"); + expect(mockLogger.debug).toHaveBeenCalledWith("test"); + }); + + it("should override default silent mode", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + logger: mockLogger, + silent: false, + }); + + logger.info("test"); + expect(mockLogger.info).toHaveBeenCalledWith("test"); + }); + + it("should use provided logger implementation", () => { + const customLogger = createMockLogger(); + + const logger = createLogger({ + logger: customLogger, + level: LogLevel.Debug, + silent: false, + }); + + logger.debug("test"); + expect(customLogger.debug).toHaveBeenCalledWith("test"); + }); + + it("should default to silent: true", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + logger: mockLogger, + level: LogLevel.Debug, + }); + + logger.debug("test"); + expect(mockLogger.debug).not.toHaveBeenCalled(); + }); + }); + + describe("Default logger", () => { + it("should have silent: true by default", () => { + const logger = createLogger(); + expect(logger.shouldLog(LogLevel.Info)).toBe(false); + }); + + it("should not log when using default logger", () => { + const logger = createLogger(); + + logger.info("test"); + expect(logger.isInfo()).toBe(false); + }); + }); + + describe("Edge cases", () => { + it("should handle empty message", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug(""); + expect(mockLogger.debug).toHaveBeenCalledWith(""); + }); + + it("should handle no arguments", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("message"); + expect(mockLogger.debug).toHaveBeenCalledWith("message"); + }); + + it("should handle complex objects", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + const complexObject = { + nested: { key: "value" }, + array: [1, 2, 3], + fn: () => "test", + }; + + logger.debug("message", complexObject); + expect(mockLogger.debug).toHaveBeenCalledWith("message", complexObject); + }); + + it("should handle errors as arguments", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Error, + logger: mockLogger, + silent: false, + }); + + const error = new Error("Test error"); + logger.error("Error occurred", error); + expect(mockLogger.error).toHaveBeenCalledWith("Error occurred", error); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/QueryStringBuilder.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/QueryStringBuilder.test.ts new file mode 100644 index 000000000000..1afa2d22248f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/QueryStringBuilder.test.ts @@ -0,0 +1,236 @@ +import { queryBuilder } from "../../../src/core/url/QueryStringBuilder"; + +describe("QueryStringBuilder", () => { + describe("add() — default repeat format", () => { + it("adds a scalar string value", () => { + const qs = queryBuilder().add("key", "value").build(); + expect(qs).toBe("key=value"); + }); + + it("adds a scalar number value", () => { + const qs = queryBuilder().add("limit", 10).build(); + expect(qs).toBe("limit=10"); + }); + + it("adds a boolean value", () => { + const qs = queryBuilder().add("active", true).build(); + expect(qs).toBe("active=true"); + }); + + it("skips undefined values", () => { + const qs = queryBuilder().add("key", undefined).build(); + expect(qs).toBe(""); + }); + + it("skips null values", () => { + const qs = queryBuilder().add("key", null).build(); + expect(qs).toBe(""); + }); + + it("repeats array elements as separate key=value pairs", () => { + const qs = queryBuilder().add("color", ["red", "blue", "green"]).build(); + expect(qs).toBe("color=red&color=blue&color=green"); + }); + + it("skips undefined items within arrays", () => { + const qs = queryBuilder().add("color", ["red", undefined, "blue"]).build(); + expect(qs).toBe("color=red&color=blue"); + }); + + it("returns empty string for empty array", () => { + const qs = queryBuilder().add("color", []).build(); + expect(qs).toBe(""); + }); + + it("encodes special characters in keys and values", () => { + const qs = queryBuilder().add("my key", "hello world").build(); + expect(qs).toBe("my%20key=hello%20world"); + }); + + it("handles nested objects", () => { + const qs = queryBuilder().add("filter", { status: "active" }).build(); + expect(qs).toBe("filter%5Bstatus%5D=active"); + }); + }); + + describe("add() — comma style", () => { + it("joins array values with literal commas", () => { + const qs = queryBuilder().add("tags", ["a", "b", "c"], { style: "comma" }).build(); + expect(qs).toBe("tags=a,b,c"); + }); + + it("handles single-element array", () => { + const qs = queryBuilder().add("tags", ["only"], { style: "comma" }).build(); + expect(qs).toBe("tags=only"); + }); + + it("returns empty string for empty array", () => { + const qs = queryBuilder().add("tags", [], { style: "comma" }).build(); + expect(qs).toBe(""); + }); + + it("skips undefined values", () => { + const qs = queryBuilder().add("tags", undefined, { style: "comma" }).build(); + expect(qs).toBe(""); + }); + + it("skips null values", () => { + const qs = queryBuilder().add("tags", null, { style: "comma" }).build(); + expect(qs).toBe(""); + }); + + it("treats scalar values same as default add()", () => { + const qs = queryBuilder().add("tag", "single", { style: "comma" }).build(); + expect(qs).toBe("tag=single"); + }); + + it("encodes commas within individual values as %2C", () => { + const qs = queryBuilder().add("items", ["a,b", "c"], { style: "comma" }).build(); + expect(qs).toBe("items=a%2Cb,c"); + }); + + it("encodes special characters in values", () => { + const qs = queryBuilder().add("tags", ["hello world", "foo&bar"], { style: "comma" }).build(); + expect(qs).toBe("tags=hello%20world,foo%26bar"); + }); + }); + + describe("chaining", () => { + it("chains multiple add() calls", () => { + const qs = queryBuilder().add("limit", 10).add("offset", 20).build(); + expect(qs).toBe("limit=10&offset=20"); + }); + + it("chains add() with default and comma styles", () => { + const qs = queryBuilder() + .add("limit", 10) + .add("tags", ["ACCESS_GRANTED", "COPY", "DELETE"], { style: "comma" }) + .add("active", true) + .build(); + expect(qs).toBe("limit=10&tags=ACCESS_GRANTED,COPY,DELETE&active=true"); + }); + + it("skips undefined/null params in chain without breaking", () => { + const qs = queryBuilder() + .add("a", "1") + .add("b", undefined) + .add("c", null, { style: "comma" }) + .add("d", "4") + .build(); + expect(qs).toBe("a=1&d=4"); + }); + }); + + describe("addMany()", () => { + it("adds all params from a record", () => { + const qs = queryBuilder().addMany({ limit: 10, offset: 20, name: "test" }).build(); + expect(qs).toBe("limit=10&offset=20&name=test"); + }); + + it("skips null and undefined values", () => { + const qs = queryBuilder().addMany({ a: "1", b: null, c: undefined, d: "4" }).build(); + expect(qs).toBe("a=1&d=4"); + }); + + it("handles empty record", () => { + const qs = queryBuilder().addMany({}).build(); + expect(qs).toBe(""); + }); + + it("works with comma-style override after addMany", () => { + const params = { limit: 10, tags: ["a", "b"], active: true }; + const qs = queryBuilder().addMany(params).add("tags", params.tags, { style: "comma" }).build(); + expect(qs).toBe("limit=10&tags=a,b&active=true"); + }); + + it("handles array values with default repeat format", () => { + const qs = queryBuilder() + .addMany({ ids: [1, 2, 3] }) + .build(); + expect(qs).toBe("ids=1&ids=2&ids=3"); + }); + }); + + describe("mergeAdditional()", () => { + it("appends additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ extra: "value" }).build(); + expect(qs).toBe("limit=10&extra=value"); + }); + + it("overrides existing keys (last-write-wins)", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ limit: 20 }).build(); + expect(qs).toBe("limit=20"); + }); + + it("handles undefined additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional(undefined).build(); + expect(qs).toBe("limit=10"); + }); + + it("skips undefined values in additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ extra: undefined }).build(); + expect(qs).toBe("limit=10"); + }); + + it("skips null values in additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ extra: null }).build(); + expect(qs).toBe("limit=10"); + }); + + it("handles array values in additional params using repeat format", () => { + const qs = queryBuilder() + .mergeAdditional({ ids: [1, 2, 3] }) + .build(); + expect(qs).toBe("ids=1&ids=2&ids=3"); + }); + + it("overrides a comma-style param with repeat format", () => { + const qs = queryBuilder() + .add("tags", ["a", "b"], { style: "comma" }) + .mergeAdditional({ tags: ["x", "y"] }) + .build(); + expect(qs).toBe("tags=x&tags=y"); + }); + }); + + describe("build()", () => { + it("returns empty string when no params added", () => { + const qs = queryBuilder().build(); + expect(qs).toBe(""); + }); + + it("does not include leading ?", () => { + const qs = queryBuilder().add("key", "value").build(); + expect(qs).not.toContain("?"); + }); + }); + + describe("end-to-end scenarios", () => { + it("matches expected query-parameters-openapi output pattern", () => { + const params: Record = { + limit: 1, + id: "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + date: "2023-01-15", + deadline: "2024-01-15T09:30:00.000Z", + bytes: "SGVsbG8gd29ybGQh", + user: "user", + userList: ["user"], + optionalString: "optionalString", + nestedUser: "nestedUser", + excludeUser: "excludeUser", + filter: "filter", + tags: ["tags"], + optionalTags: undefined, + }; + const qs = queryBuilder() + .addMany(params) + .add("tags", params.tags, { style: "comma" }) + .add("optionalTags", params.optionalTags, { style: "comma" }) + .mergeAdditional(undefined) + .build(); + expect(qs).toContain("limit=1"); + expect(qs).toContain("tags=tags"); + expect(qs).not.toContain("optionalTags"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/join.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/join.test.ts new file mode 100644 index 000000000000..123488f084ea --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/join.test.ts @@ -0,0 +1,284 @@ +import { join } from "../../../src/core/url/index"; + +describe("join", () => { + interface TestCase { + description: string; + base: string; + segments: string[]; + expected: string; + } + + describe("basic functionality", () => { + const basicTests: TestCase[] = [ + { description: "should return empty string for empty base", base: "", segments: [], expected: "" }, + { + description: "should return empty string for empty base with path", + base: "", + segments: ["path"], + expected: "", + }, + { + description: "should handle single segment", + base: "base", + segments: ["segment"], + expected: "base/segment", + }, + { + description: "should handle single segment with trailing slash on base", + base: "base/", + segments: ["segment"], + expected: "base/segment", + }, + { + description: "should handle single segment with leading slash", + base: "base", + segments: ["/segment"], + expected: "base/segment", + }, + { + description: "should handle single segment with both slashes", + base: "base/", + segments: ["/segment"], + expected: "base/segment", + }, + { + description: "should handle multiple segments", + base: "base", + segments: ["path1", "path2", "path3"], + expected: "base/path1/path2/path3", + }, + { + description: "should handle multiple segments with slashes", + base: "base/", + segments: ["/path1/", "/path2/", "/path3/"], + expected: "base/path1/path2/path3/", + }, + ]; + + basicTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("URL handling", () => { + const urlTests: TestCase[] = [ + { + description: "should handle absolute URLs", + base: "https://example.com", + segments: ["api", "v1"], + expected: "https://example.com/api/v1", + }, + { + description: "should handle absolute URLs with slashes", + base: "https://example.com/", + segments: ["/api/", "/v1/"], + expected: "https://example.com/api/v1/", + }, + { + description: "should handle absolute URLs with base path", + base: "https://example.com/base", + segments: ["api", "v1"], + expected: "https://example.com/base/api/v1", + }, + { + description: "should preserve URL query parameters", + base: "https://example.com?query=1", + segments: ["api"], + expected: "https://example.com/api?query=1", + }, + { + description: "should preserve URL fragments", + base: "https://example.com#fragment", + segments: ["api"], + expected: "https://example.com/api#fragment", + }, + { + description: "should preserve URL query and fragments", + base: "https://example.com?query=1#fragment", + segments: ["api"], + expected: "https://example.com/api?query=1#fragment", + }, + { + description: "should handle http protocol", + base: "http://example.com", + segments: ["api"], + expected: "http://example.com/api", + }, + { + description: "should handle ftp protocol", + base: "ftp://example.com", + segments: ["files"], + expected: "ftp://example.com/files", + }, + { + description: "should handle ws protocol", + base: "ws://example.com", + segments: ["socket"], + expected: "ws://example.com/socket", + }, + { + description: "should fallback to path joining for malformed URLs", + base: "not-a-url://", + segments: ["path"], + expected: "not-a-url:///path", + }, + ]; + + urlTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("edge cases", () => { + const edgeCaseTests: TestCase[] = [ + { + description: "should handle empty segments", + base: "base", + segments: ["", "path"], + expected: "base/path", + }, + { + description: "should handle null segments", + base: "base", + segments: [null as any, "path"], + expected: "base/path", + }, + { + description: "should handle undefined segments", + base: "base", + segments: [undefined as any, "path"], + expected: "base/path", + }, + { + description: "should handle segments with only single slash", + base: "base", + segments: ["/", "path"], + expected: "base/path", + }, + { + description: "should handle segments with only double slash", + base: "base", + segments: ["//", "path"], + expected: "base/path", + }, + { + description: "should handle base paths with trailing slashes", + base: "base/", + segments: ["path"], + expected: "base/path", + }, + { + description: "should handle complex nested paths", + base: "api/v1/", + segments: ["/users/", "/123/", "/profile"], + expected: "api/v1/users/123/profile", + }, + ]; + + edgeCaseTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("real-world scenarios", () => { + const realWorldTests: TestCase[] = [ + { + description: "should handle API endpoint construction", + base: "https://api.example.com/v1", + segments: ["users", "123", "posts"], + expected: "https://api.example.com/v1/users/123/posts", + }, + { + description: "should handle file path construction", + base: "/var/www", + segments: ["html", "assets", "images"], + expected: "/var/www/html/assets/images", + }, + { + description: "should handle relative path construction", + base: "../parent", + segments: ["child", "grandchild"], + expected: "../parent/child/grandchild", + }, + { + description: "should handle Windows-style paths", + base: "C:\\Users", + segments: ["Documents", "file.txt"], + expected: "C:\\Users/Documents/file.txt", + }, + ]; + + realWorldTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("performance scenarios", () => { + it("should handle many segments efficiently", () => { + const segments = Array(100).fill("segment"); + const result = join("base", ...segments); + expect(result).toBe(`base/${segments.join("/")}`); + }); + + it("should handle long URLs", () => { + const longPath = "a".repeat(1000); + expect(join("https://example.com", longPath)).toBe(`https://example.com/${longPath}`); + }); + }); + + describe("trailing slash preservation", () => { + const trailingSlashTests: TestCase[] = [ + { + description: + "should preserve trailing slash on final result when base has trailing slash and no segments", + base: "https://api.example.com/", + segments: [], + expected: "https://api.example.com/", + }, + { + description: "should preserve trailing slash on v1 path", + base: "https://api.example.com/v1/", + segments: [], + expected: "https://api.example.com/v1/", + }, + { + description: "should preserve trailing slash when last segment has trailing slash", + base: "https://api.example.com", + segments: ["users/"], + expected: "https://api.example.com/users/", + }, + { + description: "should preserve trailing slash with relative path", + base: "api/v1", + segments: ["users/"], + expected: "api/v1/users/", + }, + { + description: "should preserve trailing slash with multiple segments", + base: "https://api.example.com", + segments: ["v1", "collections/"], + expected: "https://api.example.com/v1/collections/", + }, + { + description: "should preserve trailing slash with base path", + base: "base", + segments: ["path1", "path2/"], + expected: "base/path1/path2/", + }, + ]; + + trailingSlashTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/qs.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/qs.test.ts new file mode 100644 index 000000000000..54d2d9ce3f86 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/unit/url/qs.test.ts @@ -0,0 +1,374 @@ +import { toQueryString } from "../../../src/core/url/index"; + +describe("Test qs toQueryString", () => { + interface BasicTestCase { + description: string; + input: any; + expected: string; + } + + describe("Basic functionality", () => { + const basicTests: BasicTestCase[] = [ + { description: "should return empty string for null", input: null, expected: "" }, + { description: "should return empty string for undefined", input: undefined, expected: "" }, + { description: "should return empty string for string primitive", input: "hello", expected: "" }, + { description: "should return empty string for number primitive", input: 42, expected: "" }, + { description: "should return empty string for true boolean", input: true, expected: "" }, + { description: "should return empty string for false boolean", input: false, expected: "" }, + { description: "should handle empty objects", input: {}, expected: "" }, + { + description: "should handle simple key-value pairs", + input: { name: "John", age: 30 }, + expected: "name=John&age=30", + }, + ]; + + basicTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); + }); + }); + + describe("Array handling", () => { + interface ArrayTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" | "comma" }; + expected: string; + } + + const arrayTests: ArrayTestCase[] = [ + { + description: "should handle arrays with indices format (default)", + input: { items: ["a", "b", "c"] }, + expected: "items%5B0%5D=a&items%5B1%5D=b&items%5B2%5D=c", + }, + { + description: "should handle arrays with repeat format", + input: { items: ["a", "b", "c"] }, + options: { arrayFormat: "repeat" }, + expected: "items=a&items=b&items=c", + }, + { + description: "should handle empty arrays", + input: { items: [] }, + expected: "", + }, + { + description: "should handle arrays with mixed types", + input: { mixed: ["string", 42, true, false] }, + expected: "mixed%5B0%5D=string&mixed%5B1%5D=42&mixed%5B2%5D=true&mixed%5B3%5D=false", + }, + { + description: "should handle arrays with objects", + input: { users: [{ name: "John" }, { name: "Jane" }] }, + expected: "users%5B0%5D%5Bname%5D=John&users%5B1%5D%5Bname%5D=Jane", + }, + { + description: "should handle arrays with objects in repeat format", + input: { users: [{ name: "John" }, { name: "Jane" }] }, + options: { arrayFormat: "repeat" }, + expected: "users%5Bname%5D=John&users%5Bname%5D=Jane", + }, + ]; + + arrayTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Nested objects", () => { + const nestedTests: BasicTestCase[] = [ + { + description: "should handle nested objects", + input: { user: { name: "John", age: 30 } }, + expected: "user%5Bname%5D=John&user%5Bage%5D=30", + }, + { + description: "should handle deeply nested objects", + input: { user: { profile: { name: "John", settings: { theme: "dark" } } } }, + expected: "user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", + }, + { + description: "should handle empty nested objects", + input: { user: {} }, + expected: "", + }, + ]; + + nestedTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); + }); + }); + + describe("Encoding", () => { + interface EncodingTestCase { + description: string; + input: any; + options?: { encode?: boolean }; + expected: string; + } + + const encodingTests: EncodingTestCase[] = [ + { + description: "should encode by default", + input: { name: "John Doe", email: "john@example.com" }, + expected: "name=John%20Doe&email=john%40example.com", + }, + { + description: "should not encode when encode is false", + input: { name: "John Doe", email: "john@example.com" }, + options: { encode: false }, + expected: "name=John Doe&email=john@example.com", + }, + { + description: "should encode special characters in keys", + input: { "user name": "John", "email[primary]": "john@example.com" }, + expected: "user%20name=John&email%5Bprimary%5D=john%40example.com", + }, + { + description: "should not encode special characters in keys when encode is false", + input: { "user name": "John", "email[primary]": "john@example.com" }, + options: { encode: false }, + expected: "user name=John&email[primary]=john@example.com", + }, + ]; + + encodingTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Mixed scenarios", () => { + interface MixedTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" | "comma" }; + expected: string; + } + + const mixedTests: MixedTestCase[] = [ + { + description: "should handle complex nested structures", + input: { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], + }, + }, + sort: { field: "name", direction: "asc" }, + }, + expected: + "filters%5Bstatus%5D%5B0%5D=active&filters%5Bstatus%5D%5B1%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D%5B0%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D%5B1%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + }, + { + description: "should handle complex nested structures with repeat format", + input: { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], + }, + }, + sort: { field: "name", direction: "asc" }, + }, + options: { arrayFormat: "repeat" }, + expected: + "filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + }, + { + description: "should handle arrays with null/undefined values", + input: { items: ["a", null, "c", undefined, "e"] }, + expected: "items%5B0%5D=a&items%5B2%5D=c&items%5B4%5D=e", + }, + { + description: "should handle objects with null/undefined values", + input: { name: "John", age: null, email: undefined, active: true }, + expected: "name=John&active=true", + }, + ]; + + mixedTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Edge cases", () => { + const edgeCaseTests: BasicTestCase[] = [ + { + description: "should handle numeric keys", + input: { "0": "zero", "1": "one" }, + expected: "0=zero&1=one", + }, + { + description: "should handle boolean values in objects", + input: { enabled: true, disabled: false }, + expected: "enabled=true&disabled=false", + }, + { + description: "should handle empty strings", + input: { name: "", description: "test" }, + expected: "name=&description=test", + }, + { + description: "should handle zero values", + input: { count: 0, price: 0.0 }, + expected: "count=0&price=0", + }, + { + description: "should handle arrays with empty strings", + input: { items: ["a", "", "c"] }, + expected: "items%5B0%5D=a&items%5B1%5D=&items%5B2%5D=c", + }, + ]; + + edgeCaseTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); + }); + }); + + describe("Comma array format", () => { + interface CommaTestCase { + description: string; + input: any; + options?: { arrayFormat?: "comma"; encode?: boolean }; + expected: string; + } + + const commaTests: CommaTestCase[] = [ + { + description: "should join array values with commas", + input: { event_type: ["ACCESS_GRANTED", "COPY", "DELETE"] }, + options: { arrayFormat: "comma" }, + expected: "event_type=ACCESS_GRANTED,COPY,DELETE", + }, + { + description: "should handle single-element array", + input: { event_type: ["ACCESS_GRANTED"] }, + options: { arrayFormat: "comma" }, + expected: "event_type=ACCESS_GRANTED", + }, + { + description: "should handle empty array", + input: { event_type: [] }, + options: { arrayFormat: "comma" }, + expected: "", + }, + { + description: "should not percent-encode commas", + input: { items: ["a", "b", "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a,b,c", + }, + { + description: "should encode values but not commas", + input: { items: ["a b", "c d"] }, + options: { arrayFormat: "comma" }, + expected: "items=a%20b,c%20d", + }, + { + description: "should not encode when encode is false", + input: { items: ["a b", "c d"] }, + options: { arrayFormat: "comma", encode: false }, + expected: "items=a b,c d", + }, + { + description: "should handle mixed parameters with comma and non-array values", + input: { event_type: ["ACCESS_GRANTED", "COPY", "DELETE"], limit: 10, offset: 0 }, + options: { arrayFormat: "comma" }, + expected: "event_type=ACCESS_GRANTED,COPY,DELETE&limit=10&offset=0", + }, + { + description: "should handle numeric array values", + input: { ids: [1, 2, 3] }, + options: { arrayFormat: "comma" }, + expected: "ids=1,2,3", + }, + { + description: "should handle boolean array values", + input: { flags: [true, false, true] }, + options: { arrayFormat: "comma" }, + expected: "flags=true,false,true", + }, + { + description: "should skip null values in comma format", + input: { items: ["a", null, "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a,c", + }, + { + description: "should skip undefined values in comma format", + input: { items: ["a", undefined, "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a,c", + }, + { + description: "should produce empty string for all-null array in comma format", + input: { items: [null, undefined] }, + options: { arrayFormat: "comma" }, + expected: "", + }, + { + description: "should encode commas within values while keeping separator commas literal", + input: { items: ["a,b", "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a%2Cb,c", + }, + ]; + + commaTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Options combinations", () => { + interface OptionsTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" | "comma"; encode?: boolean }; + expected: string; + } + + const optionsTests: OptionsTestCase[] = [ + { + description: "should respect both arrayFormat and encode options", + input: { items: ["a & b", "c & d"] }, + options: { arrayFormat: "repeat", encode: false }, + expected: "items=a & b&items=c & d", + }, + { + description: "should use default options when none provided", + input: { items: ["a", "b"] }, + expected: "items%5B0%5D=a&items%5B1%5D=b", + }, + { + description: "should merge provided options with defaults", + input: { items: ["a", "b"], name: "John Doe" }, + options: { encode: false }, + expected: "items[0]=a&items[1]=b&name=John Doe", + }, + ]; + + optionsTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/.gitkeep b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts new file mode 100644 index 000000000000..fabc4a35cd41 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts @@ -0,0 +1,50 @@ +// This file was auto-generated by Fern from our API Definition. + +import { SeedTsFlattenRequestAnyAuthClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; + +describe("UsersClient", () => { + test("updateUser", async () => { + const server = mockServerPool.createServer(); + const client = new SeedTsFlattenRequestAnyAuthClient({ + maxRetries: 0, + bearerAuth: { token: "test" }, + apiKey: { apiKey: "test" }, + environment: server.baseUrl, + }); + const rawRequestBody = { id: "body-id", name: "Ada" }; + + server.mockEndpoint().put("/users/path-id").jsonBody(rawRequestBody).respondWith().statusCode(200).build(); + + const response = await client.users.updateUser({ + id: "body-id", + name: "Ada", + }); + expect(response).toEqual(undefined); + }); + + test("updateUserProfile", async () => { + const server = mockServerPool.createServer(); + const client = new SeedTsFlattenRequestAnyAuthClient({ + maxRetries: 0, + bearerAuth: { token: "test" }, + apiKey: { apiKey: "test" }, + environment: server.baseUrl, + }); + const rawRequestBody = { id: "body-id", name: "Ada" }; + + server + .mockEndpoint() + .put("/users/path-id/profile") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .build(); + + const response = await client.users.updateUserProfile("path-id", { + id: "body-id", + name: "Ada", + }); + expect(response).toEqual(undefined); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.base.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.base.json new file mode 100644 index 000000000000..93a92c0630b5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "extendedDiagnostics": true, + "strict": true, + "target": "ES6", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "isolatedModules": true, + "isolatedDeclarations": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.cjs.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.cjs.json new file mode 100644 index 000000000000..5c11446f5984 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist/cjs" + }, + "include": ["src"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.esm.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.esm.json new file mode 100644 index 000000000000..021e74d21dd3 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "dist/esm", + "verbatimModuleSyntax": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.json new file mode 100644 index 000000000000..d77fdf00d259 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.cjs.json" +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/vitest.config.mts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/vitest.config.mts new file mode 100644 index 000000000000..0dee5a752d39 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/vitest.config.mts @@ -0,0 +1,32 @@ +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { + typecheck: { + enabled: true, + tsconfig: "./tests/tsconfig.json", + }, + projects: [ + { + test: { + globals: true, + name: "unit", + environment: "node", + root: "./tests", + include: ["**/*.test.{js,ts,jsx,tsx}"], + exclude: ["wire/**"], + setupFiles: ["./setup.ts"], + }, + }, + { + test: { + globals: true, + name: "wire", + environment: "node", + root: "./tests/wire", + setupFiles: ["../setup.ts", "../mock-server/setup.ts"], + }, + }, + ], + passWithNoTests: true, + }, +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/metadata.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/metadata.json new file mode 100644 index 000000000000..0d684f61c460 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/metadata.json @@ -0,0 +1,9 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-typescript-sdk", + "generatorVersion": "latest", + "originGitCommit": "DUMMY", + "invokedBy": "manual", + "requestedVersion": "0.0.1", + "sdkVersion": "0.0.1" +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/verify.sh b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/verify.sh new file mode 100755 index 000000000000..a224ac815887 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.fern/verify.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +pnpm install +pnpm build +pnpm test diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.github/workflows/ci.yml b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.github/workflows/ci.yml new file mode 100644 index 000000000000..93fba226cb67 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up node + uses: actions/setup-node@v6 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Compile + run: pnpm build + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up node + uses: actions/setup-node@v6 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test + run: pnpm test diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.gitignore b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.gitignore new file mode 100644 index 000000000000..72271e049c02 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +/dist \ No newline at end of file diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/CONTRIBUTING.md b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/CONTRIBUTING.md new file mode 100644 index 000000000000..fe5bc2f77e0b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Node.js 20 or higher +- pnpm package manager + +### Installation + +Install the project dependencies: + +```bash +pnpm install +``` + +### Building + +Build the project: + +```bash +pnpm build +``` + +### Testing + +Run the test suite: + +```bash +pnpm test +``` + +Run specific test types: +- `pnpm test:unit` - Run unit tests +- `pnpm test:wire` - Run wire/integration tests + +### Linting and Formatting + +Check code style: + +```bash +pnpm run lint +pnpm run format:check +``` + +Fix code style issues: + +```bash +pnpm run lint:fix +pnpm run format:fix +``` + +Or use the combined check command: + +```bash +pnpm run check:fix +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/api/` - API client classes and types +- `src/serialization/` - Serialization/deserialization logic +- Most TypeScript files in `src/` + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The TypeScript SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/typescript/sdk/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `pnpm test` +4. Run linting and formatting: `pnpm run check:fix` +5. Build the project: `pnpm build` +6. Commit your changes with a clear commit message +7. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting and linting. Run `pnpm run check:fix` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md new file mode 100644 index 000000000000..09fe92adb6b2 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md @@ -0,0 +1,301 @@ +# Seed TypeScript Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FTypeScript) +[![npm shield](https://img.shields.io/npm/v/@fern/ts-flatten-request-any-auth)](https://www.npmjs.com/package/@fern/ts-flatten-request-any-auth) + +The Seed TypeScript library provides convenient access to the Seed APIs from TypeScript. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Request and Response Types](#request-and-response-types) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Subpackage Exports](#subpackage-exports) + - [Additional Headers](#additional-headers) + - [Additional Query String Parameters](#additional-query-string-parameters) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Aborting Requests](#aborting-requests) + - [Access Raw Response Data](#access-raw-response-data) + - [Logging](#logging) + - [Custom Fetch](#custom-fetch) + - [Runtime Compatibility](#runtime-compatibility) +- [Contributing](#contributing) + +## Installation + +```sh +npm i -s @fern/ts-flatten-request-any-auth +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```typescript +import { SeedTsFlattenRequestAnyAuthClient } from "@fern/ts-flatten-request-any-auth"; + +const client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: "YOUR_BASE_URL", token: "YOUR_TOKEN", apiKey: "YOUR_API_KEY" }); +await client.users.updateUser({ + id: "path-id", + body: { + id: "body-id", + name: "Ada" + } +}); +``` + +## Request and Response Types + +The SDK exports all request and response types as TypeScript interfaces. Simply import them with the +following namespace: + +```typescript +import { SeedTsFlattenRequestAnyAuth } from "@fern/ts-flatten-request-any-auth"; + +const request: SeedTsFlattenRequestAnyAuth.UpdateUserRequest = { + ... +}; +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```typescript +import { SeedTsFlattenRequestAnyAuthError } from "@fern/ts-flatten-request-any-auth"; + +try { + await client.users.updateUser(...); +} catch (err) { + if (err instanceof SeedTsFlattenRequestAnyAuthError) { + console.log(err.statusCode); + console.log(err.message); + console.log(err.body); + console.log(err.rawResponse); + } +} +``` + +## Advanced + +### Subpackage Exports + +This SDK supports direct imports of subpackage clients, which allows JavaScript bundlers to tree-shake and include only the imported subpackage code. This results in much smaller bundle sizes. + +```typescript +import { UsersClient } from '@fern/ts-flatten-request-any-auth/users'; + +const client = new UsersClient({...}); +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `headers` request option. + +```typescript +import { SeedTsFlattenRequestAnyAuthClient } from "@fern/ts-flatten-request-any-auth"; + +const client = new SeedTsFlattenRequestAnyAuthClient({ + ... + headers: { + 'X-Custom-Header': 'custom value' + } +}); + +const response = await client.users.updateUser(..., { + headers: { + 'X-Custom-Header': 'custom value' + } +}); +``` + +### Additional Query String Parameters + +If you would like to send additional query string parameters as part of the request, use the `queryParams` request option. + +```typescript +const response = await client.users.updateUser(..., { + queryParams: { + 'customQueryParamKey': 'custom query param value' + } +}); +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +Which status codes are retried depends on the `retryStatusCodes` generator configuration: + +**`legacy`** (current default): retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) + +**`recommended`**: retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `maxRetries` request option to configure this behavior. + +```typescript +const response = await client.users.updateUser(..., { + maxRetries: 0 // override maxRetries at the request level +}); +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. Use the `timeoutInSeconds` option to configure this behavior. + +```typescript +const response = await client.users.updateUser(..., { + timeoutInSeconds: 30 // override timeout to 30s +}); +``` + +### Aborting Requests + +The SDK allows users to abort requests at any point by passing in an abort signal. + +```typescript +const controller = new AbortController(); +const response = await client.users.updateUser(..., { + abortSignal: controller.signal +}); +controller.abort(); // aborts the request +``` + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.withRawResponse()` method. +The `.withRawResponse()` method returns a promise that results to an object with a `data` and a `rawResponse` property. + +```typescript +const { data, rawResponse } = await client.users.updateUser(...).withRawResponse(); + +console.log(data); +console.log(rawResponse.headers['X-My-Header']); +``` + +### Logging + +The SDK supports logging. You can configure the logger by passing in a `logging` object to the client options. + +```typescript +import { SeedTsFlattenRequestAnyAuthClient, logging } from "@fern/ts-flatten-request-any-auth"; + +const client = new SeedTsFlattenRequestAnyAuthClient({ + ... + logging: { + level: logging.LogLevel.Debug, // defaults to logging.LogLevel.Info + logger: new logging.ConsoleLogger(), // defaults to ConsoleLogger + silent: false, // defaults to true, set to false to enable logging + } +}); +``` +The `logging` object can have the following properties: +- `level`: The log level to use. Defaults to `logging.LogLevel.Info`. +- `logger`: The logger to use. Defaults to a `logging.ConsoleLogger`. +- `silent`: Whether to silence the logger. Defaults to `true`. + +The `level` property can be one of the following values: +- `logging.LogLevel.Debug` +- `logging.LogLevel.Info` +- `logging.LogLevel.Warn` +- `logging.LogLevel.Error` + +To provide a custom logger, you can pass in an object that implements the `logging.ILogger` interface. + +
+Custom logger examples + +Here's an example using the popular `winston` logging library. +```ts +import winston from 'winston'; + +const winstonLogger = winston.createLogger({...}); + +const logger: logging.ILogger = { + debug: (msg, ...args) => winstonLogger.debug(msg, ...args), + info: (msg, ...args) => winstonLogger.info(msg, ...args), + warn: (msg, ...args) => winstonLogger.warn(msg, ...args), + error: (msg, ...args) => winstonLogger.error(msg, ...args), +}; +``` + +Here's an example using the popular `pino` logging library. + +```ts +import pino from 'pino'; + +const pinoLogger = pino({...}); + +const logger: logging.ILogger = { + debug: (msg, ...args) => pinoLogger.debug(args, msg), + info: (msg, ...args) => pinoLogger.info(args, msg), + warn: (msg, ...args) => pinoLogger.warn(args, msg), + error: (msg, ...args) => pinoLogger.error(args, msg), +}; +``` +
+ + +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + +### Runtime Compatibility + + +The SDK works in the following runtimes: + + + +- Node.js 18+ +- Vercel +- Cloudflare Workers +- Deno v1.25+ +- Bun 1.0+ +- React Native + + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/biome.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/biome.json new file mode 100644 index 000000000000..6b89164f9f99 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/biome.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "root": true, + "vcs": { + "enabled": false + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!!dist", + "!!**/dist", + "!!lib", + "!!**/lib", + "!!_tmp_*", + "!!**/_tmp_*", + "!!*.tmp", + "!!**/*.tmp", + "!!.tmp/", + "!!**/.tmp/", + "!!*.log", + "!!**/*.log", + "!!**/.DS_Store", + "!!**/Thumbs.db" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 120 + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "rules": { + "style": { + "useNodejsImportProtocol": "off" + }, + "suspicious": { + "noAssignInExpressions": "warn", + "noUselessEscapeInString": { + "level": "warn", + "fix": "none", + "options": {} + }, + "noThenProperty": "warn", + "useIterableCallbackReturn": "warn", + "noShadowRestrictedNames": "warn", + "noTsIgnore": { + "level": "warn", + "fix": "none", + "options": {} + }, + "noConfusingVoidType": { + "level": "warn", + "fix": "none", + "options": {} + } + } + } + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/package.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/package.json new file mode 100644 index 000000000000..ec7d3e8ee315 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/package.json @@ -0,0 +1,80 @@ +{ + "name": "@fern/ts-flatten-request-any-auth", + "version": "0.0.1", + "private": false, + "repository": { + "type": "git", + "url": "git+https://github.com/ts-flatten-request-any-auth/fern.git" + }, + "type": "commonjs", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + }, + "default": "./dist/cjs/index.js" + }, + "./users": { + "import": { + "types": "./dist/esm/api/resources/users/exports.d.mts", + "default": "./dist/esm/api/resources/users/exports.mjs" + }, + "require": { + "types": "./dist/cjs/api/resources/users/exports.d.ts", + "default": "./dist/cjs/api/resources/users/exports.js" + }, + "default": "./dist/cjs/api/resources/users/exports.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "reference.md", + "README.md", + "LICENSE" + ], + "scripts": { + "format": "biome format --write --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "format:check": "biome format --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "lint": "biome lint --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "lint:fix": "biome lint --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "check": "biome check --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "check:fix": "biome check --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "build": "pnpm build:cjs && pnpm build:esm", + "build:cjs": "tsc --project ./tsconfig.cjs.json", + "build:esm": "tsc --project ./tsconfig.esm.json && node scripts/rename-to-esm-files.js dist/esm", + "test": "vitest", + "test:unit": "vitest --project unit", + "test:wire": "vitest --project wire" + }, + "dependencies": {}, + "devDependencies": { + "webpack": "^5.105.4", + "ts-loader": "^9.5.4", + "vitest": "^4.1.1", + "msw": "2.11.2", + "@types/node": "^20.0.0", + "typescript": "~5.9.3", + "@biomejs/biome": "2.4.10" + }, + "browser": { + "fs": false, + "os": false, + "path": false, + "stream": false, + "crypto": false + }, + "packageManager": "pnpm@10.33.0", + "engines": { + "node": ">=18.0.0" + }, + "sideEffects": false +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/pnpm-workspace.yaml b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/pnpm-workspace.yaml new file mode 100644 index 000000000000..6e4c395107df --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/pnpm-workspace.yaml @@ -0,0 +1 @@ +packages: ['.'] \ No newline at end of file diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md new file mode 100644 index 000000000000..4d8d49e5de1a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md @@ -0,0 +1,117 @@ +# Reference +## Users +
client.users.updateUser({ ...params }) -> SeedTsFlattenRequestAnyAuth.UpdateUser +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.users.updateUser({ + id: "path-id", + body: { + id: "body-id", + name: "Ada" + } +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `SeedTsFlattenRequestAnyAuth.UpdateUserRequest` + +
+
+ +
+
+ +**requestOptions:** `UsersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.users.updateUserProfile(id, { ...params }) -> SeedTsFlattenRequestAnyAuth.UpdateUser +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.users.updateUserProfile("path-id", { + id: "body-id", + name: "Ada" +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**request:** `SeedTsFlattenRequestAnyAuth.UpdateUser` + +
+
+ +
+
+ +**requestOptions:** `UsersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/scripts/rename-to-esm-files.js b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/scripts/rename-to-esm-files.js new file mode 100644 index 000000000000..0a03de13782f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/scripts/rename-to-esm-files.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +const fs = require("fs").promises; +const fsSync = require("fs"); +const path = require("path"); + +const extensionMap = { + ".js": ".mjs", + ".d.ts": ".d.mts", +}; +const oldExtensions = Object.keys(extensionMap); + +async function findFiles(rootPath) { + const files = []; + + async function scan(directory) { + const entries = await fs.readdir(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + if (entry.name !== "node_modules" && !entry.name.startsWith(".")) { + await scan(fullPath); + } + } else if (entry.isFile()) { + if (oldExtensions.some((ext) => entry.name.endsWith(ext))) { + files.push(fullPath); + } + } + } + } + + await scan(rootPath); + return files; +} + +async function updateFiles(files) { + const updatedFiles = []; + for (const file of files) { + const updated = await updateFileContents(file); + updatedFiles.push(updated); + } + + console.log(`Updated imports in ${updatedFiles.length} files.`); +} + +const KNOWN_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".jsx", ".json", ".ts", ".mts", ".cts", ".tsx", ".node"]); + +function hasFileExtension(importPath) { + const basename = path.basename(importPath); + const dotIndex = basename.lastIndexOf("."); + if (dotIndex <= 0) { + return false; + } + return KNOWN_EXTENSIONS.has(basename.slice(dotIndex).toLowerCase()); +} + +function resolveExtensionlessImport(dir, importPath) { + const resolvedPath = path.resolve(dir, importPath); + if (fsSync.existsSync(`${resolvedPath}.js`)) { + return `${importPath}.mjs`; + } + const indexPath = path.join(resolvedPath, "index.js"); + if (fsSync.existsSync(indexPath)) { + return `${importPath}/index.mjs`; + } + return null; +} + +async function updateFileContents(file) { + const content = await fs.readFile(file, "utf8"); + const dir = path.dirname(file); + + let newContent = content; + // Update each extension type defined in the map + for (const [oldExt, newExt] of Object.entries(extensionMap)) { + // Handle static imports/exports + const staticRegex = new RegExp(`(import|export)(.+from\\s+['"])(\\.\\.?\\/[^'"]+)(\\${oldExt})(['"])`, "g"); + newContent = newContent.replace(staticRegex, `$1$2$3${newExt}$5`); + + // Handle dynamic imports (yield import, await import, regular import()) + const dynamicRegex = new RegExp( + `(yield\\s+import|await\\s+import|import)\\s*\\(\\s*['"](\\.\\.\?\\/[^'"]+)(\\${oldExt})['"]\\s*\\)`, + "g", + ); + newContent = newContent.replace(dynamicRegex, `$1("$2${newExt}")`); + } + + // Handle extensionless relative imports (e.g. from "./oauth" or from "../utils"). + // These violate the ESM spec and break Node's ESM loader and Turbopack. + const staticExtensionless = /(import|export)(.+from\s+['"])(\.\.?\/[^'"]+?)(['"])/g; + const staticReplacements = []; + let match; + while ((match = staticExtensionless.exec(newContent)) !== null) { + const importPath = match[3]; + if (hasFileExtension(importPath)) continue; + const resolved = resolveExtensionlessImport(dir, importPath); + if (resolved != null) { + staticReplacements.push({ + start: match.index, + end: match.index + match[0].length, + replacement: `${match[1]}${match[2]}${resolved}${match[4]}`, + }); + } + } + for (const { start, end, replacement } of staticReplacements.reverse()) { + newContent = newContent.slice(0, start) + replacement + newContent.slice(end); + } + + // Handle extensionless dynamic imports + const dynamicExtensionless = /(yield\s+import|await\s+import|import)\s*\(\s*['"](\.\.?\/[^'"]+?)['"]\s*\)/g; + const dynamicReplacements = []; + while ((match = dynamicExtensionless.exec(newContent)) !== null) { + const importPath = match[2]; + if (hasFileExtension(importPath)) continue; + const resolved = resolveExtensionlessImport(dir, importPath); + if (resolved != null) { + dynamicReplacements.push({ + start: match.index, + end: match.index + match[0].length, + replacement: `${match[1]}("${resolved}")`, + }); + } + } + for (const { start, end, replacement } of dynamicReplacements.reverse()) { + newContent = newContent.slice(0, start) + replacement + newContent.slice(end); + } + + if (content !== newContent) { + await fs.writeFile(file, newContent, "utf8"); + return true; + } + return false; +} + +async function renameFiles(files) { + let counter = 0; + for (const file of files) { + const ext = oldExtensions.find((ext) => file.endsWith(ext)); + const newExt = extensionMap[ext]; + + if (newExt) { + const newPath = file.slice(0, -ext.length) + newExt; + await fs.rename(file, newPath); + counter++; + } + } + + console.log(`Renamed ${counter} files.`); +} + +async function main() { + try { + const targetDir = process.argv[2]; + if (!targetDir) { + console.error("Please provide a target directory"); + process.exit(1); + } + + const targetPath = path.resolve(targetDir); + const targetStats = await fs.stat(targetPath); + + if (!targetStats.isDirectory()) { + console.error("The provided path is not a directory"); + process.exit(1); + } + + console.log(`Scanning directory: ${targetDir}`); + + const files = await findFiles(targetDir); + + if (files.length === 0) { + console.log("No matching files found."); + process.exit(0); + } + + console.log(`Found ${files.length} files.`); + await updateFiles(files); + await renameFiles(files); + console.log("\nDone!"); + } catch (error) { + console.error("An error occurred:", error.message); + process.exit(1); + } +} + +main(); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json new file mode 100644 index 000000000000..f70a7de8cbc6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json @@ -0,0 +1,27 @@ +{ + "endpoints": [ + { + "id": { + "path": "/users/{id}", + "method": "PUT", + "identifier_override": "endpoint_users.updateUser" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUser({\n id: \"path-id\",\n body: {\n id: \"body-id\",\n name: \"Ada\"\n }\n});\n" + } + }, + { + "id": { + "path": "/users/{id}/profile", + "method": "PUT", + "identifier_override": "endpoint_users.updateUserProfile" + }, + "snippet": { + "type": "typescript", + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUserProfile(\"path-id\", {\n id: \"body-id\",\n name: \"Ada\"\n});\n" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/BaseClient.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/BaseClient.ts new file mode 100644 index 000000000000..5846bc819c78 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/BaseClient.ts @@ -0,0 +1,120 @@ +// This file was auto-generated by Fern from our API Definition. + +import { AnyAuthProvider } from "./auth/AnyAuthProvider.js"; +import { BearerAuthProvider } from "./auth/BearerAuthProvider.js"; +import { HeaderAuthProvider } from "./auth/HeaderAuthProvider.js"; +import { mergeHeaders } from "./core/headers.js"; +import * as core from "./core/index.js"; + +export type AuthOption = + | false + | core.AuthProvider["getAuthRequest"] + | core.AuthProvider + | AnyAuthProvider.AuthOptions<[BearerAuthProvider.AuthOptions, HeaderAuthProvider.AuthOptions]>; + +export type BaseClientOptions = { + environment: core.Supplier; + /** Specify a custom URL to connect the client to. */ + baseUrl?: core.Supplier; + /** Additional headers to include in requests. */ + headers?: Record | null | undefined>; + /** The default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** Provide a custom fetch implementation. Useful for platforms that don't have a built-in fetch or need a custom implementation. */ + fetch?: typeof fetch; + /** Configure logging for the client. */ + logging?: core.logging.LogConfig | core.logging.Logger; + /** Default options for SSE stream reconnection behavior. Has no effect on non-resumable endpoints. */ + stream?: { reconnectionEnabled?: boolean; maxReconnectionAttempts?: number }; + /** Override auth. Pass false to disable, a function returning auth headers, an AuthProvider, or auth options. */ + auth?: AuthOption; +} & AnyAuthProvider.AuthOptions<[BearerAuthProvider.AuthOptions, HeaderAuthProvider.AuthOptions]>; + +export interface BaseRequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Additional query string parameters to include in the request. */ + queryParams?: Record; + /** A dictionary containing additional parameters to spread into the request's body. */ + additionalBodyParameters?: Record; + /** Additional headers to include in the request. */ + headers?: Record | null | undefined>; + /** Options for SSE stream reconnection behavior. Has no effect on non-resumable endpoints. */ + stream?: { reconnectionEnabled?: boolean; maxReconnectionAttempts?: number }; +} + +export type NormalizedClientOptions = T & { + logging: core.logging.Logger; + authProvider?: core.AuthProvider; +}; + +export type NormalizedClientOptionsWithAuth = + NormalizedClientOptions & { + authProvider: core.AuthProvider; + }; + +export function normalizeClientOptions( + options: T, +): NormalizedClientOptions { + const headers = mergeHeaders( + { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fern/ts-flatten-request-any-auth", + "X-Fern-SDK-Version": "0.0.1", + "User-Agent": "@fern/ts-flatten-request-any-auth/0.0.1", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + }, + options?.headers, + ); + + return { + ...options, + logging: core.logging.createLogger(options?.logging), + headers, + } as NormalizedClientOptions; +} + +export function normalizeClientOptionsWithAuth( + options: T, +): NormalizedClientOptionsWithAuth { + const normalized = normalizeClientOptions(options) as NormalizedClientOptionsWithAuth; + + if (options.auth === false) { + normalized.authProvider = new core.NoOpAuthProvider(); + return normalized; + } + if (options.auth != null) { + if (typeof options.auth === "function") { + normalized.authProvider = { getAuthRequest: options.auth }; + return normalized; + } + if (core.isAuthProvider(options.auth)) { + normalized.authProvider = options.auth; + return normalized; + } + Object.assign(normalized, options.auth); + } + + const normalizedWithNoOpAuthProvider = withNoOpAuthProvider(normalized); + normalized.authProvider ??= AnyAuthProvider.createInstance(normalizedWithNoOpAuthProvider, [ + BearerAuthProvider, + HeaderAuthProvider, + ]); + return normalized; +} + +function withNoOpAuthProvider( + options: NormalizedClientOptions, +): NormalizedClientOptionsWithAuth { + return { + ...options, + authProvider: new core.NoOpAuthProvider(), + }; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/Client.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/Client.ts new file mode 100644 index 000000000000..04f3e719463d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/Client.ts @@ -0,0 +1,56 @@ +// This file was auto-generated by Fern from our API Definition. + +import { UsersClient } from "./api/resources/users/client/Client.js"; +import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; + +export declare namespace SeedTsFlattenRequestAnyAuthClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class SeedTsFlattenRequestAnyAuthClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + protected _users: UsersClient | undefined; + + constructor(options: SeedTsFlattenRequestAnyAuthClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + public get users(): UsersClient { + return (this._users ??= new UsersClient(this._options)); + } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + baseUrl: this._options.baseUrl ?? this._options.environment, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/index.ts new file mode 100644 index 000000000000..e445af0d831e --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/index.ts @@ -0,0 +1 @@ +export * from "./resources/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/index.ts new file mode 100644 index 000000000000..eede1737b98f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/index.ts @@ -0,0 +1,3 @@ +export * from "./users/client/requests/index.js"; +export * as users from "./users/index.js"; +export * from "./users/types/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts new file mode 100644 index 000000000000..1c3bfad7625b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts @@ -0,0 +1,163 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import { mergeAdditionalBodyParameters } from "../../../../core/requestBody.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import type * as SeedTsFlattenRequestAnyAuth from "../../../index.js"; + +export declare namespace UsersClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class UsersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + + constructor(options: UsersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + /** + * @param {SeedTsFlattenRequestAnyAuth.UpdateUserRequest} request + * @param {UsersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SeedTsFlattenRequestAnyAuthError} + * @throws {@link errors.SeedTsFlattenRequestAnyAuthTimeoutError} + * + * @example + * await client.users.updateUser({ + * id: "path-id", + * body: { + * id: "body-id", + * name: "Ada" + * } + * }) + */ + public updateUser( + request: SeedTsFlattenRequestAnyAuth.UpdateUserRequest, + requestOptions?: UsersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__updateUser(request, requestOptions)); + } + + private async __updateUser( + request: SeedTsFlattenRequestAnyAuth.UpdateUserRequest, + requestOptions?: UsersClient.RequestOptions, + ): Promise> { + const { id, body: _body } = request; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `users/${core.url.encodePathParam(id)}`, + ), + method: "PUT", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters(_body, requestOptions?.additionalBodyParameters), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as SeedTsFlattenRequestAnyAuth.UpdateUser, + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/users/{id}"); + } + + /** + * @param {string} id + * @param {SeedTsFlattenRequestAnyAuth.UpdateUser} request + * @param {UsersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link errors.SeedTsFlattenRequestAnyAuthError} + * @throws {@link errors.SeedTsFlattenRequestAnyAuthTimeoutError} + * + * @example + * await client.users.updateUserProfile("path-id", { + * id: "body-id", + * name: "Ada" + * }) + */ + public updateUserProfile( + id: string, + request: SeedTsFlattenRequestAnyAuth.UpdateUser, + requestOptions?: UsersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__updateUserProfile(id, request, requestOptions)); + } + + private async __updateUserProfile( + id: string, + request: SeedTsFlattenRequestAnyAuth.UpdateUser, + requestOptions?: UsersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await core.fetcher({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `users/${core.url.encodePathParam(id)}/profile`, + ), + method: "PUT", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters(request, requestOptions?.additionalBodyParameters), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: _response.body as SeedTsFlattenRequestAnyAuth.UpdateUser, + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/users/{id}/profile"); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/index.ts new file mode 100644 index 000000000000..195f9aa8a846 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts new file mode 100644 index 000000000000..823ba1676d92 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as SeedTsFlattenRequestAnyAuth from "../../../../index.js"; + +/** + * @example + * { + * id: "path-id", + * body: { + * id: "body-id", + * name: "Ada" + * } + * } + */ +export interface UpdateUserRequest { + id: string; + body: SeedTsFlattenRequestAnyAuth.UpdateUser; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/index.ts new file mode 100644 index 000000000000..2292f5395065 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/index.ts @@ -0,0 +1 @@ +export type { UpdateUserRequest } from "./UpdateUserRequest.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/exports.ts new file mode 100644 index 000000000000..788add4edbfc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { UsersClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/index.ts new file mode 100644 index 000000000000..d9adb1af9a93 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/index.ts @@ -0,0 +1,2 @@ +export * from "./client/index.js"; +export * from "./types/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/UpdateUser.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/UpdateUser.ts new file mode 100644 index 000000000000..c377e06cd091 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/UpdateUser.ts @@ -0,0 +1,6 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface UpdateUser { + id: string; + name: string; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/index.ts new file mode 100644 index 000000000000..d6915ffe9774 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/types/index.ts @@ -0,0 +1 @@ +export * from "./UpdateUser.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/AnyAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/AnyAuthProvider.ts new file mode 100644 index 000000000000..e97994fb2fe6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/AnyAuthProvider.ts @@ -0,0 +1,59 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { NormalizedClientOptions } from "../BaseClient.js"; +import type * as core from "../core/index.js"; + +export class AnyAuthProvider implements core.AuthProvider { + private readonly authProviders: core.AuthProvider[]; + + constructor(authProviders: core.AuthProvider[]) { + this.authProviders = authProviders; + } + + public async getAuthRequest(arg?: { endpointMetadata?: core.EndpointMetadata }): Promise { + const availableProviders = this.authProviders; + + for (const provider of availableProviders) { + try { + const authRequest = await provider.getAuthRequest(arg); + if (authRequest.headers.Authorization != null || Object.keys(authRequest.headers).length > 0) { + return authRequest; + } + } catch (_e) { + // Continue to next auth provider + } + } + + // No auth credentials found + throw new Error( + "No authentication credentials provided. Please provide one of the supported authentication methods.", + ); + } +} + +export namespace AnyAuthProvider { + type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + + type AtLeastOneOf = { + [K in keyof T]: T[K] & Partial>>; + }[number]; + + export type AuthOptions = AtLeastOneOf; + export type Options = Partial>; + + type InstantiatableAuthProvider = { + canCreate: (opts: NormalizedClientOptions) => boolean; + createInstance: (opts: NormalizedClientOptions) => core.AuthProvider; + }; + + export function createInstance( + options: NormalizedClientOptions, + authProviderClasses: InstantiatableAuthProvider[], + ): core.AuthProvider { + const authProviders: core.AuthProvider[] = authProviderClasses + .filter((providerClass) => providerClass.canCreate(options)) + .map((providerClass) => providerClass.createInstance(options)); + + return new AnyAuthProvider(authProviders); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/BearerAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/BearerAuthProvider.ts new file mode 100644 index 000000000000..2aa01c47ca50 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/BearerAuthProvider.ts @@ -0,0 +1,52 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as core from "../core/index.js"; +import * as errors from "../errors/index.js"; + +const WRAPPER_PROPERTY = "bearerAuth" as const; +const TOKEN_PARAM = "token" as const; +const ENV_TOKEN = "MY_TOKEN" as const; + +export class BearerAuthProvider implements core.AuthProvider { + private readonly options: BearerAuthProvider.Options; + + constructor(options: BearerAuthProvider.Options) { + this.options = options; + } + + public static canCreate(options: Partial): boolean { + return options?.[WRAPPER_PROPERTY]?.[TOKEN_PARAM] != null || process.env?.[ENV_TOKEN] != null; + } + + public async getAuthRequest({ + endpointMetadata, + }: { + endpointMetadata?: core.EndpointMetadata; + } = {}): Promise { + const token = + (await core.Supplier.get(this.options[WRAPPER_PROPERTY]?.[TOKEN_PARAM])) ?? process.env?.[ENV_TOKEN]; + if (token == null) { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: BearerAuthProvider.AUTH_CONFIG_ERROR_MESSAGE, + }); + } + + return { + headers: { Authorization: `Bearer ${token}` }, + }; + } +} + +export namespace BearerAuthProvider { + export const AUTH_SCHEME = "BearerAuth" as const; + export const AUTH_CONFIG_ERROR_MESSAGE: string = + `Please provide '${TOKEN_PARAM}' when initializing the client, or set the '${ENV_TOKEN}' environment variable` as const; + export type Options = AuthOptions; + export type AuthOptions = { + [WRAPPER_PROPERTY]?: { [TOKEN_PARAM]?: core.Supplier | undefined }; + }; + + export function createInstance(options: Options): core.AuthProvider { + return new BearerAuthProvider(options); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/HeaderAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/HeaderAuthProvider.ts new file mode 100644 index 000000000000..74e84b14a6c0 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/HeaderAuthProvider.ts @@ -0,0 +1,53 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as core from "../core/index.js"; +import * as errors from "../errors/index.js"; + +const WRAPPER_PROPERTY = "apiKey" as const; +const PARAM_KEY = "apiKey" as const; +const ENV_HEADER_KEY = "MY_API_KEY" as const; +const HEADER_NAME = "X-API-Key" as const; + +export class HeaderAuthProvider implements core.AuthProvider { + private readonly options: HeaderAuthProvider.Options; + + constructor(options: HeaderAuthProvider.Options) { + this.options = options; + } + + public static canCreate(options: Partial): boolean { + return options?.[WRAPPER_PROPERTY]?.[PARAM_KEY] != null || process.env?.[ENV_HEADER_KEY] != null; + } + + public async getAuthRequest({ + endpointMetadata, + }: { + endpointMetadata?: core.EndpointMetadata; + } = {}): Promise { + const headerValue = + (await core.Supplier.get(this.options[WRAPPER_PROPERTY]?.[PARAM_KEY])) ?? process.env?.[ENV_HEADER_KEY]; + if (headerValue == null) { + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: HeaderAuthProvider.AUTH_CONFIG_ERROR_MESSAGE, + }); + } + + return { + headers: { [HEADER_NAME]: headerValue }, + }; + } +} + +export namespace HeaderAuthProvider { + export const AUTH_SCHEME = "ApiKey" as const; + export const AUTH_CONFIG_ERROR_MESSAGE: string = + `Please provide '${PARAM_KEY}' when initializing the client, or set the '${ENV_HEADER_KEY}' environment variable` as const; + export type Options = AuthOptions; + export type AuthOptions = { + [WRAPPER_PROPERTY]?: { [PARAM_KEY]?: core.Supplier | undefined }; + }; + + export function createInstance(options: Options): core.AuthProvider { + return new HeaderAuthProvider(options); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/index.ts new file mode 100644 index 000000000000..cfdbd6f9a9e5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/auth/index.ts @@ -0,0 +1,3 @@ +export { AnyAuthProvider } from "./AnyAuthProvider.js"; +export { BearerAuthProvider } from "./BearerAuthProvider.js"; +export { HeaderAuthProvider } from "./HeaderAuthProvider.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthProvider.ts new file mode 100644 index 000000000000..c9478669fb89 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthProvider.ts @@ -0,0 +1,15 @@ +import type { EndpointMetadata } from "../fetcher/EndpointMetadata.js"; +import type { AuthRequest } from "./AuthRequest.js"; + +export interface AuthProvider { + getAuthRequest(arg?: { endpointMetadata?: EndpointMetadata }): Promise; +} + +export function isAuthProvider(value: unknown): value is AuthProvider { + return ( + typeof value === "object" && + value !== null && + "getAuthRequest" in value && + typeof value.getAuthRequest === "function" + ); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthRequest.ts new file mode 100644 index 000000000000..f6218b42211e --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/AuthRequest.ts @@ -0,0 +1,9 @@ +/** + * Request parameters for authentication requests. + */ +export interface AuthRequest { + /** + * The headers to be included in the request. + */ + headers: Record; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BasicAuth.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BasicAuth.ts new file mode 100644 index 000000000000..f34fca5cc4dd --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BasicAuth.ts @@ -0,0 +1,37 @@ +import { base64Decode, base64Encode } from "../base64.js"; + +export interface BasicAuth { + username?: string; + password?: string; +} + +const BASIC_AUTH_HEADER_PREFIX = /^Basic /i; + +export const BasicAuth = { + toAuthorizationHeader: (basicAuth: BasicAuth | undefined): string | undefined => { + if (basicAuth == null) { + return undefined; + } + const username = basicAuth.username ?? ""; + const password = basicAuth.password ?? ""; + if (username === "" && password === "") { + return undefined; + } + const token = base64Encode(`${username}:${password}`); + return `Basic ${token}`; + }, + fromAuthorizationHeader: (header: string): BasicAuth => { + const credentials = header.replace(BASIC_AUTH_HEADER_PREFIX, ""); + const decoded = base64Decode(credentials); + const [username, ...passwordParts] = decoded.split(":"); + const password = passwordParts.length > 0 ? passwordParts.join(":") : undefined; + + if (username == null || password == null) { + throw new Error("Invalid basic auth"); + } + return { + username, + password, + }; + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BearerToken.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BearerToken.ts new file mode 100644 index 000000000000..c44a06c38f06 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/BearerToken.ts @@ -0,0 +1,20 @@ +export type BearerToken = string; + +const BEARER_AUTH_HEADER_PREFIX = /^Bearer /i; + +function toAuthorizationHeader(token: string | undefined): string | undefined { + if (token == null) { + return undefined; + } + return `Bearer ${token}`; +} + +export const BearerToken: { + toAuthorizationHeader: typeof toAuthorizationHeader; + fromAuthorizationHeader: (header: string) => BearerToken; +} = { + toAuthorizationHeader: toAuthorizationHeader, + fromAuthorizationHeader: (header: string): BearerToken => { + return header.replace(BEARER_AUTH_HEADER_PREFIX, "").trim() as BearerToken; + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/NoOpAuthProvider.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/NoOpAuthProvider.ts new file mode 100644 index 000000000000..5b7acfd2bd8b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/NoOpAuthProvider.ts @@ -0,0 +1,8 @@ +import type { AuthProvider } from "./AuthProvider.js"; +import type { AuthRequest } from "./AuthRequest.js"; + +export class NoOpAuthProvider implements AuthProvider { + public getAuthRequest(): Promise { + return Promise.resolve({ headers: {} }); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/index.ts new file mode 100644 index 000000000000..77effd090dce --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/auth/index.ts @@ -0,0 +1,5 @@ +export { type AuthProvider, isAuthProvider } from "./AuthProvider.js"; +export type { AuthRequest } from "./AuthRequest.js"; +export { BasicAuth } from "./BasicAuth.js"; +export { BearerToken } from "./BearerToken.js"; +export { NoOpAuthProvider } from "./NoOpAuthProvider.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/base64.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/base64.ts new file mode 100644 index 000000000000..448a0db638a6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/base64.ts @@ -0,0 +1,27 @@ +function base64ToBytes(base64: string): Uint8Array { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)!); +} + +function bytesToBase64(bytes: Uint8Array): string { + const binString = String.fromCodePoint(...bytes); + return btoa(binString); +} + +export function base64Encode(input: string): string { + if (typeof Buffer !== "undefined") { + return Buffer.from(input, "utf8").toString("base64"); + } + + const bytes = new TextEncoder().encode(input); + return bytesToBase64(bytes); +} + +export function base64Decode(input: string): string { + if (typeof Buffer !== "undefined") { + return Buffer.from(input, "base64").toString("utf8"); + } + + const bytes = base64ToBytes(input); + return new TextDecoder().decode(bytes); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/exports.ts new file mode 100644 index 000000000000..69296d7100d6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/exports.ts @@ -0,0 +1 @@ +export * from "./logging/exports.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/APIResponse.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/APIResponse.ts new file mode 100644 index 000000000000..97ab83c2b195 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/APIResponse.ts @@ -0,0 +1,23 @@ +import type { RawResponse } from "./RawResponse.js"; + +/** + * The response of an API call. + * It is a successful response or a failed response. + */ +export type APIResponse = SuccessfulResponse | FailedResponse; + +export interface SuccessfulResponse { + ok: true; + body: T; + /** + * @deprecated Use `rawResponse` instead + */ + headers?: Record; + rawResponse: RawResponse; +} + +export interface FailedResponse { + ok: false; + error: T; + rawResponse: RawResponse; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/BinaryResponse.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/BinaryResponse.ts new file mode 100644 index 000000000000..b9e40fb62cc4 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/BinaryResponse.ts @@ -0,0 +1,34 @@ +export type BinaryResponse = { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + bodyUsed: Response["bodyUsed"]; + /** + * Returns a ReadableStream of the response body. + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) + */ + stream: () => Response["body"]; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer: () => ReturnType; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob: () => ReturnType; + /** + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) + * Some versions of the Fetch API may not support this method. + */ + bytes?(): Promise; +}; + +export function getBinaryResponse(response: Response): BinaryResponse { + const binaryResponse: BinaryResponse = { + get bodyUsed() { + return response.bodyUsed; + }, + stream: () => response.body, + arrayBuffer: response.arrayBuffer.bind(response), + blob: response.blob.bind(response), + }; + if ("bytes" in response && typeof response.bytes === "function") { + binaryResponse.bytes = response.bytes.bind(response); + } + + return binaryResponse; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointMetadata.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointMetadata.ts new file mode 100644 index 000000000000..998d68f5c20c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointMetadata.ts @@ -0,0 +1,13 @@ +export type SecuritySchemeKey = string; +/** + * A collection of security schemes, where the key is the name of the security scheme and the value is the list of scopes required for that scheme. + * All schemes in the collection must be satisfied for authentication to be successful. + */ +export type SecuritySchemeCollection = Record; +export type AuthScope = string; +export type EndpointMetadata = { + /** + * An array of security scheme collections. Each collection represents an alternative way to authenticate. + */ + security?: SecuritySchemeCollection[]; +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointSupplier.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointSupplier.ts new file mode 100644 index 000000000000..aad81f0d9040 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/EndpointSupplier.ts @@ -0,0 +1,14 @@ +import type { EndpointMetadata } from "./EndpointMetadata.js"; +import type { Supplier } from "./Supplier.js"; + +type EndpointSupplierFn = (arg: { endpointMetadata?: EndpointMetadata }) => T | Promise; +export type EndpointSupplier = Supplier | EndpointSupplierFn; +export const EndpointSupplier = { + get: async (supplier: EndpointSupplier, arg: { endpointMetadata?: EndpointMetadata }): Promise => { + if (typeof supplier === "function") { + return (supplier as EndpointSupplierFn)(arg); + } else { + return supplier; + } + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Fetcher.ts new file mode 100644 index 000000000000..cd5c5793d670 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Fetcher.ts @@ -0,0 +1,311 @@ +import { toJson } from "../json.js"; +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import type { APIResponse } from "./APIResponse.js"; +import { createRequestUrl } from "./createRequestUrl.js"; +import type { EndpointMetadata } from "./EndpointMetadata.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getErrorResponseBody } from "./getErrorResponseBody.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { getRequestBody } from "./getRequestBody.js"; +import { getResponseBody } from "./getResponseBody.js"; +import { Headers } from "./Headers.js"; +import { makeRequest } from "./makeRequest.js"; +import { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; +import { redactUrl, SENSITIVE_QUERY_PARAMS } from "./redactUrl.js"; +import { requestWithRetries } from "./requestWithRetries.js"; + +export type FetchFunction = (args: Fetcher.Args) => Promise>; + +export declare namespace Fetcher { + export interface Args { + url: string; + method: string; + contentType?: string; + headers?: Record; + /** + * @deprecated Prefer `queryString` (produced by `core.url.queryBuilder()`). + * Retained for backwards compatibility with custom fetchers and callers that + * still construct request args with a query-parameter object. + */ + queryParameters?: Record; + queryString?: string; + body?: unknown; + timeoutMs?: number; + maxRetries?: number; + withCredentials?: boolean; + abortSignal?: AbortSignal; + requestType?: "json" | "file" | "bytes" | "form" | "other"; + responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response"; + duplex?: "half"; + endpointMetadata?: EndpointMetadata; + fetchFn?: typeof fetch; + logging?: LogConfig | Logger; + } + + export type Error = FailedStatusCodeError | NonJsonError | BodyIsNullError | TimeoutError | UnknownError; + + export interface FailedStatusCodeError { + reason: "status-code"; + statusCode: number; + body: unknown; + } + + export interface NonJsonError { + reason: "non-json"; + statusCode: number; + rawBody: string; + } + + export interface BodyIsNullError { + reason: "body-is-null"; + statusCode: number; + } + + export interface TimeoutError { + reason: "timeout"; + cause?: unknown; + } + + export interface UnknownError { + reason: "unknown"; + errorMessage: string; + cause?: unknown; + } +} + +const SENSITIVE_HEADERS = new Set([ + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", +]); + +function redactHeaders(headers: Headers | Record): Record { + const filtered: Record = {}; + for (const [key, value] of headers instanceof Headers ? headers.entries() : Object.entries(headers)) { + if (SENSITIVE_HEADERS.has(key.toLowerCase())) { + filtered[key] = "[REDACTED]"; + } else { + filtered[key] = value; + } + } + return filtered; +} + +function redactQueryParameters( + queryParameters: Record | undefined, +): Record | undefined { + if (queryParameters == null) { + return undefined; + } + const redacted: Record = {}; + for (const [key, value] of Object.entries(queryParameters)) { + redacted[key] = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()) ? "[REDACTED]" : value; + } + return redacted; +} + +async function getHeaders(args: Fetcher.Args): Promise { + const newHeaders: Headers = new Headers(); + + newHeaders.set( + "Accept", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", + ); + if (args.body !== undefined && args.contentType != null) { + newHeaders.set("Content-Type", args.contentType); + } + + if (args.headers == null) { + return newHeaders; + } + + for (const [key, value] of Object.entries(args.headers)) { + const result = await EndpointSupplier.get(value, { endpointMetadata: args.endpointMetadata ?? {} }); + if (typeof result === "string") { + newHeaders.set(key, result); + continue; + } + if (result == null) { + continue; + } + newHeaders.set(key, `${result}`); + } + return newHeaders; +} + +export async function fetcherImpl(args: Fetcher.Args): Promise> { + let url = args.url; + if (args.queryString != null && args.queryString.length > 0) { + url = `${url}?${args.queryString}`; + } else { + url = createRequestUrl(args.url, args.queryParameters); + } + const requestBody: BodyInit | undefined = await getRequestBody({ + body: args.body, + type: args.requestType ?? "other", + }); + const fetchFn = args.fetchFn ?? (await getFetchFn()); + const headers = await getHeaders(args); + const logger = createLogger(args.logging); + + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + headers: redactHeaders(headers), + queryParameters: redactQueryParameters(args.queryParameters), + hasBody: requestBody != null, + }; + logger.debug("Making HTTP request", metadata); + } + + try { + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + url, + args.method, + headers, + requestBody, + args.timeoutMs, + args.abortSignal, + args.withCredentials, + args.duplex, + args.responseType === "streaming" || args.responseType === "sse", + ), + args.maxRetries, + ); + + if (response.status >= 200 && response.status < 400) { + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(response.headers), + }; + logger.debug("HTTP request succeeded", metadata); + } + const body = await getResponseBody(response, args.responseType); + return { + ok: true, + body: body as R, + headers: response.headers, + rawResponse: toRawResponse(response), + }; + } else { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())), + }; + logger.error("HTTP request failed with error status", metadata); + } + return { + ok: false, + error: { + reason: "status-code", + statusCode: response.status, + body: await getErrorResponseBody(response), + }, + rawResponse: toRawResponse(response), + }; + } + } catch (error) { + if (args.abortSignal?.aborted) { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + }; + logger.error("HTTP request was aborted", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: "The user aborted a request", + cause: error, + }, + rawResponse: abortRawResponse, + }; + } else if (error instanceof Error && error.name === "AbortError") { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + timeoutMs: args.timeoutMs, + }; + logger.error("HTTP request timed out", metadata); + } + return { + ok: false, + error: { + reason: "timeout", + cause: error, + }, + rawResponse: abortRawResponse, + }; + } else if (error instanceof Error) { + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + errorMessage: error.message, + }; + logger.error("HTTP request failed with error", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: error.message, + cause: error, + }, + rawResponse: unknownRawResponse, + }; + } + + if (logger.isError()) { + const metadata = { + method: args.method, + url: redactUrl(url), + error: toJson(error), + }; + logger.error("HTTP request failed with unknown error", metadata); + } + return { + ok: false, + error: { + reason: "unknown", + errorMessage: toJson(error), + cause: error, + }, + rawResponse: unknownRawResponse, + }; + } +} + +export const fetcher: FetchFunction = fetcherImpl; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Headers.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Headers.ts new file mode 100644 index 000000000000..f02246c50757 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Headers.ts @@ -0,0 +1,93 @@ +let Headers: typeof globalThis.Headers; + +if (typeof globalThis.Headers !== "undefined") { + Headers = globalThis.Headers; +} else { + Headers = class Headers implements Headers { + private headers: Map; + + constructor(init?: HeadersInit) { + this.headers = new Map(); + + if (init) { + if (init instanceof Headers) { + init.forEach((value, key) => this.append(key, value)); + } else if (Array.isArray(init)) { + for (const [key, value] of init) { + if (typeof key === "string" && typeof value === "string") { + this.append(key, value); + } else { + throw new TypeError("Each header entry must be a [string, string] tuple"); + } + } + } else { + for (const [key, value] of Object.entries(init)) { + if (typeof value === "string") { + this.append(key, value); + } else { + throw new TypeError("Header values must be strings"); + } + } + } + } + } + + append(name: string, value: string): void { + const key = name.toLowerCase(); + const existing = this.headers.get(key) || []; + this.headers.set(key, [...existing, value]); + } + + delete(name: string): void { + const key = name.toLowerCase(); + this.headers.delete(key); + } + + get(name: string): string | null { + const key = name.toLowerCase(); + const values = this.headers.get(key); + return values ? values.join(", ") : null; + } + + has(name: string): boolean { + const key = name.toLowerCase(); + return this.headers.has(key); + } + + set(name: string, value: string): void { + const key = name.toLowerCase(); + this.headers.set(key, [value]); + } + + forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void { + const boundCallback = thisArg ? callbackfn.bind(thisArg) : callbackfn; + this.headers.forEach((values, key) => boundCallback(values.join(", "), key, this)); + } + + getSetCookie(): string[] { + return this.headers.get("set-cookie") || []; + } + + *entries(): IterableIterator<[string, string]> { + for (const [key, values] of this.headers.entries()) { + yield [key, values.join(", ")]; + } + } + + *keys(): IterableIterator { + yield* this.headers.keys(); + } + + *values(): IterableIterator { + for (const values of this.headers.values()) { + yield values.join(", "); + } + } + + [Symbol.iterator](): IterableIterator<[string, string]> { + return this.entries(); + } + }; +} + +export { Headers }; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/HttpResponsePromise.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/HttpResponsePromise.ts new file mode 100644 index 000000000000..692ca7d795f0 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/HttpResponsePromise.ts @@ -0,0 +1,116 @@ +import type { WithRawResponse } from "./RawResponse.js"; + +/** + * A promise that returns the parsed response and lets you retrieve the raw response too. + */ +export class HttpResponsePromise extends Promise { + private innerPromise: Promise>; + private unwrappedPromise: Promise | undefined; + + private constructor(promise: Promise>) { + // Initialize with a no-op to avoid premature parsing + super((resolve) => { + resolve(undefined as unknown as T); + }); + this.innerPromise = promise; + } + + /** + * Creates an `HttpResponsePromise` from a function that returns a promise. + * + * @param fn - A function that returns a promise resolving to a `WithRawResponse` object. + * @param args - Arguments to pass to the function. + * @returns An `HttpResponsePromise` instance. + */ + public static fromFunction Promise>, T>( + fn: F, + ...args: Parameters + ): HttpResponsePromise { + return new HttpResponsePromise(fn(...args)); + } + + /** + * Creates a function that returns an `HttpResponsePromise` from a function that returns a promise. + * + * @param fn - A function that returns a promise resolving to a `WithRawResponse` object. + * @returns A function that returns an `HttpResponsePromise` instance. + */ + public static interceptFunction< + F extends (...args: never[]) => Promise>, + T = Awaited>["data"], + >(fn: F): (...args: Parameters) => HttpResponsePromise { + return (...args: Parameters): HttpResponsePromise => { + return HttpResponsePromise.fromPromise(fn(...args)); + }; + } + + /** + * Creates an `HttpResponsePromise` from an existing promise. + * + * @param promise - A promise resolving to a `WithRawResponse` object. + * @returns An `HttpResponsePromise` instance. + */ + public static fromPromise(promise: Promise>): HttpResponsePromise { + return new HttpResponsePromise(promise); + } + + /** + * Creates an `HttpResponsePromise` from an executor function. + * + * @param executor - A function that takes resolve and reject callbacks to create a promise. + * @returns An `HttpResponsePromise` instance. + */ + public static fromExecutor( + executor: (resolve: (value: WithRawResponse) => void, reject: (reason?: unknown) => void) => void, + ): HttpResponsePromise { + const promise = new Promise>(executor); + return new HttpResponsePromise(promise); + } + + /** + * Creates an `HttpResponsePromise` from a resolved result. + * + * @param result - A `WithRawResponse` object to resolve immediately. + * @returns An `HttpResponsePromise` instance. + */ + public static fromResult(result: WithRawResponse): HttpResponsePromise { + const promise = Promise.resolve(result); + return new HttpResponsePromise(promise); + } + + private unwrap(): Promise { + if (!this.unwrappedPromise) { + this.unwrappedPromise = this.innerPromise.then(({ data }) => data); + } + return this.unwrappedPromise; + } + + /** @inheritdoc */ + public override then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ): Promise { + return this.unwrap().then(onfulfilled, onrejected); + } + + /** @inheritdoc */ + public override catch( + onrejected?: ((reason: unknown) => TResult | PromiseLike) | null, + ): Promise { + return this.unwrap().catch(onrejected); + } + + /** @inheritdoc */ + public override finally(onfinally?: (() => void) | null): Promise { + return this.unwrap().finally(onfinally); + } + + /** + * Retrieves the data and raw response. + * + * @returns A promise resolving to a `WithRawResponse` object. + */ + public async withRawResponse(): Promise> { + return await this.innerPromise; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/RawResponse.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/RawResponse.ts new file mode 100644 index 000000000000..37fb44e2aa99 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/RawResponse.ts @@ -0,0 +1,61 @@ +import { Headers } from "./Headers.js"; + +/** + * The raw response from the fetch call excluding the body. + */ +export type RawResponse = Omit< + { + [K in keyof Response as Response[K] extends Function ? never : K]: Response[K]; // strips out functions + }, + "ok" | "body" | "bodyUsed" +>; // strips out body and bodyUsed + +/** + * A raw response indicating that the request was aborted. + */ +export const abortRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 499, + statusText: "Client Closed Request", + type: "error", + url: "", +} as const; + +/** + * A raw response indicating an unknown error. + */ +export const unknownRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 0, + statusText: "Unknown Error", + type: "error", + url: "", +} as const; + +/** + * Converts a `RawResponse` object into a `RawResponse` by extracting its properties, + * excluding the `body` and `bodyUsed` fields. + * + * @param response - The `RawResponse` object to convert. + * @returns A `RawResponse` object containing the extracted properties of the input response. + */ +export function toRawResponse(response: Response): RawResponse { + return { + headers: response.headers, + redirected: response.redirected, + status: response.status, + statusText: response.statusText, + type: response.type, + url: response.url, + }; +} + +/** + * Creates a `RawResponse` from a standard `Response` object. + */ +export interface WithRawResponse { + readonly data: T; + readonly rawResponse: RawResponse; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Supplier.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Supplier.ts new file mode 100644 index 000000000000..867c931c02f4 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/Supplier.ts @@ -0,0 +1,11 @@ +export type Supplier = T | Promise | (() => T | Promise); + +export const Supplier = { + get: async (supplier: Supplier): Promise => { + if (typeof supplier === "function") { + return (supplier as () => T)(); + } else { + return supplier; + } + }, +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/createRequestUrl.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/createRequestUrl.ts new file mode 100644 index 000000000000..88e13265e112 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/createRequestUrl.ts @@ -0,0 +1,6 @@ +import { toQueryString } from "../url/qs.js"; + +export function createRequestUrl(baseUrl: string, queryParameters?: Record): string { + const queryString = toQueryString(queryParameters, { arrayFormat: "repeat" }); + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getErrorResponseBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getErrorResponseBody.ts new file mode 100644 index 000000000000..7cf4e623c2f5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getErrorResponseBody.ts @@ -0,0 +1,33 @@ +import { fromJson } from "../json.js"; +import { getResponseBody } from "./getResponseBody.js"; + +export async function getErrorResponseBody(response: Response): Promise { + let contentType = response.headers.get("Content-Type")?.toLowerCase(); + if (contentType == null || contentType.length === 0) { + return getResponseBody(response); + } + + if (contentType.indexOf(";") !== -1) { + contentType = contentType.split(";")[0]?.trim() ?? ""; + } + switch (contentType) { + case "application/hal+json": + case "application/json": + case "application/ld+json": + case "application/problem+json": + case "application/vnd.api+json": + case "text/json": { + const text = await response.text(); + return text.length > 0 ? fromJson(text) : undefined; + } + default: + if (contentType.startsWith("application/vnd.") && contentType.endsWith("+json")) { + const text = await response.text(); + return text.length > 0 ? fromJson(text) : undefined; + } + + // Fallback to plain text if content type is not recognized + // Even if no body is present, the response will be an empty string + return await response.text(); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getFetchFn.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getFetchFn.ts new file mode 100644 index 000000000000..9f845b956392 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getFetchFn.ts @@ -0,0 +1,3 @@ +export async function getFetchFn(): Promise { + return fetch; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getHeader.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getHeader.ts new file mode 100644 index 000000000000..50f922b0e87f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getHeader.ts @@ -0,0 +1,8 @@ +export function getHeader(headers: Record, header: string): string | undefined { + for (const [headerKey, headerValue] of Object.entries(headers)) { + if (headerKey.toLowerCase() === header.toLowerCase()) { + return headerValue; + } + } + return undefined; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getRequestBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getRequestBody.ts new file mode 100644 index 000000000000..91d9d81f50e5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getRequestBody.ts @@ -0,0 +1,20 @@ +import { toJson } from "../json.js"; +import { toQueryString } from "../url/qs.js"; + +export declare namespace GetRequestBody { + interface Args { + body: unknown; + type: "json" | "file" | "bytes" | "form" | "other"; + } +} + +export async function getRequestBody({ body, type }: GetRequestBody.Args): Promise { + if (type === "form") { + return toQueryString(body, { arrayFormat: "repeat", encode: true }); + } + if (type.includes("json")) { + return toJson(body); + } else { + return body as BodyInit; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getResponseBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getResponseBody.ts new file mode 100644 index 000000000000..2e831e4956e8 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/getResponseBody.ts @@ -0,0 +1,70 @@ +import { fromJson } from "../json.js"; +import { getBinaryResponse } from "./BinaryResponse.js"; + +// Pins the upstream Response so undici's FinalizationRegistry can't GC it and cancel the body stream. +function retainResponse(target: object, response: Response): void { + Object.defineProperty(target, "__fern_response_ref", { + value: response, + enumerable: false, + configurable: true, + writable: false, + }); +} + +export async function getResponseBody(response: Response, responseType?: string): Promise { + switch (responseType) { + case "binary-response": + return getBinaryResponse(response); + case "blob": + return await response.blob(); + case "arrayBuffer": + return await response.arrayBuffer(); + case "sse": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + retainResponse(response.body, response); + return response.body; + case "streaming": + if (response.body == null) { + return { + ok: false, + error: { + reason: "body-is-null", + statusCode: response.status, + }, + }; + } + + retainResponse(response.body, response); + return response.body; + + case "text": + return await response.text(); + } + + // if responseType is "json" or not specified, try to parse as JSON + const text = await response.text(); + if (text.length > 0) { + try { + const responseBody = fromJson(text); + return responseBody; + } catch (_err) { + return { + ok: false, + error: { + reason: "non-json", + statusCode: response.status, + rawBody: text, + }, + }; + } + } + return undefined; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/index.ts new file mode 100644 index 000000000000..bd5db362c778 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/index.ts @@ -0,0 +1,13 @@ +export type { APIResponse } from "./APIResponse.js"; +export type { BinaryResponse } from "./BinaryResponse.js"; +export type { EndpointMetadata } from "./EndpointMetadata.js"; +export { EndpointSupplier } from "./EndpointSupplier.js"; +export type { Fetcher, FetchFunction } from "./Fetcher.js"; +export { fetcher } from "./Fetcher.js"; +export { getHeader } from "./getHeader.js"; +export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; +export type { RawResponse, WithRawResponse } from "./RawResponse.js"; +export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; +export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..e8dceda9383d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,211 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { redactUrl } from "./redactUrl.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers, but only when the resolved URL targets the configured base URL. + // This prevents the SDK's credentials from leaking to an unrelated host when a caller + // passes an absolute cross-origin URL into the passthrough fetch escape hatch. + if (clientOptions.getAuthHeaders != null && targetsBaseUrl(fullUrl, baseUrl)) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: redactUrl(fullUrl), + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: redactUrl(fullUrl), + statusCode: response.status, + }); + } + + return response; +} + +/** + * Returns true when the resolved request URL points at the same origin as the + * configured base URL. Relative paths are always joined onto the base URL, so + * they resolve to the base origin and return true. Absolute URLs only match when + * their origin equals the base origin. When there is no base URL to compare + * against, or either value is not a parseable absolute URL, this returns false so + * auth headers are not attached. + */ +function targetsBaseUrl(fullUrl: string, baseUrl: string | undefined): boolean { + if (baseUrl == null) { + return false; + } + try { + return new URL(fullUrl).origin === new URL(baseUrl).origin; + } catch { + return false; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makeRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makeRequest.ts new file mode 100644 index 000000000000..360a86df40ad --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/makeRequest.ts @@ -0,0 +1,70 @@ +import { anySignal, getTimeoutSignal } from "./signals.js"; + +/** + * Cached result of checking whether the current runtime supports + * the `cache` option in `Request`. Some runtimes (e.g. Cloudflare Workers) + * throw a TypeError when this option is used. + */ +let _cacheNoStoreSupported: boolean | undefined; +export function isCacheNoStoreSupported(): boolean { + if (_cacheNoStoreSupported != null) { + return _cacheNoStoreSupported; + } + try { + new Request("http://localhost", { cache: "no-store" }); + _cacheNoStoreSupported = true; + } catch { + _cacheNoStoreSupported = false; + } + return _cacheNoStoreSupported; +} + +/** + * Reset the cached result of `isCacheNoStoreSupported`. Exposed for testing only. + */ +export function resetCacheNoStoreSupported(): void { + _cacheNoStoreSupported = undefined; +} + +export const makeRequest = async ( + fetchFn: (url: string, init: RequestInit) => Promise, + url: string, + method: string, + headers: Headers | Record, + requestBody: BodyInit | undefined, + timeoutMs?: number, + abortSignal?: AbortSignal, + withCredentials?: boolean, + duplex?: "half", + disableCache?: boolean, +): Promise => { + const signals: AbortSignal[] = []; + + let timeoutAbortId: ReturnType | undefined; + if (timeoutMs != null) { + const { signal, abortId } = getTimeoutSignal(timeoutMs); + timeoutAbortId = abortId; + signals.push(signal); + } + + if (abortSignal != null) { + signals.push(abortSignal); + } + const newSignals = anySignal(signals); + const response = await fetchFn(url, { + method: method, + headers, + body: requestBody, + signal: newSignals, + credentials: withCredentials ? "include" : undefined, + // @ts-ignore + duplex, + ...(disableCache && isCacheNoStoreSupported() ? { cache: "no-store" as RequestCache } : {}), + }); + + if (timeoutAbortId != null) { + clearTimeout(timeoutAbortId); + } + + return response; +}; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/redactUrl.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/redactUrl.ts new file mode 100644 index 000000000000..3c2e897a619c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/redactUrl.ts @@ -0,0 +1,102 @@ +export const SENSITIVE_QUERY_PARAMS: Set = new Set([ + "api_key", + "api-key", + "apikey", + "token", + "access_token", + "access-token", + "auth_token", + "auth-token", + "password", + "passwd", + "secret", + "api_secret", + "api-secret", + "apisecret", + "key", + "session", + "session_id", + "session-id", +]); + +export function redactUrl(url: string): string { + const protocolIndex = url.indexOf("://"); + if (protocolIndex === -1) return url; + + const afterProtocol = protocolIndex + 3; + + // Find the first delimiter that marks the end of the authority section + const pathStart = url.indexOf("/", afterProtocol); + let queryStart = url.indexOf("?", afterProtocol); + let fragmentStart = url.indexOf("#", afterProtocol); + + const firstDelimiter = Math.min( + pathStart === -1 ? url.length : pathStart, + queryStart === -1 ? url.length : queryStart, + fragmentStart === -1 ? url.length : fragmentStart, + ); + + // Find the LAST @ before the delimiter (handles multiple @ in credentials) + let atIndex = -1; + for (let i = afterProtocol; i < firstDelimiter; i++) { + if (url[i] === "@") { + atIndex = i; + } + } + + if (atIndex !== -1) { + url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`; + } + + // Recalculate queryStart since url might have changed + queryStart = url.indexOf("?"); + if (queryStart === -1) return url; + + fragmentStart = url.indexOf("#", queryStart); + const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length; + const queryString = url.slice(queryStart + 1, queryEnd); + + if (queryString.length === 0) return url; + + // FAST PATH: Quick check if any sensitive keywords present + // Using indexOf is faster than regex for simple substring matching + const lower = queryString.toLowerCase(); + const hasSensitive = + lower.includes("token") || + lower.includes("key") || + lower.includes("password") || + lower.includes("passwd") || + lower.includes("secret") || + lower.includes("session") || + lower.includes("auth"); + + if (!hasSensitive) { + return url; + } + + // SLOW PATH: Parse and redact + const redactedParams: string[] = []; + const params = queryString.split("&"); + + for (const param of params) { + const equalIndex = param.indexOf("="); + if (equalIndex === -1) { + redactedParams.push(param); + continue; + } + + const key = param.slice(0, equalIndex); + let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()); + + if (!shouldRedact && key.includes("%")) { + try { + const decodedKey = decodeURIComponent(key); + shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase()); + } catch {} + } + + redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param); + } + + return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/requestWithRetries.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/requestWithRetries.ts new file mode 100644 index 000000000000..5e66b9330e51 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/requestWithRetries.ts @@ -0,0 +1,68 @@ +const INITIAL_RETRY_DELAY = 1000; // in milliseconds +const MAX_RETRY_DELAY = 60000; // in milliseconds +const DEFAULT_MAX_RETRIES = 2; +const JITTER_FACTOR = 0.2; // 20% random jitter + +function isRetryableStatusCode(statusCode: number): boolean { + return [408, 429].includes(statusCode) || statusCode >= 500; +} + +function addPositiveJitter(delay: number): number { + const jitterMultiplier = 1 + Math.random() * JITTER_FACTOR; + return delay * jitterMultiplier; +} + +function addSymmetricJitter(delay: number): number { + const jitterMultiplier = 1 + (Math.random() - 0.5) * JITTER_FACTOR; + return delay * jitterMultiplier; +} + +function getRetryDelayFromHeaders(response: Response, retryAttempt: number): number { + const retryAfter = response.headers.get("Retry-After"); + if (retryAfter) { + const retryAfterSeconds = parseInt(retryAfter, 10); + if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) { + return Math.min(retryAfterSeconds * 1000, MAX_RETRY_DELAY); + } + + const retryAfterDate = new Date(retryAfter); + if (!Number.isNaN(retryAfterDate.getTime())) { + const delay = retryAfterDate.getTime() - Date.now(); + if (delay > 0) { + return Math.min(Math.max(delay, 0), MAX_RETRY_DELAY); + } + } + } + + const rateLimitReset = response.headers.get("X-RateLimit-Reset"); + if (rateLimitReset) { + const resetTime = parseInt(rateLimitReset, 10); + if (!Number.isNaN(resetTime)) { + const delay = resetTime * 1000 - Date.now(); + if (delay > 0) { + return addPositiveJitter(Math.min(delay, MAX_RETRY_DELAY)); + } + } + } + + return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** retryAttempt, MAX_RETRY_DELAY)); +} + +export async function requestWithRetries( + requestFn: () => Promise, + maxRetries: number = DEFAULT_MAX_RETRIES, +): Promise { + let response: Response = await requestFn(); + + for (let i = 0; i < maxRetries; ++i) { + if (isRetryableStatusCode(response.status)) { + const delay = getRetryDelayFromHeaders(response, i); + + await new Promise((resolve) => setTimeout(resolve, delay)); + response = await requestFn(); + } else { + break; + } + } + return response!; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/signals.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/signals.ts new file mode 100644 index 000000000000..ba74c4d02be6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/fetcher/signals.ts @@ -0,0 +1,35 @@ +const TIMEOUT = "timeout"; + +export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: ReturnType } { + const controller = new AbortController(); + const abortId = setTimeout(() => controller.abort(TIMEOUT), timeoutMs); + return { signal: controller.signal, abortId }; +} + +export function anySignal(...args: AbortSignal[] | [AbortSignal[]]): AbortSignal { + const signals = (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as AbortSignal[]; + + const controller = new AbortController(); + + for (const signal of signals) { + if (signal.aborted) { + controller.abort((signal as any)?.reason); + return controller.signal; + } + + signal.addEventListener("abort", () => controller.abort((signal as any)?.reason), { + signal: controller.signal, + }); + + // Re-check after adding listener: the signal may have aborted + // between the initial `signal.aborted` check and the `addEventListener` + // call above. If it did, the abort event was already dispatched and + // the listener will never fire — we must manually abort. + if (signal.aborted) { + controller.abort((signal as any)?.reason); + return controller.signal; + } + } + + return controller.signal; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/headers.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/headers.ts new file mode 100644 index 000000000000..be45c4552a35 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/headers.ts @@ -0,0 +1,33 @@ +export function mergeHeaders(...headersArray: (Record | null | undefined)[]): Record { + const result: Record = {}; + + for (const [key, value] of headersArray + .filter((headers) => headers != null) + .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); + if (value != null) { + result[insensitiveKey] = value; + } else if (insensitiveKey in result) { + delete result[insensitiveKey]; + } + } + + return result; +} + +export function mergeOnlyDefinedHeaders( + ...headersArray: (Record | null | undefined)[] +): Record { + const result: Record = {}; + + for (const [key, value] of headersArray + .filter((headers) => headers != null) + .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); + if (value != null) { + result[insensitiveKey] = value; + } + } + + return result; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/index.ts new file mode 100644 index 000000000000..92290bfadcac --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/index.ts @@ -0,0 +1,6 @@ +export * from "./auth/index.js"; +export * from "./base64.js"; +export * from "./fetcher/index.js"; +export * as logging from "./logging/index.js"; +export * from "./runtime/index.js"; +export * as url from "./url/index.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/json.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/json.ts new file mode 100644 index 000000000000..c052f3249f4f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/json.ts @@ -0,0 +1,27 @@ +/** + * Serialize a value to JSON + * @param value A JavaScript value, usually an object or array, to be converted. + * @param replacer A function that transforms the results. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + * @returns JSON string + */ +export const toJson = ( + value: unknown, + replacer?: (this: unknown, key: string, value: unknown) => unknown, + space?: string | number, +): string => { + return JSON.stringify(value, replacer, space); +}; + +/** + * Parse JSON string to object, array, or other type + * @param text A valid JSON string. + * @param reviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. + * @returns Parsed object, array, or other type + */ +export function fromJson( + text: string, + reviver?: (this: unknown, key: string, value: unknown) => unknown, +): T { + return JSON.parse(text, reviver); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/exports.ts new file mode 100644 index 000000000000..88f6c00db0cf --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/exports.ts @@ -0,0 +1,19 @@ +import * as logger from "./logger.js"; + +export namespace logging { + /** + * Configuration for logger instances. + */ + export type LogConfig = logger.LogConfig; + export type LogLevel = logger.LogLevel; + export const LogLevel: typeof logger.LogLevel = logger.LogLevel; + export type ILogger = logger.ILogger; + /** + * Console logger implementation that outputs to the console. + */ + export type ConsoleLogger = logger.ConsoleLogger; + /** + * Console logger implementation that outputs to the console. + */ + export const ConsoleLogger: typeof logger.ConsoleLogger = logger.ConsoleLogger; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/index.ts new file mode 100644 index 000000000000..d81cc32c40f9 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/index.ts @@ -0,0 +1 @@ +export * from "./logger.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/logger.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/logger.ts new file mode 100644 index 000000000000..a3f3673cda93 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/logging/logger.ts @@ -0,0 +1,203 @@ +export const LogLevel = { + Debug: "debug", + Info: "info", + Warn: "warn", + Error: "error", +} as const; +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +const logLevelMap: Record = { + [LogLevel.Debug]: 1, + [LogLevel.Info]: 2, + [LogLevel.Warn]: 3, + [LogLevel.Error]: 4, +}; + +export interface ILogger { + /** + * Logs a debug message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + debug(message: string, ...args: unknown[]): void; + /** + * Logs an info message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + info(message: string, ...args: unknown[]): void; + /** + * Logs a warning message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + warn(message: string, ...args: unknown[]): void; + /** + * Logs an error message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + error(message: string, ...args: unknown[]): void; +} + +/** + * Configuration for logger initialization. + */ +export interface LogConfig { + /** + * Minimum log level to output. + * @default LogLevel.Info + */ + level?: LogLevel; + /** + * Logger implementation to use. + * @default new ConsoleLogger() + */ + logger?: ILogger; + /** + * Whether logging should be silenced. + * @default true + */ + silent?: boolean; +} + +/** + * Default console-based logger implementation. + */ +export class ConsoleLogger implements ILogger { + debug(message: string, ...args: unknown[]): void { + console.debug(message, ...args); + } + info(message: string, ...args: unknown[]): void { + console.info(message, ...args); + } + warn(message: string, ...args: unknown[]): void { + console.warn(message, ...args); + } + error(message: string, ...args: unknown[]): void { + console.error(message, ...args); + } +} + +/** + * Logger class that provides level-based logging functionality. + */ +export class Logger { + private readonly level: number; + private readonly logger: ILogger; + private readonly silent: boolean; + + /** + * Creates a new logger instance. + * @param config - Logger configuration + */ + constructor(config: Required) { + this.level = logLevelMap[config.level]; + this.logger = config.logger; + this.silent = config.silent; + } + + /** + * Checks if a log level should be output based on configuration. + * @param level - The log level to check + * @returns True if the level should be logged + */ + public shouldLog(level: LogLevel): boolean { + return !this.silent && this.level <= logLevelMap[level]; + } + + /** + * Checks if debug logging is enabled. + * @returns True if debug logs should be output + */ + public isDebug(): boolean { + return this.shouldLog(LogLevel.Debug); + } + + /** + * Logs a debug message if debug logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public debug(message: string, ...args: unknown[]): void { + if (this.isDebug()) { + this.logger.debug(message, ...args); + } + } + + /** + * Checks if info logging is enabled. + * @returns True if info logs should be output + */ + public isInfo(): boolean { + return this.shouldLog(LogLevel.Info); + } + + /** + * Logs an info message if info logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public info(message: string, ...args: unknown[]): void { + if (this.isInfo()) { + this.logger.info(message, ...args); + } + } + + /** + * Checks if warning logging is enabled. + * @returns True if warning logs should be output + */ + public isWarn(): boolean { + return this.shouldLog(LogLevel.Warn); + } + + /** + * Logs a warning message if warning logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public warn(message: string, ...args: unknown[]): void { + if (this.isWarn()) { + this.logger.warn(message, ...args); + } + } + + /** + * Checks if error logging is enabled. + * @returns True if error logs should be output + */ + public isError(): boolean { + return this.shouldLog(LogLevel.Error); + } + + /** + * Logs an error message if error logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public error(message: string, ...args: unknown[]): void { + if (this.isError()) { + this.logger.error(message, ...args); + } + } +} + +export function createLogger(config?: LogConfig | Logger): Logger { + if (config == null) { + return defaultLogger; + } + if (config instanceof Logger) { + return config; + } + config = config ?? {}; + config.level ??= LogLevel.Info; + config.logger ??= new ConsoleLogger(); + config.silent ??= true; + return new Logger(config as Required); +} + +const defaultLogger: Logger = new Logger({ + level: LogLevel.Info, + logger: new ConsoleLogger(), + silent: true, +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/requestBody.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/requestBody.ts new file mode 100644 index 000000000000..d58ef590cffc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/requestBody.ts @@ -0,0 +1,26 @@ +/** + * Spreads caller-supplied `additionalBodyParameters` (from `requestOptions.additionalBodyParameters`) + * on top of the request body. Caller-supplied properties win over the endpoint body. When no + * additional body parameters are provided, the original body is returned unchanged so serialization + * is unaffected. + * + * The merge only applies to plain-object (JSON object) bodies. When the body is `null`/`undefined` + * the additional parameters become the body; when the body is an array or a primitive JSON value it + * is returned unchanged, since object properties cannot be spread into it. This mirrors the Python + * SDK, which only merges additional body parameters into mapping bodies. + */ +export function mergeAdditionalBodyParameters( + body: unknown, + additionalBodyParameters: Record | undefined, +): unknown { + if (additionalBodyParameters == null) { + return body; + } + if (body == null) { + return { ...additionalBodyParameters }; + } + if (typeof body === "object" && !Array.isArray(body)) { + return { ...body, ...additionalBodyParameters }; + } + return body; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/index.ts new file mode 100644 index 000000000000..85a327200031 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/index.ts @@ -0,0 +1 @@ +export { getUserAgent, RUNTIME } from "./runtime.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/runtime.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/runtime.ts new file mode 100644 index 000000000000..d367ce6b9041 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/runtime/runtime.ts @@ -0,0 +1,231 @@ +interface DenoGlobal { + version: { + deno: string; + }; + build?: { + os?: string; + arch?: string; + }; +} + +interface BunGlobal { + version: string; +} + +declare const Deno: DenoGlobal | undefined; +declare const Bun: BunGlobal | undefined; +declare const EdgeRuntime: string | undefined; +declare const self: typeof globalThis.self & { + importScripts?: unknown; +}; + +/** + * A constant that indicates which environment and version the SDK is running in. + */ +export const RUNTIME: Runtime = evaluateRuntime(); + +export interface Runtime { + type: "browser" | "web-worker" | "deno" | "bun" | "node" | "react-native" | "unknown" | "workerd" | "edge-runtime"; + version?: string; + parsedVersion?: number; + /** + * The operating system the SDK is running on, when it can be determined + * (e.g. "linux", "darwin", "win32" on server runtimes). Undefined in + * environments where the OS is not observable (e.g. browsers). + */ + os?: string; + /** + * The CPU architecture the SDK is running on, when it can be determined + * (e.g. "x64", "arm64" on server runtimes). Undefined in environments where + * the architecture is not observable (e.g. browsers). + */ + arch?: string; +} + +function evaluateRuntime(): Runtime { + /** + * A constant that indicates whether the environment the code is running is a Web Browser. + */ + const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; + if (isBrowser) { + return { + type: "browser", + version: window.navigator.userAgent, + }; + } + + /** + * A constant that indicates whether the environment the code is running is Cloudflare. + * https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent + */ + const isCloudflare = typeof globalThis !== "undefined" && globalThis?.navigator?.userAgent === "Cloudflare-Workers"; + if (isCloudflare) { + return { + type: "workerd", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Edge Runtime. + * https://vercel.com/docs/functions/runtimes/edge-runtime#check-if-you're-running-on-the-edge-runtime + */ + const isEdgeRuntime = typeof EdgeRuntime === "string"; + if (isEdgeRuntime) { + return { + type: "edge-runtime", + }; + } + + /** + * A constant that indicates whether the environment the code is running is a Web Worker. + */ + const isWebWorker = + typeof self === "object" && + typeof self?.importScripts === "function" && + (self.constructor?.name === "DedicatedWorkerGlobalScope" || + self.constructor?.name === "ServiceWorkerGlobalScope" || + self.constructor?.name === "SharedWorkerGlobalScope"); + if (isWebWorker) { + return { + type: "web-worker", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Deno. + * FYI Deno spoofs process.versions.node, see https://deno.land/std@0.177.0/node/process.ts?s=versions + */ + const isDeno = + typeof Deno !== "undefined" && typeof Deno.version !== "undefined" && typeof Deno.version.deno !== "undefined"; + if (isDeno) { + return { + type: "deno", + version: Deno.version.deno, + os: Deno.build?.os, + arch: Deno.build?.arch, + }; + } + + /** + * A constant that indicates whether the environment the code is running is Bun.sh. + */ + const isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined"; + if (isBun) { + return { + type: "bun", + version: Bun.version, + os: typeof process !== "undefined" ? process.platform : undefined, + arch: typeof process !== "undefined" ? process.arch : undefined, + }; + } + + /** + * A constant that indicates whether the environment the code is running is in React-Native. + * This check should come before Node.js detection since React Native may have a process polyfill. + * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/setUpNavigator.js + */ + const isReactNative = typeof navigator !== "undefined" && navigator?.product === "ReactNative"; + if (isReactNative) { + return { + type: "react-native", + }; + } + + /** + * A constant that indicates whether the environment the code is running is Node.JS. + * + * We assign `process` to a local variable first to avoid being flagged by + * bundlers that perform static analysis on `process.versions` (e.g. Next.js + * Edge Runtime warns about Node.js APIs even when they are guarded). + */ + const _process = typeof process !== "undefined" ? process : undefined; + const isNode = typeof _process !== "undefined" && typeof _process.versions?.node === "string"; + if (isNode) { + return { + type: "node", + version: _process.versions.node, + parsedVersion: Number(_process.versions.node.split(".")[0]), + os: _process.platform, + arch: _process.arch, + }; + } + + return { + type: "unknown", + }; +} + +/** + * Display names for the language runtimes whose version is meaningful to encode + * in a User-Agent. Environments where a version string is not useful (e.g. + * browsers, where `version` is the full navigator UA) are intentionally mapped + * to `undefined` so they are omitted from the User-Agent. + */ +const RUNTIME_DISPLAY_NAMES: Record = { + node: "Node", + deno: "Deno", + bun: "Bun", + browser: undefined, + "web-worker": undefined, + "react-native": undefined, + workerd: undefined, + "edge-runtime": undefined, + unknown: undefined, +}; + +/** + * CPU architecture aliases that all refer to 64-bit x86. They are normalized to + * the single canonical token `x86_64` so the User-Agent architecture label is + * consistent regardless of which runtime reports it (Node reports `x64`, others + * report `amd64` or `x86_64`). + */ +const X86_64_ARCH_ALIASES = new Set(["x64", "amd64", "x86_64"]); + +/** + * Normalizes a CPU architecture token, collapsing the 64-bit x86 aliases + * (`x64`, `amd64`, `x86_64`) to `x86_64`. Other architectures are returned + * unchanged. + */ +function normalizeArch(arch: string | undefined): string | undefined { + if (arch == null) { + return arch; + } + return X86_64_ARCH_ALIASES.has(arch.toLowerCase()) ? "x86_64" : arch; +} + +/** + * Percent-encodes the `@` and `/` characters in an npm package name so the + * User-Agent product token stays within the RFC 7230 token grammar. The + * original scoped package name can be recovered by URL-decoding (e.g. + * `@dummy/sdk` becomes `%40dummy%2Fsdk`). + */ +function encodeProductName(sdkName: string): string { + return sdkName.replace(/@/g, "%40").replace(/\//g, "%2F"); +} + +/** + * Builds a structured User-Agent string of the form + * `{sdkName}/{sdkVersion} ({os}; {arch}) {runtime}/{runtimeVersion}` + * where the platform group and runtime segment are omitted gracefully when the + * underlying values cannot be determined (e.g. in a browser). + */ +export function getUserAgent(sdkName: string, sdkVersion: string): string { + let userAgent = `${encodeProductName(sdkName)}/${sdkVersion}`; + + const platform = [RUNTIME.os, normalizeArch(RUNTIME.arch)].filter( + (part): part is string => part != null && part.length > 0, + ); + if (platform.length > 0) { + userAgent += ` (${platform.join("; ")})`; + } + + const runtimeName = RUNTIME_DISPLAY_NAMES[RUNTIME.type]; + if (runtimeName != null) { + userAgent += ` ${runtimeName}`; + if (RUNTIME.version != null && RUNTIME.version.length > 0) { + userAgent += `/${RUNTIME.version}`; + } + } + + return userAgent; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/QueryStringBuilder.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/QueryStringBuilder.ts new file mode 100644 index 000000000000..b045221c381c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/QueryStringBuilder.ts @@ -0,0 +1,87 @@ +import { toQueryString } from "./qs.js"; + +/** + * Creates a fluent builder for constructing URL query strings. + * + * Each `.add()` call serializes its value immediately (like C#'s builder), + * so no format tracking is needed — the style is applied at add-time. + * + * Usage (generated code): + * + * const qs = core.url.queryBuilder() + * .add("limit", limit) + * .add("tags", tags, { style: "comma" }) // explode: false + * .mergeAdditional(requestOptions?.queryParams) + * .build(); + */ +export function queryBuilder(): QueryStringBuilder { + return new QueryStringBuilder(); +} + +class QueryStringBuilder { + private parts: Map = new Map(); + + /** + * Adds a query parameter, serializing it immediately. + * + * By default arrays use "repeat" format (`key=a&key=b`). + * Pass `{ style: "comma" }` for OpenAPI `explode: false` parameters + * to get comma-separated values (`key=a,b,c`). + * + * Null / undefined values are silently skipped. + */ + add(key: string, value: unknown, options?: { style?: "comma" }): this { + if (value === undefined || value === null) { + return this; + } + const serialized = toQueryString( + { [key]: value }, + { arrayFormat: options?.style === "comma" ? "comma" : "repeat" }, + ); + if (serialized.length > 0) { + this.parts.set(key, serialized); + } + return this; + } + + /** + * Adds multiple query parameters at once from a record. + * All parameters use the default "repeat" array format. + * Null / undefined values are silently skipped. + */ + addMany(params: Record): this { + if (params != null) { + for (const [key, value] of Object.entries(params)) { + this.add(key, value); + } + } + return this; + } + + /** + * Merges additional query parameters supplied at call-time via + * `requestOptions.queryParams`. Overrides existing keys (last-write-wins). + */ + mergeAdditional(additionalParams?: Record): this { + if (additionalParams != null) { + for (const [key, value] of Object.entries(additionalParams)) { + if (value === undefined || value === null) { + continue; + } + const serialized = toQueryString({ [key]: value }, { arrayFormat: "repeat" }); + if (serialized.length > 0) { + this.parts.set(key, serialized); + } + } + } + return this; + } + + /** + * Returns the assembled query string (without the leading `?`). + * Returns an empty string when no parameters were added. + */ + build(): string { + return [...this.parts.values()].join("&"); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/encodePathParam.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/encodePathParam.ts new file mode 100644 index 000000000000..19b901244218 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/encodePathParam.ts @@ -0,0 +1,18 @@ +export function encodePathParam(param: unknown): string { + if (param === null) { + return "null"; + } + const typeofParam = typeof param; + switch (typeofParam) { + case "undefined": + return "undefined"; + case "string": + case "number": + case "boolean": + break; + default: + param = String(param); + break; + } + return encodeURIComponent(param as string | number | boolean); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/index.ts new file mode 100644 index 000000000000..ca9d4fbd1eb6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/index.ts @@ -0,0 +1,4 @@ +export { encodePathParam } from "./encodePathParam.js"; +export { join } from "./join.js"; +export { queryBuilder } from "./QueryStringBuilder.js"; +export { toQueryString } from "./qs.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/join.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/join.ts new file mode 100644 index 000000000000..7ca7daef094d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/join.ts @@ -0,0 +1,79 @@ +export function join(base: string, ...segments: string[]): string { + if (!base) { + return ""; + } + + if (segments.length === 0) { + return base; + } + + if (base.includes("://")) { + let url: URL; + try { + url = new URL(base); + } catch { + return joinPath(base, ...segments); + } + + const lastSegment = segments[segments.length - 1]; + const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + + for (const segment of segments) { + const cleanSegment = trimSlashes(segment); + if (cleanSegment) { + url.pathname = joinPathSegments(url.pathname, cleanSegment); + } + } + + if (shouldPreserveTrailingSlash && !url.pathname.endsWith("/")) { + url.pathname += "/"; + } + + return url.toString(); + } + + return joinPath(base, ...segments); +} + +function joinPath(base: string, ...segments: string[]): string { + if (segments.length === 0) { + return base; + } + + let result = base; + + const lastSegment = segments[segments.length - 1]; + const shouldPreserveTrailingSlash = lastSegment?.endsWith("/"); + + for (const segment of segments) { + const cleanSegment = trimSlashes(segment); + if (cleanSegment) { + result = joinPathSegments(result, cleanSegment); + } + } + + if (shouldPreserveTrailingSlash && !result.endsWith("/")) { + result += "/"; + } + + return result; +} + +function joinPathSegments(left: string, right: string): string { + if (left.endsWith("/")) { + return left + right; + } + return `${left}/${right}`; +} + +function trimSlashes(str: string): string { + if (!str) return str; + + let start = 0; + let end = str.length; + + if (str.startsWith("/")) start = 1; + if (str.endsWith("/")) end = str.length - 1; + + return start === 0 && end === str.length ? str : str.slice(start, end); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/qs.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/qs.ts new file mode 100644 index 000000000000..aebb95a38bd4 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/core/url/qs.ts @@ -0,0 +1,87 @@ +type ArrayFormat = "indices" | "repeat" | "comma"; + +interface QueryStringOptions { + arrayFormat?: ArrayFormat; + encode?: boolean; +} + +const defaultQsOptions: Required = { + arrayFormat: "indices", + encode: true, +} as const; + +function encodeValue(value: unknown, shouldEncode: boolean): string { + if (value === undefined) { + return ""; + } + if (value === null) { + return ""; + } + const stringValue = String(value); + return shouldEncode ? encodeURIComponent(stringValue) : stringValue; +} + +function stringifyObject(obj: Record, prefix = "", options: Required): string[] { + const parts: string[] = []; + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}[${key}]` : key; + + if (value == null) { + continue; + } + + if (Array.isArray(value)) { + if (value.length === 0) { + continue; + } + const effectiveFormat = options.arrayFormat; + if (effectiveFormat === "comma") { + const encodedKey = options.encode ? encodeURIComponent(fullKey) : fullKey; + const encodedValues = value + .filter((item) => item !== undefined && item !== null) + .map((item) => encodeValue(item, options.encode)); + if (encodedValues.length > 0) { + parts.push(`${encodedKey}=${encodedValues.join(",")}`); + } + } else { + for (let i = 0; i < value.length; i++) { + const item = value[i]; + if (item == null) { + continue; + } + if (typeof item === "object" && !Array.isArray(item) && item !== null) { + const arrayKey = effectiveFormat === "indices" ? `${fullKey}[${i}]` : fullKey; + parts.push(...stringifyObject(item as Record, arrayKey, options)); + } else { + const arrayKey = effectiveFormat === "indices" ? `${fullKey}[${i}]` : fullKey; + const encodedKey = options.encode ? encodeURIComponent(arrayKey) : arrayKey; + parts.push(`${encodedKey}=${encodeValue(item, options.encode)}`); + } + } + } + } else if (typeof value === "object" && value !== null) { + if (Object.keys(value as Record).length === 0) { + continue; + } + parts.push(...stringifyObject(value as Record, fullKey, options)); + } else { + const encodedKey = options.encode ? encodeURIComponent(fullKey) : fullKey; + parts.push(`${encodedKey}=${encodeValue(value, options.encode)}`); + } + } + + return parts; +} + +export function toQueryString(obj: unknown, options?: QueryStringOptions): string { + if (obj == null || typeof obj !== "object") { + return ""; + } + + const parts = stringifyObject(obj as Record, "", { + ...defaultQsOptions, + ...options, + }); + return parts.join("&"); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthError.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthError.ts new file mode 100644 index 000000000000..3c9be0ee12e6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthError.ts @@ -0,0 +1,68 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import { toJson } from "../core/json.js"; + +export class SeedTsFlattenRequestAnyAuthError extends Error { + public readonly statusCode?: number; + public readonly body?: unknown; + public readonly rawResponse?: core.RawResponse; + public readonly cause?: unknown; + + constructor({ + message, + statusCode, + body, + rawResponse, + cause, + }: { + message?: string; + statusCode?: number; + body?: unknown; + rawResponse?: core.RawResponse; + cause?: unknown; + }) { + super(buildMessage({ message, statusCode, body })); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "SeedTsFlattenRequestAnyAuthError"; + this.statusCode = statusCode; + this.body = body; + this.rawResponse = rawResponse; + if (cause != null) { + this.cause = cause; + } + } + + public get requestId(): string | undefined { + return this.rawResponse?.headers?.get("x-request-id") ?? undefined; + } +} + +function buildMessage({ + message, + statusCode, + body, +}: { + message: string | undefined; + statusCode: number | undefined; + body: unknown | undefined; +}): string { + const lines: string[] = []; + if (message != null) { + lines.push(message); + } + + if (statusCode != null) { + lines.push(`Status code: ${statusCode.toString()}`); + } + + if (body != null) { + lines.push(`Body: ${toJson(body, undefined, 2)}`); + } + + return lines.join("\n"); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts new file mode 100644 index 000000000000..51256ce1a8a9 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/SeedTsFlattenRequestAnyAuthTimeoutError.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as errors from "./index.js"; + +export class SeedTsFlattenRequestAnyAuthTimeoutError extends errors.SeedTsFlattenRequestAnyAuthError { + constructor(message: string, opts?: { cause?: unknown }) { + super({ + message: message, + cause: opts?.cause, + }); + Object.setPrototypeOf(this, new.target.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = "SeedTsFlattenRequestAnyAuthTimeoutError"; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/handleNonStatusCodeError.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/handleNonStatusCodeError.ts new file mode 100644 index 000000000000..43d16dac85fc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/handleNonStatusCodeError.ts @@ -0,0 +1,43 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as core from "../core/index.js"; +import * as errors from "./index.js"; + +export function handleNonStatusCodeError( + error: core.Fetcher.Error, + rawResponse: core.RawResponse, + method: string, + path: string, +): never { + switch (error.reason) { + case "non-json": + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: error.statusCode, + body: error.rawBody, + rawResponse: rawResponse, + }); + case "body-is-null": + throw new errors.SeedTsFlattenRequestAnyAuthError({ + statusCode: error.statusCode, + rawResponse: rawResponse, + }); + case "timeout": + throw new errors.SeedTsFlattenRequestAnyAuthTimeoutError( + `Timeout exceeded when calling ${method} ${path}.`, + { + cause: error.cause, + }, + ); + case "unknown": + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: error.errorMessage, + rawResponse: rawResponse, + cause: error.cause, + }); + default: + throw new errors.SeedTsFlattenRequestAnyAuthError({ + message: "Unknown error", + rawResponse: rawResponse, + }); + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/index.ts new file mode 100644 index 000000000000..cd36e0da75f7 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/errors/index.ts @@ -0,0 +1,2 @@ +export { SeedTsFlattenRequestAnyAuthError } from "./SeedTsFlattenRequestAnyAuthError.js"; +export { SeedTsFlattenRequestAnyAuthTimeoutError } from "./SeedTsFlattenRequestAnyAuthTimeoutError.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/exports.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/exports.ts new file mode 100644 index 000000000000..7b70ee14fc02 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/exports.ts @@ -0,0 +1 @@ +export * from "./core/exports.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/index.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/index.ts new file mode 100644 index 000000000000..e7b7c7e7db0a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/index.ts @@ -0,0 +1,5 @@ +export * as SeedTsFlattenRequestAnyAuth from "./api/index.js"; +export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; +export { SeedTsFlattenRequestAnyAuthClient } from "./Client.js"; +export { SeedTsFlattenRequestAnyAuthError, SeedTsFlattenRequestAnyAuthTimeoutError } from "./errors/index.js"; +export * from "./exports.js"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/version.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/version.ts new file mode 100644 index 000000000000..b643a3e3ea27 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/version.ts @@ -0,0 +1 @@ +export const SDK_VERSION = "0.0.1"; diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/custom.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/custom.test.ts new file mode 100644 index 000000000000..7f5e031c8396 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/custom.test.ts @@ -0,0 +1,13 @@ +/** + * This is a custom test file, if you wish to add more tests + * to your SDK. + * Be sure to mark this file in `.fernignore`. + * + * If you include example requests/responses in your fern definition, + * you will have tests automatically generated for you. + */ +describe("test", () => { + it("default", () => { + expect(true).toBe(true); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServer.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServer.ts new file mode 100644 index 000000000000..954872157d52 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServer.ts @@ -0,0 +1,29 @@ +import type { RequestHandlerOptions } from "msw"; +import type { SetupServer } from "msw/node"; + +import { mockEndpointBuilder } from "./mockEndpointBuilder"; + +export interface MockServerOptions { + baseUrl: string; + server: SetupServer; +} + +export class MockServer { + private readonly server: SetupServer; + public readonly baseUrl: string; + + constructor({ baseUrl, server }: MockServerOptions) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + this.server = server; + } + + public mockEndpoint(options?: RequestHandlerOptions): ReturnType { + const builder = mockEndpointBuilder({ + once: options?.once ?? true, + onBuild: (handler) => { + this.server.use(handler); + }, + }).baseUrl(this.baseUrl); + return builder; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServerPool.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServerPool.ts new file mode 100644 index 000000000000..d7d891a2d80b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/MockServerPool.ts @@ -0,0 +1,106 @@ +import { setupServer } from "msw/node"; + +import { fromJson, toJson } from "../../src/core/json"; +import { MockServer } from "./MockServer"; +import { randomBaseUrl } from "./randomBaseUrl"; + +const mswServer = setupServer(); +interface MockServerOptions { + baseUrl?: string; +} + +async function formatHttpRequest(request: Request, id?: string): Promise { + try { + const clone = request.clone(); + const headers = [...clone.headers.entries()].map(([k, v]) => `${k}: ${v}`).join("\n"); + + let body = ""; + try { + const contentType = clone.headers.get("content-type"); + if (contentType?.includes("application/json")) { + body = toJson(fromJson(await clone.text()), undefined, 2); + } else if (clone.body) { + body = await clone.text(); + } + } catch (_e) { + body = "(unable to parse body)"; + } + + const title = id ? `### Request ${id} ###\n` : ""; + const firstLine = `${title}${request.method} ${request.url.toString()} HTTP/1.1`; + + return `\n${firstLine}\n${headers}\n\n${body || "(no body)"}\n`; + } catch (e) { + return `Error formatting request: ${e}`; + } +} + +async function formatHttpResponse(response: Response, id?: string): Promise { + try { + const clone = response.clone(); + const headers = [...clone.headers.entries()].map(([k, v]) => `${k}: ${v}`).join("\n"); + + let body = ""; + try { + const contentType = clone.headers.get("content-type"); + if (contentType?.includes("application/json")) { + body = toJson(fromJson(await clone.text()), undefined, 2); + } else if (clone.body) { + body = await clone.text(); + } + } catch (_e) { + body = "(unable to parse body)"; + } + + const title = id ? `### Response for ${id} ###\n` : ""; + const firstLine = `${title}HTTP/1.1 ${response.status} ${response.statusText}`; + + return `\n${firstLine}\n${headers}\n\n${body || "(no body)"}\n`; + } catch (e) { + return `Error formatting response: ${e}`; + } +} + +class MockServerPool { + private servers: MockServer[] = []; + + public createServer(options?: Partial): MockServer { + const baseUrl = options?.baseUrl || randomBaseUrl(); + const server = new MockServer({ baseUrl, server: mswServer }); + this.servers.push(server); + return server; + } + + public getServers(): MockServer[] { + return [...this.servers]; + } + + public listen(): void { + const onUnhandledRequest = process.env.LOG_LEVEL === "debug" ? "warn" : "bypass"; + mswServer.listen({ onUnhandledRequest }); + + if (process.env.LOG_LEVEL === "debug") { + mswServer.events.on("request:start", async ({ request, requestId }) => { + const formattedRequest = await formatHttpRequest(request, requestId); + console.debug(`request:start\n${formattedRequest}`); + }); + + mswServer.events.on("request:unhandled", async ({ request, requestId }) => { + const formattedRequest = await formatHttpRequest(request, requestId); + console.debug(`request:unhandled\n${formattedRequest}`); + }); + + mswServer.events.on("response:mocked", async ({ request, response, requestId }) => { + const formattedResponse = await formatHttpResponse(response, requestId); + console.debug(`response:mocked\n${formattedResponse}`); + }); + } + } + + public close(): void { + this.servers = []; + mswServer.close(); + } +} + +export const mockServerPool: MockServerPool = new MockServerPool(); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/mockEndpointBuilder.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/mockEndpointBuilder.ts new file mode 100644 index 000000000000..3e8540a3ba5a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/mockEndpointBuilder.ts @@ -0,0 +1,234 @@ +import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponseResolver, http } from "msw"; + +import { url } from "../../src/core"; +import { toJson } from "../../src/core/json"; +import { type WithFormUrlEncodedOptions, withFormUrlEncoded } from "./withFormUrlEncoded"; +import { withHeaders } from "./withHeaders"; +import { type WithJsonOptions, withJson } from "./withJson"; + +type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; + +interface MethodStage { + baseUrl(baseUrl: string): MethodStage; + all(path: string): RequestHeadersStage; + get(path: string): RequestHeadersStage; + post(path: string): RequestHeadersStage; + put(path: string): RequestHeadersStage; + delete(path: string): RequestHeadersStage; + patch(path: string): RequestHeadersStage; + options(path: string): RequestHeadersStage; + head(path: string): RequestHeadersStage; +} + +interface RequestHeadersStage extends RequestBodyStage, ResponseStage { + header(name: string, value: string): RequestHeadersStage; + headers(headers: Record): RequestBodyStage; +} + +interface RequestBodyStage extends ResponseStage { + jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage; + formUrlEncodedBody(body: unknown, options?: WithFormUrlEncodedOptions): ResponseStage; +} + +interface ResponseStage { + respondWith(): ResponseStatusStage; +} +interface ResponseStatusStage { + statusCode(statusCode: number): ResponseHeaderStage; +} + +interface ResponseHeaderStage extends ResponseBodyStage, BuildStage { + header(name: string, value: string): ResponseHeaderStage; + headers(headers: Record): ResponseHeaderStage; +} + +interface ResponseBodyStage { + jsonBody(body: unknown): BuildStage; + sseBody(body: string): BuildStage; +} + +interface BuildStage { + build(): HttpHandler; +} + +export interface HttpHandlerBuilderOptions { + onBuild?: (handler: HttpHandler) => void; + once?: boolean; +} + +class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodyStage, ResponseStage { + private method: HttpMethod = "get"; + private _baseUrl: string = ""; + private path: string = "/"; + private readonly predicates: ((resolver: HttpResponseResolver) => HttpResponseResolver)[] = []; + private readonly handlerOptions?: HttpHandlerBuilderOptions; + + constructor(options?: HttpHandlerBuilderOptions) { + this.handlerOptions = options; + } + + baseUrl(baseUrl: string): MethodStage { + this._baseUrl = baseUrl; + return this; + } + + all(path: string): RequestHeadersStage { + this.method = "all"; + this.path = path; + return this; + } + + get(path: string): RequestHeadersStage { + this.method = "get"; + this.path = path; + return this; + } + + post(path: string): RequestHeadersStage { + this.method = "post"; + this.path = path; + return this; + } + + put(path: string): RequestHeadersStage { + this.method = "put"; + this.path = path; + return this; + } + + delete(path: string): RequestHeadersStage { + this.method = "delete"; + this.path = path; + return this; + } + + patch(path: string): RequestHeadersStage { + this.method = "patch"; + this.path = path; + return this; + } + + options(path: string): RequestHeadersStage { + this.method = "options"; + this.path = path; + return this; + } + + head(path: string): RequestHeadersStage { + this.method = "head"; + this.path = path; + return this; + } + + header(name: string, value: string): RequestHeadersStage { + this.predicates.push((resolver) => withHeaders({ [name]: value }, resolver)); + return this; + } + + headers(headers: Record): RequestBodyStage { + this.predicates.push((resolver) => withHeaders(headers, resolver)); + return this; + } + + jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage { + if (body === undefined) { + throw new Error("Undefined is not valid JSON. Do not call jsonBody if you want an empty body."); + } + this.predicates.push((resolver) => withJson(body, resolver, options)); + return this; + } + + formUrlEncodedBody(body: unknown, options?: WithFormUrlEncodedOptions): ResponseStage { + if (body === undefined) { + throw new Error( + "Undefined is not valid for form-urlencoded. Do not call formUrlEncodedBody if you want an empty body.", + ); + } + this.predicates.push((resolver) => withFormUrlEncoded(body, resolver, options)); + return this; + } + + respondWith(): ResponseStatusStage { + return new ResponseBuilder(this.method, this.buildUrl(), this.predicates, this.handlerOptions); + } + + private buildUrl(): string { + return url.join(this._baseUrl, this.path); + } +} + +class ResponseBuilder implements ResponseStatusStage, ResponseHeaderStage, ResponseBodyStage, BuildStage { + private readonly method: HttpMethod; + private readonly url: string; + private readonly requestPredicates: ((resolver: HttpResponseResolver) => HttpResponseResolver)[]; + private readonly handlerOptions?: HttpHandlerBuilderOptions; + + private responseStatusCode: number = 200; + private responseHeaders: Record = {}; + private responseBody: DefaultBodyType = undefined; + + constructor( + method: HttpMethod, + url: string, + requestPredicates: ((resolver: HttpResponseResolver) => HttpResponseResolver)[], + options?: HttpHandlerBuilderOptions, + ) { + this.method = method; + this.url = url; + this.requestPredicates = requestPredicates; + this.handlerOptions = options; + } + + public statusCode(code: number): ResponseHeaderStage { + this.responseStatusCode = code; + return this; + } + + public header(name: string, value: string): ResponseHeaderStage { + this.responseHeaders[name] = value; + return this; + } + + public headers(headers: Record): ResponseHeaderStage { + this.responseHeaders = { ...this.responseHeaders, ...headers }; + return this; + } + + public jsonBody(body: unknown): BuildStage { + if (body === undefined) { + throw new Error("Undefined is not valid JSON. Do not call jsonBody if you expect an empty body."); + } + this.responseBody = toJson(body); + return this; + } + + public sseBody(body: string): BuildStage { + this.responseHeaders["Content-Type"] = "text/event-stream"; + this.responseBody = body; + return this; + } + + public build(): HttpHandler { + const responseResolver: HttpResponseResolver = () => { + const response = new HttpResponse(this.responseBody, { + status: this.responseStatusCode, + headers: this.responseHeaders, + }); + // if no Content-Type header is set, delete the default text content type that is set + if (Object.keys(this.responseHeaders).some((key) => key.toLowerCase() === "content-type") === false) { + response.headers.delete("Content-Type"); + } + return response; + }; + + const finalResolver = this.requestPredicates.reduceRight((acc, predicate) => predicate(acc), responseResolver); + + const handler = http[this.method](this.url, finalResolver, this.handlerOptions); + this.handlerOptions?.onBuild?.(handler); + return handler; + } +} + +export function mockEndpointBuilder(options?: HttpHandlerBuilderOptions): MethodStage { + return new RequestBuilder(options); +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/randomBaseUrl.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/randomBaseUrl.ts new file mode 100644 index 000000000000..031aa6408aca --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/randomBaseUrl.ts @@ -0,0 +1,4 @@ +export function randomBaseUrl(): string { + const randomString = Math.random().toString(36).substring(2, 15); + return `http://${randomString}.localhost`; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/setup.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/setup.ts new file mode 100644 index 000000000000..aeb3a95af7dc --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/setup.ts @@ -0,0 +1,10 @@ +import { afterAll, beforeAll } from "vitest"; + +import { mockServerPool } from "./MockServerPool"; + +beforeAll(() => { + mockServerPool.listen(); +}); +afterAll(() => { + mockServerPool.close(); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withFormUrlEncoded.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withFormUrlEncoded.ts new file mode 100644 index 000000000000..2b23448e3102 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withFormUrlEncoded.ts @@ -0,0 +1,104 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +import { toJson } from "../../src/core/json"; + +export interface WithFormUrlEncodedOptions { + /** + * List of field names to ignore when comparing request bodies. + * This is useful for pagination cursor fields that change between requests. + */ + ignoredFields?: string[]; +} + +/** + * Creates a request matcher that validates if the request form-urlencoded body exactly matches the expected object + * @param expectedBody - The exact body object to match against + * @param resolver - Response resolver to execute if body matches + * @param options - Optional configuration including fields to ignore + */ +export function withFormUrlEncoded( + expectedBody: unknown, + resolver: HttpResponseResolver, + options?: WithFormUrlEncodedOptions, +): HttpResponseResolver { + const ignoredFields = options?.ignoredFields ?? []; + return async (args) => { + const { request } = args; + + let clonedRequest: Request; + let bodyText: string | undefined; + let actualBody: Record; + try { + clonedRequest = request.clone(); + bodyText = await clonedRequest.text(); + if (bodyText === "") { + // Empty body is valid if expected body is also empty + const isExpectedEmpty = + expectedBody != null && + typeof expectedBody === "object" && + Object.keys(expectedBody as Record).length === 0; + if (!isExpectedEmpty) { + console.error("Request body is empty, expected a form-urlencoded body."); + return passthrough(); + } + actualBody = {}; + } else { + const params = new URLSearchParams(bodyText); + actualBody = {}; + for (const [key, value] of params.entries()) { + actualBody[key] = value; + } + } + } catch (error) { + console.error(`Error processing form-urlencoded request body:\n\tError: ${error}\n\tBody: ${bodyText}`); + return passthrough(); + } + + const mismatches = findMismatches(actualBody, expectedBody); + const filteredMismatches = Object.keys(mismatches).filter((key) => !ignoredFields.includes(key)); + if (filteredMismatches.length > 0) { + console.error("Form-urlencoded body mismatch:", toJson(mismatches, undefined, 2)); + return passthrough(); + } + + return resolver(args); + }; +} + +function findMismatches(actual: any, expected: any): Record { + const mismatches: Record = {}; + + if (typeof actual !== typeof expected) { + return { value: { actual, expected } }; + } + + if (typeof actual !== "object" || actual === null || expected === null) { + if (actual !== expected) { + return { value: { actual, expected } }; + } + return {}; + } + + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + + const allKeys = new Set([...actualKeys, ...expectedKeys]); + + for (const key of allKeys) { + if (!expectedKeys.includes(key)) { + if (actual[key] === undefined) { + continue; + } + mismatches[key] = { actual: actual[key], expected: undefined }; + } else if (!actualKeys.includes(key)) { + if (expected[key] === undefined) { + continue; + } + mismatches[key] = { actual: undefined, expected: expected[key] }; + } else if (actual[key] !== expected[key]) { + mismatches[key] = { actual: actual[key], expected: expected[key] }; + } + } + + return mismatches; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withHeaders.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withHeaders.ts new file mode 100644 index 000000000000..6599d2b4a92d --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withHeaders.ts @@ -0,0 +1,70 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +/** + * Creates a request matcher that validates if request headers match specified criteria + * @param expectedHeaders - Headers to match against + * @param resolver - Response resolver to execute if headers match + */ +export function withHeaders( + expectedHeaders: Record boolean)>, + resolver: HttpResponseResolver, +): HttpResponseResolver { + return (args) => { + const { request } = args; + const { headers } = request; + + const mismatches: Record< + string, + { actual: string | null; expected: string | RegExp | ((value: string) => boolean) } + > = {}; + + for (const [key, expectedValue] of Object.entries(expectedHeaders)) { + const actualValue = headers.get(key); + + if (actualValue === null) { + mismatches[key] = { actual: null, expected: expectedValue }; + continue; + } + + if (typeof expectedValue === "function") { + if (!expectedValue(actualValue)) { + mismatches[key] = { actual: actualValue, expected: expectedValue }; + } + } else if (expectedValue instanceof RegExp) { + if (!expectedValue.test(actualValue)) { + mismatches[key] = { actual: actualValue, expected: expectedValue }; + } + } else if (expectedValue !== actualValue) { + mismatches[key] = { actual: actualValue, expected: expectedValue }; + } + } + + if (Object.keys(mismatches).length > 0) { + const formattedMismatches = formatHeaderMismatches(mismatches); + console.error("Header mismatch:", formattedMismatches); + return passthrough(); + } + + return resolver(args); + }; +} + +function formatHeaderMismatches( + mismatches: Record boolean) }>, +): Record { + const formatted: Record = {}; + + for (const [key, { actual, expected }] of Object.entries(mismatches)) { + formatted[key] = { + actual, + expected: + expected instanceof RegExp + ? expected.toString() + : typeof expected === "function" + ? "[Function]" + : expected, + }; + } + + return formatted; +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withJson.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withJson.ts new file mode 100644 index 000000000000..3e8800a0c374 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/mock-server/withJson.ts @@ -0,0 +1,173 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +import { fromJson, toJson } from "../../src/core/json"; + +export interface WithJsonOptions { + /** + * List of field names to ignore when comparing request bodies. + * This is useful for pagination cursor fields that change between requests. + */ + ignoredFields?: string[]; +} + +/** + * Creates a request matcher that validates if the request JSON body exactly matches the expected object + * @param expectedBody - The exact body object to match against + * @param resolver - Response resolver to execute if body matches + * @param options - Optional configuration including fields to ignore + */ +export function withJson( + expectedBody: unknown, + resolver: HttpResponseResolver, + options?: WithJsonOptions, +): HttpResponseResolver { + const ignoredFields = options?.ignoredFields ?? []; + return async (args) => { + const { request } = args; + + let clonedRequest: Request; + let bodyText: string | undefined; + let actualBody: unknown; + try { + clonedRequest = request.clone(); + bodyText = await clonedRequest.text(); + if (bodyText === "") { + console.error("Request body is empty, expected a JSON object."); + return passthrough(); + } + actualBody = fromJson(bodyText); + } catch (error) { + console.error(`Error processing request body:\n\tError: ${error}\n\tBody: ${bodyText}`); + return passthrough(); + } + + const mismatches = findMismatches(actualBody, expectedBody); + const filteredMismatches = Object.keys(mismatches).filter((key) => !ignoredFields.includes(key)); + if (filteredMismatches.length > 0) { + console.error("JSON body mismatch:", toJson(mismatches, undefined, 2)); + return passthrough(); + } + + return resolver(args); + }; +} + +function findMismatches(actual: any, expected: any): Record { + const mismatches: Record = {}; + + if (typeof actual !== typeof expected) { + if (areEquivalent(actual, expected)) { + return {}; + } + return { value: { actual, expected } }; + } + + if (typeof actual !== "object" || actual === null || expected === null) { + if (actual !== expected) { + if (areEquivalent(actual, expected)) { + return {}; + } + return { value: { actual, expected } }; + } + return {}; + } + + if (Array.isArray(actual) && Array.isArray(expected)) { + if (actual.length !== expected.length) { + return { length: { actual: actual.length, expected: expected.length } }; + } + + const arrayMismatches: Record = {}; + for (let i = 0; i < actual.length; i++) { + const itemMismatches = findMismatches(actual[i], expected[i]); + if (Object.keys(itemMismatches).length > 0) { + for (const [mismatchKey, mismatchValue] of Object.entries(itemMismatches)) { + arrayMismatches[`[${i}]${mismatchKey === "value" ? "" : `.${mismatchKey}`}`] = mismatchValue; + } + } + } + return arrayMismatches; + } + + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + + const allKeys = new Set([...actualKeys, ...expectedKeys]); + + for (const key of allKeys) { + if (!expectedKeys.includes(key)) { + if (actual[key] === undefined) { + continue; // Skip undefined values in actual + } + mismatches[key] = { actual: actual[key], expected: undefined }; + } else if (!actualKeys.includes(key)) { + if (expected[key] === undefined) { + continue; // Skip undefined values in expected + } + mismatches[key] = { actual: undefined, expected: expected[key] }; + } else if ( + typeof actual[key] === "object" && + actual[key] !== null && + typeof expected[key] === "object" && + expected[key] !== null + ) { + const nestedMismatches = findMismatches(actual[key], expected[key]); + if (Object.keys(nestedMismatches).length > 0) { + for (const [nestedKey, nestedValue] of Object.entries(nestedMismatches)) { + mismatches[`${key}${nestedKey === "value" ? "" : `.${nestedKey}`}`] = nestedValue; + } + } + } else if (actual[key] !== expected[key]) { + if (areEquivalent(actual[key], expected[key])) { + continue; + } + mismatches[key] = { actual: actual[key], expected: expected[key] }; + } + } + + return mismatches; +} + +function areEquivalent(actual: unknown, expected: unknown): boolean { + if (actual === expected) { + return true; + } + if (isEquivalentBigInt(actual, expected)) { + return true; + } + if (isEquivalentDatetime(actual, expected)) { + return true; + } + return false; +} + +function isEquivalentBigInt(actual: unknown, expected: unknown) { + if (typeof actual === "number") { + actual = BigInt(actual); + } + if (typeof expected === "number") { + expected = BigInt(expected); + } + if (typeof actual === "bigint" && typeof expected === "bigint") { + return actual === expected; + } + return false; +} + +function isEquivalentDatetime(str1: unknown, str2: unknown): boolean { + if (typeof str1 !== "string" || typeof str2 !== "string") { + return false; + } + const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/; + if (!isoDatePattern.test(str1) || !isoDatePattern.test(str2)) { + return false; + } + + try { + const date1 = new Date(str1).getTime(); + const date2 = new Date(str2).getTime(); + return date1 === date2; + } catch { + return false; + } +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/setup.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/setup.ts new file mode 100644 index 000000000000..a5651f81ba10 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/setup.ts @@ -0,0 +1,80 @@ +import { expect } from "vitest"; + +interface CustomMatchers { + toContainHeaders(expectedHeaders: Record): R; +} + +declare module "vitest" { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + +expect.extend({ + toContainHeaders(actual: unknown, expectedHeaders: Record) { + const isHeaders = actual instanceof Headers; + const isPlainObject = typeof actual === "object" && actual !== null && !Array.isArray(actual); + + if (!isHeaders && !isPlainObject) { + throw new TypeError("Received value must be an instance of Headers or a plain object!"); + } + + if (typeof expectedHeaders !== "object" || expectedHeaders === null || Array.isArray(expectedHeaders)) { + throw new TypeError("Expected headers must be a plain object!"); + } + + const missingHeaders: string[] = []; + const mismatchedHeaders: Array<{ key: string; expected: string; actual: string | null }> = []; + + for (const [key, value] of Object.entries(expectedHeaders)) { + let actualValue: string | null = null; + + if (isHeaders) { + // Headers.get() is already case-insensitive + actualValue = (actual as Headers).get(key); + } else { + // For plain objects, do case-insensitive lookup + const actualObj = actual as Record; + const lowerKey = key.toLowerCase(); + const foundKey = Object.keys(actualObj).find((k) => k.toLowerCase() === lowerKey); + actualValue = foundKey ? actualObj[foundKey] : null; + } + + if (actualValue === null || actualValue === undefined) { + missingHeaders.push(key); + } else if (actualValue !== value) { + mismatchedHeaders.push({ key, expected: value, actual: actualValue }); + } + } + + const pass = missingHeaders.length === 0 && mismatchedHeaders.length === 0; + + const actualType = isHeaders ? "Headers" : "object"; + + if (pass) { + return { + message: () => `expected ${actualType} not to contain ${this.utils.printExpected(expectedHeaders)}`, + pass: true, + }; + } else { + const messages: string[] = []; + + if (missingHeaders.length > 0) { + messages.push(`Missing headers: ${this.utils.printExpected(missingHeaders.join(", "))}`); + } + + if (mismatchedHeaders.length > 0) { + const mismatches = mismatchedHeaders.map( + ({ key, expected, actual }) => + `${key}: expected ${this.utils.printExpected(expected)} but got ${this.utils.printReceived(actual)}`, + ); + messages.push(mismatches.join("\n")); + } + + return { + message: () => + `expected ${actualType} to contain ${this.utils.printExpected(expectedHeaders)}\n\n${messages.join("\n")}`, + pass: false, + }; + } + }, +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/tsconfig.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/tsconfig.json new file mode 100644 index 000000000000..ac39744de7b2 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": null, + "rootDir": "..", + "types": ["vitest/globals"] + }, + "include": ["../src", "../tests"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BasicAuth.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BasicAuth.test.ts new file mode 100644 index 000000000000..8c82c1b723db --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BasicAuth.test.ts @@ -0,0 +1,112 @@ +import { BasicAuth } from "../../../src/core/auth/BasicAuth"; + +describe("BasicAuth", () => { + interface ToHeaderTestCase { + description: string; + input: { username?: string; password?: string }; + expected: string | undefined; + } + + interface FromHeaderTestCase { + description: string; + input: string; + expected: { username: string; password: string }; + } + + interface ErrorTestCase { + description: string; + input: string; + expectedError: string; + } + + describe("toAuthorizationHeader", () => { + const toHeaderTests: ToHeaderTestCase[] = [ + { + description: "correctly converts to header with both username and password", + input: { username: "username", password: "password" }, + expected: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + }, + { + description: "encodes username only with trailing colon", + input: { username: "username" }, + expected: "Basic dXNlcm5hbWU6", + }, + { + description: "encodes password only with leading colon", + input: { password: "password" }, + expected: "Basic OnBhc3N3b3Jk", + }, + { + description: "returns undefined when neither provided", + input: {}, + expected: undefined, + }, + { + description: "returns undefined when both are empty strings", + input: { username: "", password: "" }, + expected: undefined, + }, + ]; + + toHeaderTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(BasicAuth.toAuthorizationHeader(input)).toBe(expected); + }); + }); + }); + + describe("fromAuthorizationHeader", () => { + const fromHeaderTests: FromHeaderTestCase[] = [ + { + description: "correctly parses header", + input: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + expected: { username: "username", password: "password" }, + }, + { + description: "handles password with colons", + input: "Basic dXNlcjpwYXNzOndvcmQ=", + expected: { username: "user", password: "pass:word" }, + }, + { + description: "handles empty username and password (just colon)", + input: "Basic Og==", + expected: { username: "", password: "" }, + }, + { + description: "handles empty username", + input: "Basic OnBhc3N3b3Jk", + expected: { username: "", password: "password" }, + }, + { + description: "handles empty password", + input: "Basic dXNlcm5hbWU6", + expected: { username: "username", password: "" }, + }, + ]; + + fromHeaderTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(BasicAuth.fromAuthorizationHeader(input)).toEqual(expected); + }); + }); + + const errorTests: ErrorTestCase[] = [ + { + description: "throws error for completely empty credentials", + input: "Basic ", + expectedError: "Invalid basic auth", + }, + { + description: "throws error for credentials without colon", + input: "Basic dXNlcm5hbWU=", + expectedError: "Invalid basic auth", + }, + ]; + + errorTests.forEach(({ description, input, expectedError }) => { + it(description, () => { + expect(() => BasicAuth.fromAuthorizationHeader(input)).toThrow(expectedError); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BearerToken.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BearerToken.test.ts new file mode 100644 index 000000000000..7757b87cb97e --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/auth/BearerToken.test.ts @@ -0,0 +1,14 @@ +import { BearerToken } from "../../../src/core/auth/BearerToken"; + +describe("BearerToken", () => { + describe("toAuthorizationHeader", () => { + it("correctly converts to header", () => { + expect(BearerToken.toAuthorizationHeader("my-token")).toBe("Bearer my-token"); + }); + }); + describe("fromAuthorizationHeader", () => { + it("correctly parses header", () => { + expect(BearerToken.fromAuthorizationHeader("Bearer my-token")).toBe("my-token"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/base64.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/base64.test.ts new file mode 100644 index 000000000000..939594ca277b --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/base64.test.ts @@ -0,0 +1,53 @@ +import { base64Decode, base64Encode } from "../../src/core/base64"; + +describe("base64", () => { + describe("base64Encode", () => { + it("should encode ASCII strings", () => { + expect(base64Encode("hello")).toBe("aGVsbG8="); + expect(base64Encode("")).toBe(""); + }); + + it("should encode UTF-8 strings", () => { + expect(base64Encode("café")).toBe("Y2Fmw6k="); + expect(base64Encode("🎉")).toBe("8J+OiQ=="); + }); + + it("should handle basic auth credentials", () => { + expect(base64Encode("username:password")).toBe("dXNlcm5hbWU6cGFzc3dvcmQ="); + }); + }); + + describe("base64Decode", () => { + it("should decode ASCII strings", () => { + expect(base64Decode("aGVsbG8=")).toBe("hello"); + expect(base64Decode("")).toBe(""); + }); + + it("should decode UTF-8 strings", () => { + expect(base64Decode("Y2Fmw6k=")).toBe("café"); + expect(base64Decode("8J+OiQ==")).toBe("🎉"); + }); + + it("should handle basic auth credentials", () => { + expect(base64Decode("dXNlcm5hbWU6cGFzc3dvcmQ=")).toBe("username:password"); + }); + }); + + describe("round-trip encoding", () => { + const testStrings = [ + "hello world", + "test@example.com", + "café", + "username:password", + "user@domain.com:super$ecret123!", + ]; + + testStrings.forEach((testString) => { + it(`should round-trip encode/decode: "${testString}"`, () => { + const encoded = base64Encode(testString); + const decoded = base64Decode(encoded); + expect(decoded).toBe(testString); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/Fetcher.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/Fetcher.test.ts new file mode 100644 index 000000000000..6c17624228bb --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/Fetcher.test.ts @@ -0,0 +1,262 @@ +import fs from "fs"; +import { join } from "path"; +import stream from "stream"; +import type { BinaryResponse } from "../../../src/core"; +import { type Fetcher, fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +describe("Test fetcherImpl", () => { + it("should handle successful request", async () => { + const mockArgs: Fetcher.Args = { + url: "https://httpbin.org/post", + method: "POST", + headers: { "X-Test": "x-test-header" }, + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + maxRetries: 0, + responseType: "json", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + }), + ); + + const result = await fetcherImpl(mockArgs); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.body).toEqual({ data: "test" }); + } + + expect(global.fetch).toHaveBeenCalledWith( + "https://httpbin.org/post", + expect.objectContaining({ + method: "POST", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + body: JSON.stringify({ data: "test" }), + }), + ); + }); + + it("should send octet stream", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "POST", + headers: { "X-Test": "x-test-header" }, + contentType: "application/octet-stream", + requestType: "bytes", + maxRetries: 0, + responseType: "json", + body: fs.createReadStream(join(__dirname, "test-file.txt")), + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + }), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "POST", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + body: expect.any(fs.ReadStream), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.body).toEqual({ data: "test" }); + } + }); + + it("should receive file as stream", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.stream).toBe("function"); + const stream = body.stream(); + expect(stream).toBeInstanceOf(ReadableStream); + const readableStream = stream as ReadableStream; + const reader = readableStream.getReader(); + const { value } = await reader.read(); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(value); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); + + it("should receive file as blob", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.blob).toBe("function"); + const blob = await body.blob(); + expect(blob).toBeInstanceOf(Blob); + const reader = blob.stream().getReader(); + const { value } = await reader.read(); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(value); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); + + it("should receive file as arraybuffer", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.arrayBuffer).toBe("function"); + const arrayBuffer = await body.arrayBuffer(); + expect(arrayBuffer).toBeInstanceOf(ArrayBuffer); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(new Uint8Array(arrayBuffer)); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); + + it("should receive file as bytes", async () => { + const url = "https://httpbin.org/post/file"; + const mockArgs: Fetcher.Args = { + url, + method: "GET", + headers: { "X-Test": "x-test-header" }, + maxRetries: 0, + responseType: "binary-response", + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response( + stream.Readable.toWeb(fs.createReadStream(join(__dirname, "test-file.txt"))) as ReadableStream, + { + status: 200, + statusText: "OK", + }, + ), + ); + + const result = await fetcherImpl(mockArgs); + + expect(global.fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: "GET", + headers: expect.toContainHeaders({ "X-Test": "x-test-header" }), + }), + ); + expect(result.ok).toBe(true); + if (result.ok) { + const body = result.body as BinaryResponse; + expect(body).toBeDefined(); + expect(body.bodyUsed).toBe(false); + expect(typeof body.bytes).toBe("function"); + if (!body.bytes) { + return; + } + const bytes = await body.bytes(); + expect(bytes).toBeInstanceOf(Uint8Array); + const decoder = new TextDecoder(); + const streamContent = decoder.decode(bytes); + expect(streamContent.trim()).toBe("This is a test file!"); + expect(body.bodyUsed).toBe(true); + } + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/HttpResponsePromise.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/HttpResponsePromise.test.ts new file mode 100644 index 000000000000..2ec008e581d8 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/HttpResponsePromise.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { HttpResponsePromise } from "../../../src/core/fetcher/HttpResponsePromise"; +import type { RawResponse, WithRawResponse } from "../../../src/core/fetcher/RawResponse"; + +describe("HttpResponsePromise", () => { + const mockRawResponse: RawResponse = { + headers: new Headers(), + redirected: false, + status: 200, + statusText: "OK", + type: "basic" as ResponseType, + url: "https://example.com", + }; + const mockData = { id: "123", name: "test" }; + const mockWithRawResponse: WithRawResponse = { + data: mockData, + rawResponse: mockRawResponse, + }; + + describe("fromFunction", () => { + it("should create an HttpResponsePromise from a function", async () => { + const mockFn = vi + .fn<(arg1: string, arg2: string) => Promise>>() + .mockResolvedValue(mockWithRawResponse); + + const responsePromise = HttpResponsePromise.fromFunction(mockFn, "arg1", "arg2"); + + const result = await responsePromise; + expect(result).toEqual(mockData); + expect(mockFn).toHaveBeenCalledWith("arg1", "arg2"); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("fromPromise", () => { + it("should create an HttpResponsePromise from a promise", async () => { + const promise = Promise.resolve(mockWithRawResponse); + + const responsePromise = HttpResponsePromise.fromPromise(promise); + + const result = await responsePromise; + expect(result).toEqual(mockData); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("fromExecutor", () => { + it("should create an HttpResponsePromise from an executor function", async () => { + const responsePromise = HttpResponsePromise.fromExecutor((resolve) => { + resolve(mockWithRawResponse); + }); + + const result = await responsePromise; + expect(result).toEqual(mockData); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("fromResult", () => { + it("should create an HttpResponsePromise from a result", async () => { + const responsePromise = HttpResponsePromise.fromResult(mockWithRawResponse); + + const result = await responsePromise; + expect(result).toEqual(mockData); + + const resultWithRawResponse = await responsePromise.withRawResponse(); + expect(resultWithRawResponse).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); + + describe("Promise methods", () => { + let responsePromise: HttpResponsePromise; + + beforeEach(() => { + responsePromise = HttpResponsePromise.fromResult(mockWithRawResponse); + }); + + it("should support then() method", async () => { + const result = await responsePromise.then((data) => ({ + ...data, + modified: true, + })); + + expect(result).toEqual({ + ...mockData, + modified: true, + }); + }); + + it("should support catch() method", async () => { + const errorResponsePromise = HttpResponsePromise.fromExecutor((_, reject) => { + reject(new Error("Test error")); + }); + + const catchSpy = vi.fn(); + await errorResponsePromise.catch(catchSpy); + + expect(catchSpy).toHaveBeenCalled(); + const error = catchSpy.mock.calls[0]?.[0]; + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toBe("Test error"); + }); + + it("should support finally() method", async () => { + const finallySpy = vi.fn(); + await responsePromise.finally(finallySpy); + + expect(finallySpy).toHaveBeenCalled(); + }); + }); + + describe("withRawResponse", () => { + it("should return both data and raw response", async () => { + const responsePromise = HttpResponsePromise.fromResult(mockWithRawResponse); + + const result = await responsePromise.withRawResponse(); + + expect(result).toEqual({ + data: mockData, + rawResponse: mockRawResponse, + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/RawResponse.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/RawResponse.test.ts new file mode 100644 index 000000000000..375ee3f38064 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/RawResponse.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { toRawResponse } from "../../../src/core/fetcher/RawResponse"; + +describe("RawResponse", () => { + describe("toRawResponse", () => { + it("should convert Response to RawResponse by removing body, bodyUsed, and ok properties", () => { + const mockHeaders = new Headers({ "content-type": "application/json" }); + const mockResponse = { + body: "test body", + bodyUsed: false, + ok: true, + headers: mockHeaders, + redirected: false, + status: 200, + statusText: "OK", + type: "basic" as ResponseType, + url: "https://example.com", + }; + + const result = toRawResponse(mockResponse as unknown as Response); + + expect("body" in result).toBe(false); + expect("bodyUsed" in result).toBe(false); + expect("ok" in result).toBe(false); + expect(result.headers).toBe(mockHeaders); + expect(result.redirected).toBe(false); + expect(result.status).toBe(200); + expect(result.statusText).toBe("OK"); + expect(result.type).toBe("basic"); + expect(result.url).toBe("https://example.com"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/createRequestUrl.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/createRequestUrl.test.ts new file mode 100644 index 000000000000..7787e5530fcd --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/createRequestUrl.test.ts @@ -0,0 +1,167 @@ +import { createRequestUrl } from "../../../src/core/fetcher/createRequestUrl"; + +describe("Test createRequestUrl", () => { + const BASE_URL = "https://api.example.com"; + + interface TestCase { + description: string; + baseUrl: string; + queryParams?: Record; + expected: string; + } + + const testCases: TestCase[] = [ + { + description: "should return the base URL when no query parameters are provided", + baseUrl: BASE_URL, + expected: BASE_URL, + }, + { + description: "should append simple query parameters", + baseUrl: BASE_URL, + queryParams: { key: "value", another: "param" }, + expected: "https://api.example.com?key=value&another=param", + }, + { + description: "should handle array query parameters", + baseUrl: BASE_URL, + queryParams: { items: ["a", "b", "c"] }, + expected: "https://api.example.com?items=a&items=b&items=c", + }, + { + description: "should handle object query parameters", + baseUrl: BASE_URL, + queryParams: { filter: { name: "John", age: 30 } }, + expected: "https://api.example.com?filter%5Bname%5D=John&filter%5Bage%5D=30", + }, + { + description: "should handle mixed types of query parameters", + baseUrl: BASE_URL, + queryParams: { + simple: "value", + array: ["x", "y"], + object: { key: "value" }, + }, + expected: "https://api.example.com?simple=value&array=x&array=y&object%5Bkey%5D=value", + }, + { + description: "should handle empty query parameters object", + baseUrl: BASE_URL, + queryParams: {}, + expected: BASE_URL, + }, + { + description: "should encode special characters in query parameters", + baseUrl: BASE_URL, + queryParams: { special: "a&b=c d" }, + expected: "https://api.example.com?special=a%26b%3Dc%20d", + }, + { + description: "should handle numeric values", + baseUrl: BASE_URL, + queryParams: { count: 42, price: 19.99, active: 1, inactive: 0 }, + expected: "https://api.example.com?count=42&price=19.99&active=1&inactive=0", + }, + { + description: "should handle boolean values", + baseUrl: BASE_URL, + queryParams: { enabled: true, disabled: false }, + expected: "https://api.example.com?enabled=true&disabled=false", + }, + { + description: "should handle null and undefined values", + baseUrl: BASE_URL, + queryParams: { + valid: "value", + nullValue: null, + undefinedValue: undefined, + emptyString: "", + }, + expected: "https://api.example.com?valid=value&emptyString=", + }, + { + description: "should handle deeply nested objects", + baseUrl: BASE_URL, + queryParams: { + user: { + profile: { + name: "John", + settings: { theme: "dark" }, + }, + }, + }, + expected: + "https://api.example.com?user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", + }, + { + description: "should handle arrays of objects", + baseUrl: BASE_URL, + queryParams: { + users: [ + { name: "John", age: 30 }, + { name: "Jane", age: 25 }, + ], + }, + expected: + "https://api.example.com?users%5Bname%5D=John&users%5Bage%5D=30&users%5Bname%5D=Jane&users%5Bage%5D=25", + }, + { + description: "should handle mixed arrays", + baseUrl: BASE_URL, + queryParams: { + mixed: ["string", 42, true, { key: "value" }], + }, + expected: "https://api.example.com?mixed=string&mixed=42&mixed=true&mixed%5Bkey%5D=value", + }, + { + description: "should handle empty arrays", + baseUrl: BASE_URL, + queryParams: { emptyArray: [] }, + expected: BASE_URL, + }, + { + description: "should handle empty objects", + baseUrl: BASE_URL, + queryParams: { emptyObject: {} }, + expected: BASE_URL, + }, + { + description: "should handle special characters in keys", + baseUrl: BASE_URL, + queryParams: { "key with spaces": "value", "key[with]brackets": "value" }, + expected: "https://api.example.com?key%20with%20spaces=value&key%5Bwith%5Dbrackets=value", + }, + { + description: "should handle URL with existing query parameters", + baseUrl: "https://api.example.com?existing=param", + queryParams: { new: "value" }, + expected: "https://api.example.com?existing=param?new=value", + }, + { + description: "should handle complex nested structures", + baseUrl: BASE_URL, + queryParams: { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], + }, + }, + sort: { field: "name", direction: "asc" }, + }, + expected: + "https://api.example.com?filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + }, + ]; + + testCases.forEach(({ description, baseUrl, queryParams, expected }) => { + it(description, () => { + expect(createRequestUrl(baseUrl, queryParams)).toBe(expected); + }); + }); + + it("should default to repeat format for arrays", () => { + expect(createRequestUrl(BASE_URL, { items: ["a", "b"] })).toBe("https://api.example.com?items=a&items=b"); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getRequestBody.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getRequestBody.test.ts new file mode 100644 index 000000000000..8a6c3a57e211 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getRequestBody.test.ts @@ -0,0 +1,129 @@ +import { getRequestBody } from "../../../src/core/fetcher/getRequestBody"; +import { RUNTIME } from "../../../src/core/runtime"; + +describe("Test getRequestBody", () => { + interface TestCase { + description: string; + input: any; + type: "json" | "form" | "file" | "bytes" | "other"; + expected: any; + skipCondition?: () => boolean; + } + + const testCases: TestCase[] = [ + { + description: "should stringify body if not FormData in Node environment", + input: { key: "value" }, + type: "json", + expected: '{"key":"value"}', + skipCondition: () => RUNTIME.type !== "node", + }, + { + description: "should stringify body if not FormData in browser environment", + input: { key: "value" }, + type: "json", + expected: '{"key":"value"}', + skipCondition: () => RUNTIME.type !== "browser", + }, + { + description: "should return the Uint8Array", + input: new Uint8Array([1, 2, 3]), + type: "bytes", + expected: new Uint8Array([1, 2, 3]), + }, + { + description: "should serialize objects for form-urlencoded content type", + input: { username: "johndoe", email: "john@example.com" }, + type: "form", + expected: "username=johndoe&email=john%40example.com", + }, + { + description: "should serialize complex nested objects and arrays for form-urlencoded content type", + input: { + user: { + profile: { + name: "John Doe", + settings: { + theme: "dark", + notifications: true, + }, + }, + tags: ["admin", "user"], + contacts: [ + { type: "email", value: "john@example.com" }, + { type: "phone", value: "+1234567890" }, + ], + }, + filters: { + status: ["active", "pending"], + metadata: { + created: "2024-01-01", + categories: ["electronics", "books"], + }, + }, + preferences: ["notifications", "updates"], + }, + type: "form", + expected: + "user%5Bprofile%5D%5Bname%5D=John%20Doe&" + + "user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark&" + + "user%5Bprofile%5D%5Bsettings%5D%5Bnotifications%5D=true&" + + "user%5Btags%5D=admin&" + + "user%5Btags%5D=user&" + + "user%5Bcontacts%5D%5Btype%5D=email&" + + "user%5Bcontacts%5D%5Bvalue%5D=john%40example.com&" + + "user%5Bcontacts%5D%5Btype%5D=phone&" + + "user%5Bcontacts%5D%5Bvalue%5D=%2B1234567890&" + + "filters%5Bstatus%5D=active&" + + "filters%5Bstatus%5D=pending&" + + "filters%5Bmetadata%5D%5Bcreated%5D=2024-01-01&" + + "filters%5Bmetadata%5D%5Bcategories%5D=electronics&" + + "filters%5Bmetadata%5D%5Bcategories%5D=books&" + + "preferences=notifications&" + + "preferences=updates", + }, + { + description: "should return the input for pre-serialized form-urlencoded strings", + input: "key=value&another=param", + type: "other", + expected: "key=value&another=param", + }, + { + description: "should JSON stringify objects", + input: { key: "value" }, + type: "json", + expected: '{"key":"value"}', + }, + ]; + + testCases.forEach(({ description, input, type, expected, skipCondition }) => { + it(description, async () => { + if (skipCondition?.()) { + return; + } + + const result = await getRequestBody({ + body: input, + type, + }); + + if (input instanceof Uint8Array) { + expect(result).toBe(input); + } else { + expect(result).toBe(expected); + } + }); + }); + + it("should return FormData in browser environment", async () => { + if (RUNTIME.type === "browser") { + const formData = new FormData(); + formData.append("key", "value"); + const result = await getRequestBody({ + body: formData, + type: "file", + }); + expect(result).toBe(formData); + } + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getResponseBody.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getResponseBody.test.ts new file mode 100644 index 000000000000..64ed7461ec3a --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/getResponseBody.test.ts @@ -0,0 +1,123 @@ +import { getResponseBody } from "../../../src/core/fetcher/getResponseBody"; + +import { RUNTIME } from "../../../src/core/runtime"; + +describe("Test getResponseBody", () => { + interface SimpleTestCase { + description: string; + responseData: string | Record; + responseType?: "blob" | "sse" | "streaming" | "text"; + expected: any; + skipCondition?: () => boolean; + } + + const simpleTestCases: SimpleTestCase[] = [ + { + description: "should handle text response type", + responseData: "test text", + responseType: "text", + expected: "test text", + }, + { + description: "should handle JSON response", + responseData: { key: "value" }, + expected: { key: "value" }, + }, + { + description: "should handle empty response", + responseData: "", + expected: undefined, + }, + { + description: "should handle non-JSON response", + responseData: "invalid json", + expected: { + ok: false, + error: { + reason: "non-json", + statusCode: 200, + rawBody: "invalid json", + }, + }, + }, + ]; + + simpleTestCases.forEach(({ description, responseData, responseType, expected, skipCondition }) => { + it(description, async () => { + if (skipCondition?.()) { + return; + } + + const mockResponse = new Response( + typeof responseData === "string" ? responseData : JSON.stringify(responseData), + ); + const result = await getResponseBody(mockResponse, responseType); + expect(result).toEqual(expected); + }); + }); + + it("should handle blob response type", async () => { + const mockBlob = new Blob(["test"], { type: "text/plain" }); + const mockResponse = new Response(mockBlob); + const result = await getResponseBody(mockResponse, "blob"); + // @ts-expect-error + expect(result.constructor.name).toBe("Blob"); + }); + + it("should handle sse response type", async () => { + if (RUNTIME.type === "node") { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = await getResponseBody(mockResponse, "sse"); + expect(result).toBe(mockStream); + } + }); + + it("should retain a reference to the parent Response for sse responses", async () => { + if (RUNTIME.type === "node") { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = (await getResponseBody(mockResponse, "sse")) as ReadableStream & { + __fern_response_ref?: Response; + }; + // Pins the parent Response so undici's FinalizationRegistry can't GC it and cancel the stream. + expect(result.__fern_response_ref).toBe(mockResponse); + // The pin must be non-enumerable so it does not leak through JSON.stringify or Object.keys. + const descriptor = Object.getOwnPropertyDescriptor(result, "__fern_response_ref"); + expect(descriptor?.enumerable).toBe(false); + } + }); + + it("should handle streaming response type", async () => { + const encoder = new TextEncoder(); + const testData = "test stream data"; + const mockStream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(testData)); + controller.close(); + }, + }); + + const mockResponse = new Response(mockStream); + const result = (await getResponseBody(mockResponse, "streaming")) as ReadableStream; + + expect(result).toBeInstanceOf(ReadableStream); + + const reader = result.getReader(); + const decoder = new TextDecoder(); + const { value } = await reader.read(); + const streamContent = decoder.decode(value); + expect(streamContent).toBe(testData); + }); + + it("should retain a reference to the parent Response for streaming responses", async () => { + const mockStream = new ReadableStream(); + const mockResponse = new Response(mockStream); + const result = (await getResponseBody(mockResponse, "streaming")) as ReadableStream & { + __fern_response_ref?: Response; + }; + expect(result.__fern_response_ref).toBe(mockResponse); + const descriptor = Object.getOwnPropertyDescriptor(result, "__fern_response_ref"); + expect(descriptor?.enumerable).toBe(false); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/logging.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/logging.test.ts new file mode 100644 index 000000000000..366c9b6ced61 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/logging.test.ts @@ -0,0 +1,517 @@ +import { fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +function createMockLogger() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +function mockSuccessResponse(data: unknown = { data: "test" }, status = 200, statusText = "OK") { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(data), { + status, + statusText, + }), + ); +} + +function mockErrorResponse(data: unknown = { error: "Error" }, status = 404, statusText = "Not Found") { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(data), { + status, + statusText, + }), + ); +} + +describe("Fetcher Logging Integration", () => { + describe("Request Logging", () => { + it("should log successful request at debug level", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + headers: { "Content-Type": "application/json" }, + body: { test: "data" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "POST", + url: "https://example.com/api", + headers: expect.toContainHeaders({ + "Content-Type": "application/json", + }), + hasBody: true, + }), + ); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + method: "POST", + url: "https://example.com/api", + statusCode: 200, + }), + ); + }); + + it("should not log debug messages at info level for successful requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "info", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + }); + + it("should log request with body flag", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + hasBody: true, + }), + ); + }); + + it("should log request without body flag", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + hasBody: false, + }), + ); + }); + + it("should not log when silent mode is enabled", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: true, + }, + }); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it("should not log when no logging config is provided", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + }); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + }); + }); + + describe("Error Logging", () => { + it("should log 4xx errors at error level", async () => { + const mockLogger = createMockLogger(); + mockErrorResponse({ error: "Not found" }, 404, "Not Found"); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + statusCode: 404, + }), + ); + }); + + it("should log 5xx errors at error level", async () => { + const mockLogger = createMockLogger(); + mockErrorResponse({ error: "Internal error" }, 500, "Internal Server Error"); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + statusCode: 500, + }), + ); + }); + + it("should log aborted request errors", async () => { + const mockLogger = createMockLogger(); + + const abortController = new AbortController(); + abortController.abort(); + + global.fetch = vi.fn().mockRejectedValue(new Error("Aborted")); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + abortSignal: abortController.signal, + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request was aborted", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + }), + ); + }); + + it("should log timeout errors", async () => { + const mockLogger = createMockLogger(); + + const timeoutError = new Error("Request timeout"); + timeoutError.name = "AbortError"; + + global.fetch = vi.fn().mockRejectedValue(timeoutError); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request timed out", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + timeoutMs: undefined, + }), + ); + }); + + it("should log unknown errors", async () => { + const mockLogger = createMockLogger(); + + const unknownError = new Error("Unknown error"); + + global.fetch = vi.fn().mockRejectedValue(unknownError); + + const result = await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(result.ok).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error", + expect.objectContaining({ + method: "GET", + url: "https://example.com/api", + errorMessage: "Unknown error", + }), + ); + }); + }); + + describe("Logging with Redaction", () => { + it("should redact sensitive data in error logs", async () => { + const mockLogger = createMockLogger(); + mockErrorResponse({ error: "Unauthorized" }, 401, "Unauthorized"); + + await fetcherImpl({ + url: "https://example.com/api?api_key=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]", + }), + ); + }); + }); + + describe("Different HTTP Methods", () => { + it("should log GET requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "GET", + }), + ); + }); + + it("should log POST requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse({ data: "test" }, 201, "Created"); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "POST", + }), + ); + }); + + it("should log PUT requests", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "PUT", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "PUT", + }), + ); + }); + + it("should log DELETE requests", async () => { + const mockLogger = createMockLogger(); + global.fetch = vi.fn().mockResolvedValue( + new Response(null, { + status: 200, + statusText: "OK", + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "DELETE", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + method: "DELETE", + }), + ); + }); + }); + + describe("Status Code Logging", () => { + it("should log 2xx success status codes", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse({ data: "test" }, 201, "Created"); + + await fetcherImpl({ + url: "https://example.com/api", + method: "POST", + body: { data: "test" }, + contentType: "application/json", + requestType: "json", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + statusCode: 201, + }), + ); + }); + + it("should log 3xx redirect status codes as success", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse({ data: "test" }, 301, "Moved Permanently"); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + statusCode: 301, + }), + ); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..07cb846739c2 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,504 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + baseUrl: "https://api.example.com", + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("auth header origin scoping", () => { + it("should attach auth headers to relative paths resolved against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should attach auth headers to same-origin absolute URLs", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should attach auth headers when the absolute URL matches the environment origin", async () => { + await makePassthroughRequest("https://env.example.com/v1/users", undefined, { + environment: "https://env.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should NOT attach auth headers to a cross-origin absolute URL", async () => { + await makePassthroughRequest("https://evil.example.com/steal", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should NOT attach auth headers to a cross-origin URL differing only by port", async () => { + await makePassthroughRequest("https://api.example.com:9999/steal", undefined, { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should NOT attach auth headers when no baseUrl or environment is configured", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should still allow explicit init headers on cross-origin requests", async () => { + await makePassthroughRequest( + "https://evil.example.com/steal", + { headers: { "X-Custom": "keep-me" } }, + { + baseUrl: "https://api.example.com", + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + expect(calledOptions.headers["x-custom"]).toBe("keep-me"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 502 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); + + describe("debug logging", () => { + it("should redact credentials in the logged request URL", async () => { + const mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + await makePassthroughRequest("https://user:password@api.example.com/v1/users?token=secret", undefined, { + fetch: mockFetch, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + const loggedUrls = mockLogger.debug.mock.calls.map(([, meta]) => (meta as { url: string }).url); + expect(loggedUrls.length).toBeGreaterThan(0); + for (const loggedUrl of loggedUrls) { + expect(loggedUrl).toBe("https://[REDACTED]@api.example.com/v1/users?token=[REDACTED]"); + expect(loggedUrl).not.toContain("password"); + expect(loggedUrl).not.toContain("secret"); + } + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makeRequest.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makeRequest.test.ts new file mode 100644 index 000000000000..bde194554dd8 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/makeRequest.test.ts @@ -0,0 +1,158 @@ +import type { Mock } from "vitest"; +import { + isCacheNoStoreSupported, + makeRequest, + resetCacheNoStoreSupported, +} from "../../../src/core/fetcher/makeRequest"; + +describe("Test makeRequest", () => { + const mockPostUrl = "https://httpbin.org/post"; + const mockGetUrl = "https://httpbin.org/get"; + const mockHeaders = { "Content-Type": "application/json" }; + const mockBody = JSON.stringify({ key: "value" }); + + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ test: "successful" }), { status: 200 })); + resetCacheNoStoreSupported(); + }); + + it("should handle POST request correctly", async () => { + const response = await makeRequest(mockFetch, mockPostUrl, "POST", mockHeaders, mockBody); + const responseBody = await response.json(); + expect(responseBody).toEqual({ test: "successful" }); + expect(mockFetch).toHaveBeenCalledTimes(1); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe(mockPostUrl); + expect(calledOptions).toEqual( + expect.objectContaining({ + method: "POST", + headers: mockHeaders, + body: mockBody, + credentials: undefined, + }), + ); + expect(calledOptions.signal).toBeDefined(); + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it("should handle GET request correctly", async () => { + const response = await makeRequest(mockFetch, mockGetUrl, "GET", mockHeaders, undefined); + const responseBody = await response.json(); + expect(responseBody).toEqual({ test: "successful" }); + expect(mockFetch).toHaveBeenCalledTimes(1); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe(mockGetUrl); + expect(calledOptions).toEqual( + expect.objectContaining({ + method: "GET", + headers: mockHeaders, + body: undefined, + credentials: undefined, + }), + ); + expect(calledOptions.signal).toBeDefined(); + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it("should not include cache option when disableCache is not set", async () => { + await makeRequest(mockFetch, mockGetUrl, "GET", mockHeaders, undefined); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBeUndefined(); + }); + + it("should not include cache option when disableCache is false", async () => { + await makeRequest( + mockFetch, + mockGetUrl, + "GET", + mockHeaders, + undefined, + undefined, + undefined, + undefined, + undefined, + false, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBeUndefined(); + }); + + it("should include cache: no-store when disableCache is true and runtime supports it", async () => { + // In Node.js test environment, Request supports the cache option + expect(isCacheNoStoreSupported()).toBe(true); + await makeRequest( + mockFetch, + mockGetUrl, + "GET", + mockHeaders, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBe("no-store"); + }); + + it("should cache the result of isCacheNoStoreSupported", () => { + const first = isCacheNoStoreSupported(); + const second = isCacheNoStoreSupported(); + expect(first).toBe(second); + }); + + it("should reset cache detection state with resetCacheNoStoreSupported", () => { + // First call caches the result + const first = isCacheNoStoreSupported(); + expect(first).toBe(true); + + // Reset clears the cache + resetCacheNoStoreSupported(); + + // After reset, it should re-detect (and still return true in Node.js) + const second = isCacheNoStoreSupported(); + expect(second).toBe(true); + }); + + it("should not include cache option when runtime does not support it (e.g. Cloudflare Workers)", async () => { + // Mock Request constructor to throw when cache option is passed, + // simulating runtimes like Cloudflare Workers + const OriginalRequest = globalThis.Request; + globalThis.Request = class MockRequest { + constructor(_url: string, init?: RequestInit) { + if (init?.cache != null) { + throw new TypeError("The 'cache' field on 'RequestInitializerDict' is not implemented."); + } + } + } as unknown as typeof Request; + + try { + // Reset so the detection runs fresh with the mocked Request + resetCacheNoStoreSupported(); + expect(isCacheNoStoreSupported()).toBe(false); + + await makeRequest( + mockFetch, + mockGetUrl, + "GET", + mockHeaders, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.cache).toBeUndefined(); + } finally { + // Restore original Request + globalThis.Request = OriginalRequest; + resetCacheNoStoreSupported(); + } + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/redacting.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/redacting.test.ts new file mode 100644 index 000000000000..685f1ddafd40 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/redacting.test.ts @@ -0,0 +1,1221 @@ +import { fetcherImpl } from "../../../src/core/fetcher/Fetcher"; + +function createMockLogger() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +function mockSuccessResponse(data: unknown = { data: "test" }, status = 200, statusText = "OK") { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(data), { + status, + statusText, + }), + ); +} + +describe("Redacting Logic", () => { + describe("Header Redaction", () => { + it("should redact authorization header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { Authorization: "Bearer secret-token-12345" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + Authorization: "[REDACTED]", + }), + }), + ); + }); + + it("should redact api-key header (case-insensitive)", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "X-API-KEY": "secret-api-key" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "X-API-KEY": "[REDACTED]", + }), + }), + ); + }); + + it("should redact cookie header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { Cookie: "session=abc123; token=xyz789" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + Cookie: "[REDACTED]", + }), + }), + ); + }); + + it("should redact x-auth-token header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "x-auth-token": "auth-token-12345" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "x-auth-token": "[REDACTED]", + }), + }), + ); + }); + + it("should redact proxy-authorization header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "Proxy-Authorization": "Basic credentials" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "Proxy-Authorization": "[REDACTED]", + }), + }), + ); + }); + + it("should redact x-csrf-token header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "X-CSRF-Token": "csrf-token-abc" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "X-CSRF-Token": "[REDACTED]", + }), + }), + ); + }); + + it("should redact www-authenticate header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "WWW-Authenticate": "Bearer realm=example" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "WWW-Authenticate": "[REDACTED]", + }), + }), + ); + }); + + it("should redact x-session-token header", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { "X-Session-Token": "session-token-xyz" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "X-Session-Token": "[REDACTED]", + }), + }), + ); + }); + + it("should not redact non-sensitive headers", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { + "Content-Type": "application/json", + "User-Agent": "Test/1.0", + Accept: "application/json", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + "Content-Type": "application/json", + "User-Agent": "Test/1.0", + Accept: "application/json", + }), + }), + ); + }); + + it("should redact multiple sensitive headers at once", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + headers: { + Authorization: "Bearer token", + "X-API-Key": "api-key", + Cookie: "session=123", + "Content-Type": "application/json", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + headers: expect.toContainHeaders({ + Authorization: "[REDACTED]", + "X-API-Key": "[REDACTED]", + Cookie: "[REDACTED]", + "Content-Type": "application/json", + }), + }), + ); + }); + }); + + describe("Response Header Redaction", () => { + it("should redact Set-Cookie in response headers", async () => { + const mockLogger = createMockLogger(); + + const mockHeaders = new Headers(); + mockHeaders.set("Set-Cookie", "session=abc123; HttpOnly; Secure"); + mockHeaders.set("Content-Type", "application/json"); + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + headers: mockHeaders, + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + responseHeaders: expect.toContainHeaders({ + "set-cookie": "[REDACTED]", + "content-type": "application/json", + }), + }), + ); + }); + + it("should redact authorization in response headers", async () => { + const mockLogger = createMockLogger(); + + const mockHeaders = new Headers(); + mockHeaders.set("Authorization", "Bearer token-123"); + mockHeaders.set("Content-Type", "application/json"); + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "test" }), { + status: 200, + statusText: "OK", + headers: mockHeaders, + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "HTTP request succeeded", + expect.objectContaining({ + responseHeaders: expect.toContainHeaders({ + authorization: "[REDACTED]", + "content-type": "application/json", + }), + }), + ); + }); + + it("should redact response headers in error responses", async () => { + const mockLogger = createMockLogger(); + + const mockHeaders = new Headers(); + mockHeaders.set("WWW-Authenticate", "Bearer realm=example"); + mockHeaders.set("Content-Type", "application/json"); + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + statusText: "Unauthorized", + headers: mockHeaders, + }), + ); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "error", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "HTTP request failed with error status", + expect.objectContaining({ + responseHeaders: expect.toContainHeaders({ + "www-authenticate": "[REDACTED]", + "content-type": "application/json", + }), + }), + ); + }); + }); + + describe("Query Parameter Redaction", () => { + it("should redact api_key query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { api_key: "secret-key" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + api_key: "[REDACTED]", + }), + }), + ); + }); + + it("should redact token query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { token: "secret-token" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + token: "[REDACTED]", + }), + }), + ); + }); + + it("should redact access_token query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { access_token: "secret-access-token" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + access_token: "[REDACTED]", + }), + }), + ); + }); + + it("should redact password query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { password: "secret-password" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + password: "[REDACTED]", + }), + }), + ); + }); + + it("should redact secret query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { secret: "secret-value" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + secret: "[REDACTED]", + }), + }), + ); + }); + + it("should redact session_id query parameter", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { session_id: "session-123" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + session_id: "[REDACTED]", + }), + }), + ); + }); + + it("should not redact non-sensitive query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { + page: "1", + limit: "10", + sort: "name", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + page: "1", + limit: "10", + sort: "name", + }), + }), + ); + }); + + it("should not redact parameters containing 'auth' substring like 'author'", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { + author: "john", + authenticate: "false", + authorization_level: "user", + }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + author: "john", + authenticate: "false", + authorization_level: "user", + }), + }), + ); + }); + + it("should handle undefined query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: undefined, + }), + ); + }); + + it("should redact case-insensitive query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryParameters: { API_KEY: "secret-key", Token: "secret-token" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + queryParameters: expect.objectContaining({ + API_KEY: "[REDACTED]", + Token: "[REDACTED]", + }), + }), + ); + }); + }); + + describe("Query String Redaction", () => { + it("should redact api_key in queryString via URL", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "api_key=secret-key&page=1", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]&page=1", + }), + ); + }); + + it("should redact multiple sensitive params in queryString", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "token=t&password=p&page=1", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?token=[REDACTED]&password=[REDACTED]&page=1", + }), + ); + }); + + it("should not redact non-sensitive params in queryString", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "page=1&limit=10&sort=name", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1&limit=10&sort=name", + }), + ); + }); + + it("should prefer queryString over queryParameters when both provided", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + queryString: "page=1", + queryParameters: { api_key: "secret-key" }, + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1", + queryParameters: expect.objectContaining({ + api_key: "[REDACTED]", + }), + }), + ); + }); + }); + + describe("URL Redaction", () => { + it("should redact credentials in URL", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user:password@example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@example.com/api", + }), + ); + }); + + it("should redact api_key in query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?api_key=secret-key&page=1", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]&page=1", + }), + ); + }); + + it("should redact token in query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?token=secret-token", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?token=[REDACTED]", + }), + ); + }); + + it("should redact password in query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?username=user&password=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?username=user&password=[REDACTED]", + }), + ); + }); + + it("should not redact non-sensitive query strings", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?page=1&limit=10&sort=name", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1&limit=10&sort=name", + }), + ); + }); + + it("should not redact URL parameters containing 'auth' substring like 'author'", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?author=john&authenticate=false&page=1", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?author=john&authenticate=false&page=1", + }), + ); + }); + + it("should handle URL with fragment", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?token=secret#section", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?token=[REDACTED]#section", + }), + ); + }); + + it("should redact URL-encoded query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?api%5Fkey=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api%5Fkey=[REDACTED]", + }), + ); + }); + + it("should handle URL without query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api", + }), + ); + }); + + it("should handle empty query string", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?", + }), + ); + }); + + it("should redact multiple sensitive parameters in URL", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?api_key=secret1&token=secret2&page=1", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?api_key=[REDACTED]&token=[REDACTED]&page=1", + }), + ); + }); + + it("should redact both credentials and query parameters", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user:pass@example.com/api?token=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@example.com/api?token=[REDACTED]", + }), + ); + }); + + it("should use fast path for URLs without sensitive keywords", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?page=1&limit=10&sort=name&filter=value", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?page=1&limit=10&sort=name&filter=value", + }), + ); + }); + + it("should handle query parameter without value", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?flag&token=secret", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?flag&token=[REDACTED]", + }), + ); + }); + + it("should handle URL with multiple @ symbols in credentials", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user@example.com:pass@host.com/api", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@host.com/api", + }), + ); + }); + + it("should handle URL with @ in query parameter but not in credentials", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://example.com/api?email=user@example.com", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://example.com/api?email=user@example.com", + }), + ); + }); + + it("should handle URL with both credentials and @ in path", async () => { + const mockLogger = createMockLogger(); + mockSuccessResponse(); + + await fetcherImpl({ + url: "https://user:pass@example.com/users/@username", + method: "GET", + responseType: "json", + maxRetries: 0, + logging: { + level: "debug", + logger: mockLogger, + silent: false, + }, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "Making HTTP request", + expect.objectContaining({ + url: "https://[REDACTED]@example.com/users/@username", + }), + ); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/requestWithRetries.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/requestWithRetries.test.ts new file mode 100644 index 000000000000..7c98c0abfad1 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/requestWithRetries.test.ts @@ -0,0 +1,282 @@ +import type { Mock, MockInstance } from "vitest"; +import { requestWithRetries } from "../../../src/core/fetcher/requestWithRetries"; + +describe("requestWithRetries", () => { + let mockFetch: Mock; + let originalMathRandom: typeof Math.random; + let setTimeoutSpy: MockInstance; + + beforeEach(() => { + mockFetch = vi.fn(); + originalMathRandom = Math.random; + + Math.random = vi.fn(() => 0.5); + + vi.useFakeTimers({ + toFake: [ + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "setImmediate", + "clearImmediate", + "Date", + "performance", + "requestAnimationFrame", + "cancelAnimationFrame", + "requestIdleCallback", + "cancelIdleCallback", + ], + }); + }); + + afterEach(() => { + Math.random = originalMathRandom; + vi.clearAllMocks(); + vi.clearAllTimers(); + }); + + it("should retry on retryable status codes (legacy mode)", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const retryableStatuses = [408, 429, 500, 501, 502, 503, 504, 505]; + let callCount = 0; + + mockFetch.mockImplementation(async () => { + if (callCount < retryableStatuses.length) { + return new Response("", { status: retryableStatuses[callCount++] }); + } + return new Response("", { status: 200 }); + }); + + const responsePromise = requestWithRetries(() => mockFetch(), retryableStatuses.length); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(retryableStatuses.length + 1); + expect(response.status).toBe(200); + }); + + it("should retry on 500 Internal Server Error in legacy mode", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce(new Response("", { status: 500 })) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(2); + expect(response.status).toBe(200); + }); + + it("should respect maxRetries limit", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const maxRetries = 2; + mockFetch.mockResolvedValue(new Response("", { status: 503 })); + + const responsePromise = requestWithRetries(() => mockFetch(), maxRetries); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); + expect(response.status).toBe(503); + }); + + it("should retry on status 599 (upper boundary of retryable 5xx in legacy mode)", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce(new Response("", { status: 599 })) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(2); + expect(response.status).toBe(200); + }); + + it("should not retry on non-retryable client error (400)", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch.mockResolvedValueOnce(new Response("", { status: 400 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(response.status).toBe(400); + }); + + it("should not retry on success status codes", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const successStatuses = [200, 201, 202]; + + for (const status of successStatuses) { + mockFetch.mockReset(); + setTimeoutSpy.mockClear(); + mockFetch.mockResolvedValueOnce(new Response("", { status })); + + const responsePromise = requestWithRetries(() => mockFetch(), 3); + await vi.runAllTimersAsync(); + await responsePromise; + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(setTimeoutSpy).not.toHaveBeenCalled(); + } + }); + + interface RetryHeaderTestCase { + description: string; + headerName: string; + headerValue: string | (() => string); + expectedDelayMin: number; + expectedDelayMax: number; + } + + const retryHeaderTests: RetryHeaderTestCase[] = [ + { + description: "should respect retry-after header with seconds value", + headerName: "retry-after", + headerValue: "5", + expectedDelayMin: 4000, + expectedDelayMax: 6000, + }, + { + description: "should respect retry-after header with HTTP date value", + headerName: "retry-after", + headerValue: () => new Date(Date.now() + 3000).toUTCString(), + expectedDelayMin: 2000, + expectedDelayMax: 4000, + }, + { + description: "should respect x-ratelimit-reset header", + headerName: "x-ratelimit-reset", + headerValue: () => Math.floor((Date.now() + 4000) / 1000).toString(), + expectedDelayMin: 3000, + expectedDelayMax: 6000, + }, + ]; + + retryHeaderTests.forEach(({ description, headerName, headerValue, expectedDelayMin, expectedDelayMax }) => { + it(description, async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + const value = typeof headerValue === "function" ? headerValue() : headerValue; + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ [headerName]: value }), + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expect.any(Number)); + const actualDelay = setTimeoutSpy.mock.calls[0][1]; + expect(actualDelay).toBeGreaterThan(expectedDelayMin); + expect(actualDelay).toBeLessThan(expectedDelayMax); + expect(response.status).toBe(200); + }); + }); + + it("should apply correct exponential backoff with jitter", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch.mockResolvedValue(new Response("", { status: 502 })); + const maxRetries = 3; + const expectedDelays = [1000, 2000, 4000]; + + const responsePromise = requestWithRetries(() => mockFetch(), maxRetries); + await vi.runAllTimersAsync(); + await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledTimes(expectedDelays.length); + + expectedDelays.forEach((delay, index) => { + expect(setTimeoutSpy).toHaveBeenNthCalledWith(index + 1, expect.any(Function), delay); + }); + + expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); + }); + + it("should handle concurrent retries independently", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce(new Response("", { status: 502 })) + .mockResolvedValueOnce(new Response("", { status: 502 })) + .mockResolvedValueOnce(new Response("", { status: 200 })) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const promise1 = requestWithRetries(() => mockFetch(), 1); + const promise2 = requestWithRetries(() => mockFetch(), 1); + + await vi.runAllTimersAsync(); + const [response1, response2] = await Promise.all([promise1, promise2]); + + expect(response1.status).toBe(200); + expect(response2.status).toBe(200); + }); + + it("should cap delay at MAX_RETRY_DELAY for large header values", async () => { + setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + mockFetch + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: new Headers({ "retry-after": "120" }), // 120 seconds = 120000ms > MAX_RETRY_DELAY (60000ms) + }), + ) + .mockResolvedValueOnce(new Response("", { status: 200 })); + + const responsePromise = requestWithRetries(() => mockFetch(), 1); + await vi.runAllTimersAsync(); + const response = await responsePromise; + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60000); + expect(response.status).toBe(200); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/signals.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/signals.test.ts new file mode 100644 index 000000000000..c71761723bf6 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/signals.test.ts @@ -0,0 +1,114 @@ +import { anySignal, getTimeoutSignal } from "../../../src/core/fetcher/signals"; + +describe("Test getTimeoutSignal", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should return an object with signal and abortId", () => { + const { signal, abortId } = getTimeoutSignal(1000); + + expect(signal).toBeDefined(); + expect(abortId).toBeDefined(); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(false); + }); + + it("should create a signal that aborts after the specified timeout", () => { + const timeoutMs = 5000; + const { signal } = getTimeoutSignal(timeoutMs); + + expect(signal.aborted).toBe(false); + + vi.advanceTimersByTime(timeoutMs - 1); + expect(signal.aborted).toBe(false); + + vi.advanceTimersByTime(1); + expect(signal.aborted).toBe(true); + }); +}); + +describe("Test anySignal", () => { + it("should return an AbortSignal", () => { + const signal = anySignal(new AbortController().signal); + expect(signal).toBeInstanceOf(AbortSignal); + }); + + it("should abort when any of the input signals is aborted", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = anySignal(controller1.signal, controller2.signal); + + expect(signal.aborted).toBe(false); + controller1.abort(); + expect(signal.aborted).toBe(true); + }); + + it("should handle an array of signals", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = anySignal([controller1.signal, controller2.signal]); + + expect(signal.aborted).toBe(false); + controller2.abort(); + expect(signal.aborted).toBe(true); + }); + + it("should abort immediately if one of the input signals is already aborted", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + controller1.abort(); + + const signal = anySignal(controller1.signal, controller2.signal); + expect(signal.aborted).toBe(true); + }); + + it("should detect a signal that aborts between the initial aborted check and the event listener registration", () => { + const ctrlA = new AbortController(); + const ctrlB = new AbortController(); + + const originalAddEventListener = ctrlA.signal.addEventListener.bind(ctrlA.signal); + const originalRemoveEventListener = ctrlA.signal.removeEventListener.bind(ctrlA.signal); + + let abortedAccessCount = 0; + const proxy = new Proxy(ctrlA.signal, { + get(target, prop, receiver) { + if (prop === "aborted") { + abortedAccessCount++; + if (abortedAccessCount === 1) return false; + return Reflect.get(target, prop, receiver); + } + if (prop === "addEventListener") { + return (...args: Parameters) => { + if (abortedAccessCount >= 1 && args[0] === "abort") { + ctrlA.abort("too-late"); + } + return originalAddEventListener(...args); + }; + } + if (prop === "removeEventListener") { + return originalRemoveEventListener; + } + return Reflect.get(target, prop, receiver); + }, + }); + + const combined = anySignal(proxy, ctrlB.signal); + + expect(combined.aborted).toBe(true); + expect(combined.reason).toBe("too-late"); + }); + + it("should forward the abort reason from a source signal", () => { + const controller = new AbortController(); + const combined = anySignal(controller.signal); + + controller.abort("test-reason"); + expect(combined.aborted).toBe(true); + expect(combined.reason).toBe("test-reason"); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/test-file.txt b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/test-file.txt new file mode 100644 index 000000000000..c66d471e359c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/fetcher/test-file.txt @@ -0,0 +1 @@ +This is a test file! diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/logging/logger.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/logging/logger.test.ts new file mode 100644 index 000000000000..2e0b5fe5040c --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/logging/logger.test.ts @@ -0,0 +1,454 @@ +import { ConsoleLogger, createLogger, Logger, LogLevel } from "../../../src/core/logging/logger"; + +function createMockLogger() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +describe("Logger", () => { + describe("LogLevel", () => { + it("should have correct log levels", () => { + expect(LogLevel.Debug).toBe("debug"); + expect(LogLevel.Info).toBe("info"); + expect(LogLevel.Warn).toBe("warn"); + expect(LogLevel.Error).toBe("error"); + }); + }); + + describe("ConsoleLogger", () => { + let consoleLogger: ConsoleLogger; + let consoleSpy: { + debug: ReturnType; + info: ReturnType; + warn: ReturnType; + error: ReturnType; + }; + + beforeEach(() => { + consoleLogger = new ConsoleLogger(); + consoleSpy = { + debug: vi.spyOn(console, "debug").mockImplementation(() => {}), + info: vi.spyOn(console, "info").mockImplementation(() => {}), + warn: vi.spyOn(console, "warn").mockImplementation(() => {}), + error: vi.spyOn(console, "error").mockImplementation(() => {}), + }; + }); + + afterEach(() => { + consoleSpy.debug.mockRestore(); + consoleSpy.info.mockRestore(); + consoleSpy.warn.mockRestore(); + consoleSpy.error.mockRestore(); + }); + + it("should log debug messages", () => { + consoleLogger.debug("debug message", { data: "test" }); + expect(consoleSpy.debug).toHaveBeenCalledWith("debug message", { data: "test" }); + }); + + it("should log info messages", () => { + consoleLogger.info("info message", { data: "test" }); + expect(consoleSpy.info).toHaveBeenCalledWith("info message", { data: "test" }); + }); + + it("should log warn messages", () => { + consoleLogger.warn("warn message", { data: "test" }); + expect(consoleSpy.warn).toHaveBeenCalledWith("warn message", { data: "test" }); + }); + + it("should log error messages", () => { + consoleLogger.error("error message", { data: "test" }); + expect(consoleSpy.error).toHaveBeenCalledWith("error message", { data: "test" }); + }); + + it("should handle multiple arguments", () => { + consoleLogger.debug("message", "arg1", "arg2", { key: "value" }); + expect(consoleSpy.debug).toHaveBeenCalledWith("message", "arg1", "arg2", { key: "value" }); + }); + }); + + describe("Logger with level filtering", () => { + let mockLogger: { + debug: ReturnType; + info: ReturnType; + warn: ReturnType; + error: ReturnType; + }; + + beforeEach(() => { + mockLogger = createMockLogger(); + }); + + describe("Debug level", () => { + it("should log all levels when set to debug", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).toHaveBeenCalledWith("debug"); + expect(mockLogger.info).toHaveBeenCalledWith("info"); + expect(mockLogger.warn).toHaveBeenCalledWith("warn"); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(true); + expect(logger.isInfo()).toBe(true); + expect(logger.isWarn()).toBe(true); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Info level", () => { + it("should log info, warn, and error when set to info", () => { + const logger = new Logger({ + level: LogLevel.Info, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith("info"); + expect(mockLogger.warn).toHaveBeenCalledWith("warn"); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Info, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(true); + expect(logger.isWarn()).toBe(true); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Warn level", () => { + it("should log warn and error when set to warn", () => { + const logger = new Logger({ + level: LogLevel.Warn, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).toHaveBeenCalledWith("warn"); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Warn, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(false); + expect(logger.isWarn()).toBe(true); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Error level", () => { + it("should only log error when set to error", () => { + const logger = new Logger({ + level: LogLevel.Error, + logger: mockLogger, + silent: false, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalledWith("error"); + }); + + it("should report correct level checks", () => { + const logger = new Logger({ + level: LogLevel.Error, + logger: mockLogger, + silent: false, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(false); + expect(logger.isWarn()).toBe(false); + expect(logger.isError()).toBe(true); + }); + }); + + describe("Silent mode", () => { + it("should not log anything when silent is true", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: true, + }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it("should report all level checks as false when silent", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: true, + }); + + expect(logger.isDebug()).toBe(false); + expect(logger.isInfo()).toBe(false); + expect(logger.isWarn()).toBe(false); + expect(logger.isError()).toBe(false); + }); + }); + + describe("shouldLog", () => { + it("should correctly determine if level should be logged", () => { + const logger = new Logger({ + level: LogLevel.Info, + logger: mockLogger, + silent: false, + }); + + expect(logger.shouldLog(LogLevel.Debug)).toBe(false); + expect(logger.shouldLog(LogLevel.Info)).toBe(true); + expect(logger.shouldLog(LogLevel.Warn)).toBe(true); + expect(logger.shouldLog(LogLevel.Error)).toBe(true); + }); + + it("should return false for all levels when silent", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: true, + }); + + expect(logger.shouldLog(LogLevel.Debug)).toBe(false); + expect(logger.shouldLog(LogLevel.Info)).toBe(false); + expect(logger.shouldLog(LogLevel.Warn)).toBe(false); + expect(logger.shouldLog(LogLevel.Error)).toBe(false); + }); + }); + + describe("Multiple arguments", () => { + it("should pass multiple arguments to logger", () => { + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("message", "arg1", { key: "value" }, 123); + expect(mockLogger.debug).toHaveBeenCalledWith("message", "arg1", { key: "value" }, 123); + }); + }); + }); + + describe("createLogger", () => { + it("should return default logger when no config provided", () => { + const logger = createLogger(); + expect(logger).toBeInstanceOf(Logger); + }); + + it("should return same logger instance when Logger is passed", () => { + const customLogger = new Logger({ + level: LogLevel.Debug, + logger: new ConsoleLogger(), + silent: false, + }); + + const result = createLogger(customLogger); + expect(result).toBe(customLogger); + }); + + it("should create logger with custom config", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + level: LogLevel.Warn, + logger: mockLogger, + silent: false, + }); + + expect(logger).toBeInstanceOf(Logger); + logger.warn("test"); + expect(mockLogger.warn).toHaveBeenCalledWith("test"); + }); + + it("should use default values for missing config", () => { + const logger = createLogger({}); + expect(logger).toBeInstanceOf(Logger); + }); + + it("should override default level", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("test"); + expect(mockLogger.debug).toHaveBeenCalledWith("test"); + }); + + it("should override default silent mode", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + logger: mockLogger, + silent: false, + }); + + logger.info("test"); + expect(mockLogger.info).toHaveBeenCalledWith("test"); + }); + + it("should use provided logger implementation", () => { + const customLogger = createMockLogger(); + + const logger = createLogger({ + logger: customLogger, + level: LogLevel.Debug, + silent: false, + }); + + logger.debug("test"); + expect(customLogger.debug).toHaveBeenCalledWith("test"); + }); + + it("should default to silent: true", () => { + const mockLogger = createMockLogger(); + + const logger = createLogger({ + logger: mockLogger, + level: LogLevel.Debug, + }); + + logger.debug("test"); + expect(mockLogger.debug).not.toHaveBeenCalled(); + }); + }); + + describe("Default logger", () => { + it("should have silent: true by default", () => { + const logger = createLogger(); + expect(logger.shouldLog(LogLevel.Info)).toBe(false); + }); + + it("should not log when using default logger", () => { + const logger = createLogger(); + + logger.info("test"); + expect(logger.isInfo()).toBe(false); + }); + }); + + describe("Edge cases", () => { + it("should handle empty message", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug(""); + expect(mockLogger.debug).toHaveBeenCalledWith(""); + }); + + it("should handle no arguments", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + logger.debug("message"); + expect(mockLogger.debug).toHaveBeenCalledWith("message"); + }); + + it("should handle complex objects", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Debug, + logger: mockLogger, + silent: false, + }); + + const complexObject = { + nested: { key: "value" }, + array: [1, 2, 3], + fn: () => "test", + }; + + logger.debug("message", complexObject); + expect(mockLogger.debug).toHaveBeenCalledWith("message", complexObject); + }); + + it("should handle errors as arguments", () => { + const mockLogger = createMockLogger(); + + const logger = new Logger({ + level: LogLevel.Error, + logger: mockLogger, + silent: false, + }); + + const error = new Error("Test error"); + logger.error("Error occurred", error); + expect(mockLogger.error).toHaveBeenCalledWith("Error occurred", error); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/QueryStringBuilder.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/QueryStringBuilder.test.ts new file mode 100644 index 000000000000..1afa2d22248f --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/QueryStringBuilder.test.ts @@ -0,0 +1,236 @@ +import { queryBuilder } from "../../../src/core/url/QueryStringBuilder"; + +describe("QueryStringBuilder", () => { + describe("add() — default repeat format", () => { + it("adds a scalar string value", () => { + const qs = queryBuilder().add("key", "value").build(); + expect(qs).toBe("key=value"); + }); + + it("adds a scalar number value", () => { + const qs = queryBuilder().add("limit", 10).build(); + expect(qs).toBe("limit=10"); + }); + + it("adds a boolean value", () => { + const qs = queryBuilder().add("active", true).build(); + expect(qs).toBe("active=true"); + }); + + it("skips undefined values", () => { + const qs = queryBuilder().add("key", undefined).build(); + expect(qs).toBe(""); + }); + + it("skips null values", () => { + const qs = queryBuilder().add("key", null).build(); + expect(qs).toBe(""); + }); + + it("repeats array elements as separate key=value pairs", () => { + const qs = queryBuilder().add("color", ["red", "blue", "green"]).build(); + expect(qs).toBe("color=red&color=blue&color=green"); + }); + + it("skips undefined items within arrays", () => { + const qs = queryBuilder().add("color", ["red", undefined, "blue"]).build(); + expect(qs).toBe("color=red&color=blue"); + }); + + it("returns empty string for empty array", () => { + const qs = queryBuilder().add("color", []).build(); + expect(qs).toBe(""); + }); + + it("encodes special characters in keys and values", () => { + const qs = queryBuilder().add("my key", "hello world").build(); + expect(qs).toBe("my%20key=hello%20world"); + }); + + it("handles nested objects", () => { + const qs = queryBuilder().add("filter", { status: "active" }).build(); + expect(qs).toBe("filter%5Bstatus%5D=active"); + }); + }); + + describe("add() — comma style", () => { + it("joins array values with literal commas", () => { + const qs = queryBuilder().add("tags", ["a", "b", "c"], { style: "comma" }).build(); + expect(qs).toBe("tags=a,b,c"); + }); + + it("handles single-element array", () => { + const qs = queryBuilder().add("tags", ["only"], { style: "comma" }).build(); + expect(qs).toBe("tags=only"); + }); + + it("returns empty string for empty array", () => { + const qs = queryBuilder().add("tags", [], { style: "comma" }).build(); + expect(qs).toBe(""); + }); + + it("skips undefined values", () => { + const qs = queryBuilder().add("tags", undefined, { style: "comma" }).build(); + expect(qs).toBe(""); + }); + + it("skips null values", () => { + const qs = queryBuilder().add("tags", null, { style: "comma" }).build(); + expect(qs).toBe(""); + }); + + it("treats scalar values same as default add()", () => { + const qs = queryBuilder().add("tag", "single", { style: "comma" }).build(); + expect(qs).toBe("tag=single"); + }); + + it("encodes commas within individual values as %2C", () => { + const qs = queryBuilder().add("items", ["a,b", "c"], { style: "comma" }).build(); + expect(qs).toBe("items=a%2Cb,c"); + }); + + it("encodes special characters in values", () => { + const qs = queryBuilder().add("tags", ["hello world", "foo&bar"], { style: "comma" }).build(); + expect(qs).toBe("tags=hello%20world,foo%26bar"); + }); + }); + + describe("chaining", () => { + it("chains multiple add() calls", () => { + const qs = queryBuilder().add("limit", 10).add("offset", 20).build(); + expect(qs).toBe("limit=10&offset=20"); + }); + + it("chains add() with default and comma styles", () => { + const qs = queryBuilder() + .add("limit", 10) + .add("tags", ["ACCESS_GRANTED", "COPY", "DELETE"], { style: "comma" }) + .add("active", true) + .build(); + expect(qs).toBe("limit=10&tags=ACCESS_GRANTED,COPY,DELETE&active=true"); + }); + + it("skips undefined/null params in chain without breaking", () => { + const qs = queryBuilder() + .add("a", "1") + .add("b", undefined) + .add("c", null, { style: "comma" }) + .add("d", "4") + .build(); + expect(qs).toBe("a=1&d=4"); + }); + }); + + describe("addMany()", () => { + it("adds all params from a record", () => { + const qs = queryBuilder().addMany({ limit: 10, offset: 20, name: "test" }).build(); + expect(qs).toBe("limit=10&offset=20&name=test"); + }); + + it("skips null and undefined values", () => { + const qs = queryBuilder().addMany({ a: "1", b: null, c: undefined, d: "4" }).build(); + expect(qs).toBe("a=1&d=4"); + }); + + it("handles empty record", () => { + const qs = queryBuilder().addMany({}).build(); + expect(qs).toBe(""); + }); + + it("works with comma-style override after addMany", () => { + const params = { limit: 10, tags: ["a", "b"], active: true }; + const qs = queryBuilder().addMany(params).add("tags", params.tags, { style: "comma" }).build(); + expect(qs).toBe("limit=10&tags=a,b&active=true"); + }); + + it("handles array values with default repeat format", () => { + const qs = queryBuilder() + .addMany({ ids: [1, 2, 3] }) + .build(); + expect(qs).toBe("ids=1&ids=2&ids=3"); + }); + }); + + describe("mergeAdditional()", () => { + it("appends additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ extra: "value" }).build(); + expect(qs).toBe("limit=10&extra=value"); + }); + + it("overrides existing keys (last-write-wins)", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ limit: 20 }).build(); + expect(qs).toBe("limit=20"); + }); + + it("handles undefined additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional(undefined).build(); + expect(qs).toBe("limit=10"); + }); + + it("skips undefined values in additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ extra: undefined }).build(); + expect(qs).toBe("limit=10"); + }); + + it("skips null values in additional params", () => { + const qs = queryBuilder().add("limit", 10).mergeAdditional({ extra: null }).build(); + expect(qs).toBe("limit=10"); + }); + + it("handles array values in additional params using repeat format", () => { + const qs = queryBuilder() + .mergeAdditional({ ids: [1, 2, 3] }) + .build(); + expect(qs).toBe("ids=1&ids=2&ids=3"); + }); + + it("overrides a comma-style param with repeat format", () => { + const qs = queryBuilder() + .add("tags", ["a", "b"], { style: "comma" }) + .mergeAdditional({ tags: ["x", "y"] }) + .build(); + expect(qs).toBe("tags=x&tags=y"); + }); + }); + + describe("build()", () => { + it("returns empty string when no params added", () => { + const qs = queryBuilder().build(); + expect(qs).toBe(""); + }); + + it("does not include leading ?", () => { + const qs = queryBuilder().add("key", "value").build(); + expect(qs).not.toContain("?"); + }); + }); + + describe("end-to-end scenarios", () => { + it("matches expected query-parameters-openapi output pattern", () => { + const params: Record = { + limit: 1, + id: "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + date: "2023-01-15", + deadline: "2024-01-15T09:30:00.000Z", + bytes: "SGVsbG8gd29ybGQh", + user: "user", + userList: ["user"], + optionalString: "optionalString", + nestedUser: "nestedUser", + excludeUser: "excludeUser", + filter: "filter", + tags: ["tags"], + optionalTags: undefined, + }; + const qs = queryBuilder() + .addMany(params) + .add("tags", params.tags, { style: "comma" }) + .add("optionalTags", params.optionalTags, { style: "comma" }) + .mergeAdditional(undefined) + .build(); + expect(qs).toContain("limit=1"); + expect(qs).toContain("tags=tags"); + expect(qs).not.toContain("optionalTags"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/join.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/join.test.ts new file mode 100644 index 000000000000..123488f084ea --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/join.test.ts @@ -0,0 +1,284 @@ +import { join } from "../../../src/core/url/index"; + +describe("join", () => { + interface TestCase { + description: string; + base: string; + segments: string[]; + expected: string; + } + + describe("basic functionality", () => { + const basicTests: TestCase[] = [ + { description: "should return empty string for empty base", base: "", segments: [], expected: "" }, + { + description: "should return empty string for empty base with path", + base: "", + segments: ["path"], + expected: "", + }, + { + description: "should handle single segment", + base: "base", + segments: ["segment"], + expected: "base/segment", + }, + { + description: "should handle single segment with trailing slash on base", + base: "base/", + segments: ["segment"], + expected: "base/segment", + }, + { + description: "should handle single segment with leading slash", + base: "base", + segments: ["/segment"], + expected: "base/segment", + }, + { + description: "should handle single segment with both slashes", + base: "base/", + segments: ["/segment"], + expected: "base/segment", + }, + { + description: "should handle multiple segments", + base: "base", + segments: ["path1", "path2", "path3"], + expected: "base/path1/path2/path3", + }, + { + description: "should handle multiple segments with slashes", + base: "base/", + segments: ["/path1/", "/path2/", "/path3/"], + expected: "base/path1/path2/path3/", + }, + ]; + + basicTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("URL handling", () => { + const urlTests: TestCase[] = [ + { + description: "should handle absolute URLs", + base: "https://example.com", + segments: ["api", "v1"], + expected: "https://example.com/api/v1", + }, + { + description: "should handle absolute URLs with slashes", + base: "https://example.com/", + segments: ["/api/", "/v1/"], + expected: "https://example.com/api/v1/", + }, + { + description: "should handle absolute URLs with base path", + base: "https://example.com/base", + segments: ["api", "v1"], + expected: "https://example.com/base/api/v1", + }, + { + description: "should preserve URL query parameters", + base: "https://example.com?query=1", + segments: ["api"], + expected: "https://example.com/api?query=1", + }, + { + description: "should preserve URL fragments", + base: "https://example.com#fragment", + segments: ["api"], + expected: "https://example.com/api#fragment", + }, + { + description: "should preserve URL query and fragments", + base: "https://example.com?query=1#fragment", + segments: ["api"], + expected: "https://example.com/api?query=1#fragment", + }, + { + description: "should handle http protocol", + base: "http://example.com", + segments: ["api"], + expected: "http://example.com/api", + }, + { + description: "should handle ftp protocol", + base: "ftp://example.com", + segments: ["files"], + expected: "ftp://example.com/files", + }, + { + description: "should handle ws protocol", + base: "ws://example.com", + segments: ["socket"], + expected: "ws://example.com/socket", + }, + { + description: "should fallback to path joining for malformed URLs", + base: "not-a-url://", + segments: ["path"], + expected: "not-a-url:///path", + }, + ]; + + urlTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("edge cases", () => { + const edgeCaseTests: TestCase[] = [ + { + description: "should handle empty segments", + base: "base", + segments: ["", "path"], + expected: "base/path", + }, + { + description: "should handle null segments", + base: "base", + segments: [null as any, "path"], + expected: "base/path", + }, + { + description: "should handle undefined segments", + base: "base", + segments: [undefined as any, "path"], + expected: "base/path", + }, + { + description: "should handle segments with only single slash", + base: "base", + segments: ["/", "path"], + expected: "base/path", + }, + { + description: "should handle segments with only double slash", + base: "base", + segments: ["//", "path"], + expected: "base/path", + }, + { + description: "should handle base paths with trailing slashes", + base: "base/", + segments: ["path"], + expected: "base/path", + }, + { + description: "should handle complex nested paths", + base: "api/v1/", + segments: ["/users/", "/123/", "/profile"], + expected: "api/v1/users/123/profile", + }, + ]; + + edgeCaseTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("real-world scenarios", () => { + const realWorldTests: TestCase[] = [ + { + description: "should handle API endpoint construction", + base: "https://api.example.com/v1", + segments: ["users", "123", "posts"], + expected: "https://api.example.com/v1/users/123/posts", + }, + { + description: "should handle file path construction", + base: "/var/www", + segments: ["html", "assets", "images"], + expected: "/var/www/html/assets/images", + }, + { + description: "should handle relative path construction", + base: "../parent", + segments: ["child", "grandchild"], + expected: "../parent/child/grandchild", + }, + { + description: "should handle Windows-style paths", + base: "C:\\Users", + segments: ["Documents", "file.txt"], + expected: "C:\\Users/Documents/file.txt", + }, + ]; + + realWorldTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); + + describe("performance scenarios", () => { + it("should handle many segments efficiently", () => { + const segments = Array(100).fill("segment"); + const result = join("base", ...segments); + expect(result).toBe(`base/${segments.join("/")}`); + }); + + it("should handle long URLs", () => { + const longPath = "a".repeat(1000); + expect(join("https://example.com", longPath)).toBe(`https://example.com/${longPath}`); + }); + }); + + describe("trailing slash preservation", () => { + const trailingSlashTests: TestCase[] = [ + { + description: + "should preserve trailing slash on final result when base has trailing slash and no segments", + base: "https://api.example.com/", + segments: [], + expected: "https://api.example.com/", + }, + { + description: "should preserve trailing slash on v1 path", + base: "https://api.example.com/v1/", + segments: [], + expected: "https://api.example.com/v1/", + }, + { + description: "should preserve trailing slash when last segment has trailing slash", + base: "https://api.example.com", + segments: ["users/"], + expected: "https://api.example.com/users/", + }, + { + description: "should preserve trailing slash with relative path", + base: "api/v1", + segments: ["users/"], + expected: "api/v1/users/", + }, + { + description: "should preserve trailing slash with multiple segments", + base: "https://api.example.com", + segments: ["v1", "collections/"], + expected: "https://api.example.com/v1/collections/", + }, + { + description: "should preserve trailing slash with base path", + base: "base", + segments: ["path1", "path2/"], + expected: "base/path1/path2/", + }, + ]; + + trailingSlashTests.forEach(({ description, base, segments, expected }) => { + it(description, () => { + expect(join(base, ...segments)).toBe(expected); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/qs.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/qs.test.ts new file mode 100644 index 000000000000..54d2d9ce3f86 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/unit/url/qs.test.ts @@ -0,0 +1,374 @@ +import { toQueryString } from "../../../src/core/url/index"; + +describe("Test qs toQueryString", () => { + interface BasicTestCase { + description: string; + input: any; + expected: string; + } + + describe("Basic functionality", () => { + const basicTests: BasicTestCase[] = [ + { description: "should return empty string for null", input: null, expected: "" }, + { description: "should return empty string for undefined", input: undefined, expected: "" }, + { description: "should return empty string for string primitive", input: "hello", expected: "" }, + { description: "should return empty string for number primitive", input: 42, expected: "" }, + { description: "should return empty string for true boolean", input: true, expected: "" }, + { description: "should return empty string for false boolean", input: false, expected: "" }, + { description: "should handle empty objects", input: {}, expected: "" }, + { + description: "should handle simple key-value pairs", + input: { name: "John", age: 30 }, + expected: "name=John&age=30", + }, + ]; + + basicTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); + }); + }); + + describe("Array handling", () => { + interface ArrayTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" | "comma" }; + expected: string; + } + + const arrayTests: ArrayTestCase[] = [ + { + description: "should handle arrays with indices format (default)", + input: { items: ["a", "b", "c"] }, + expected: "items%5B0%5D=a&items%5B1%5D=b&items%5B2%5D=c", + }, + { + description: "should handle arrays with repeat format", + input: { items: ["a", "b", "c"] }, + options: { arrayFormat: "repeat" }, + expected: "items=a&items=b&items=c", + }, + { + description: "should handle empty arrays", + input: { items: [] }, + expected: "", + }, + { + description: "should handle arrays with mixed types", + input: { mixed: ["string", 42, true, false] }, + expected: "mixed%5B0%5D=string&mixed%5B1%5D=42&mixed%5B2%5D=true&mixed%5B3%5D=false", + }, + { + description: "should handle arrays with objects", + input: { users: [{ name: "John" }, { name: "Jane" }] }, + expected: "users%5B0%5D%5Bname%5D=John&users%5B1%5D%5Bname%5D=Jane", + }, + { + description: "should handle arrays with objects in repeat format", + input: { users: [{ name: "John" }, { name: "Jane" }] }, + options: { arrayFormat: "repeat" }, + expected: "users%5Bname%5D=John&users%5Bname%5D=Jane", + }, + ]; + + arrayTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Nested objects", () => { + const nestedTests: BasicTestCase[] = [ + { + description: "should handle nested objects", + input: { user: { name: "John", age: 30 } }, + expected: "user%5Bname%5D=John&user%5Bage%5D=30", + }, + { + description: "should handle deeply nested objects", + input: { user: { profile: { name: "John", settings: { theme: "dark" } } } }, + expected: "user%5Bprofile%5D%5Bname%5D=John&user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark", + }, + { + description: "should handle empty nested objects", + input: { user: {} }, + expected: "", + }, + ]; + + nestedTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); + }); + }); + + describe("Encoding", () => { + interface EncodingTestCase { + description: string; + input: any; + options?: { encode?: boolean }; + expected: string; + } + + const encodingTests: EncodingTestCase[] = [ + { + description: "should encode by default", + input: { name: "John Doe", email: "john@example.com" }, + expected: "name=John%20Doe&email=john%40example.com", + }, + { + description: "should not encode when encode is false", + input: { name: "John Doe", email: "john@example.com" }, + options: { encode: false }, + expected: "name=John Doe&email=john@example.com", + }, + { + description: "should encode special characters in keys", + input: { "user name": "John", "email[primary]": "john@example.com" }, + expected: "user%20name=John&email%5Bprimary%5D=john%40example.com", + }, + { + description: "should not encode special characters in keys when encode is false", + input: { "user name": "John", "email[primary]": "john@example.com" }, + options: { encode: false }, + expected: "user name=John&email[primary]=john@example.com", + }, + ]; + + encodingTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Mixed scenarios", () => { + interface MixedTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" | "comma" }; + expected: string; + } + + const mixedTests: MixedTestCase[] = [ + { + description: "should handle complex nested structures", + input: { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], + }, + }, + sort: { field: "name", direction: "asc" }, + }, + expected: + "filters%5Bstatus%5D%5B0%5D=active&filters%5Bstatus%5D%5B1%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D%5B0%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D%5B1%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + }, + { + description: "should handle complex nested structures with repeat format", + input: { + filters: { + status: ["active", "pending"], + category: { + type: "electronics", + subcategories: ["phones", "laptops"], + }, + }, + sort: { field: "name", direction: "asc" }, + }, + options: { arrayFormat: "repeat" }, + expected: + "filters%5Bstatus%5D=active&filters%5Bstatus%5D=pending&filters%5Bcategory%5D%5Btype%5D=electronics&filters%5Bcategory%5D%5Bsubcategories%5D=phones&filters%5Bcategory%5D%5Bsubcategories%5D=laptops&sort%5Bfield%5D=name&sort%5Bdirection%5D=asc", + }, + { + description: "should handle arrays with null/undefined values", + input: { items: ["a", null, "c", undefined, "e"] }, + expected: "items%5B0%5D=a&items%5B2%5D=c&items%5B4%5D=e", + }, + { + description: "should handle objects with null/undefined values", + input: { name: "John", age: null, email: undefined, active: true }, + expected: "name=John&active=true", + }, + ]; + + mixedTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Edge cases", () => { + const edgeCaseTests: BasicTestCase[] = [ + { + description: "should handle numeric keys", + input: { "0": "zero", "1": "one" }, + expected: "0=zero&1=one", + }, + { + description: "should handle boolean values in objects", + input: { enabled: true, disabled: false }, + expected: "enabled=true&disabled=false", + }, + { + description: "should handle empty strings", + input: { name: "", description: "test" }, + expected: "name=&description=test", + }, + { + description: "should handle zero values", + input: { count: 0, price: 0.0 }, + expected: "count=0&price=0", + }, + { + description: "should handle arrays with empty strings", + input: { items: ["a", "", "c"] }, + expected: "items%5B0%5D=a&items%5B1%5D=&items%5B2%5D=c", + }, + ]; + + edgeCaseTests.forEach(({ description, input, expected }) => { + it(description, () => { + expect(toQueryString(input)).toBe(expected); + }); + }); + }); + + describe("Comma array format", () => { + interface CommaTestCase { + description: string; + input: any; + options?: { arrayFormat?: "comma"; encode?: boolean }; + expected: string; + } + + const commaTests: CommaTestCase[] = [ + { + description: "should join array values with commas", + input: { event_type: ["ACCESS_GRANTED", "COPY", "DELETE"] }, + options: { arrayFormat: "comma" }, + expected: "event_type=ACCESS_GRANTED,COPY,DELETE", + }, + { + description: "should handle single-element array", + input: { event_type: ["ACCESS_GRANTED"] }, + options: { arrayFormat: "comma" }, + expected: "event_type=ACCESS_GRANTED", + }, + { + description: "should handle empty array", + input: { event_type: [] }, + options: { arrayFormat: "comma" }, + expected: "", + }, + { + description: "should not percent-encode commas", + input: { items: ["a", "b", "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a,b,c", + }, + { + description: "should encode values but not commas", + input: { items: ["a b", "c d"] }, + options: { arrayFormat: "comma" }, + expected: "items=a%20b,c%20d", + }, + { + description: "should not encode when encode is false", + input: { items: ["a b", "c d"] }, + options: { arrayFormat: "comma", encode: false }, + expected: "items=a b,c d", + }, + { + description: "should handle mixed parameters with comma and non-array values", + input: { event_type: ["ACCESS_GRANTED", "COPY", "DELETE"], limit: 10, offset: 0 }, + options: { arrayFormat: "comma" }, + expected: "event_type=ACCESS_GRANTED,COPY,DELETE&limit=10&offset=0", + }, + { + description: "should handle numeric array values", + input: { ids: [1, 2, 3] }, + options: { arrayFormat: "comma" }, + expected: "ids=1,2,3", + }, + { + description: "should handle boolean array values", + input: { flags: [true, false, true] }, + options: { arrayFormat: "comma" }, + expected: "flags=true,false,true", + }, + { + description: "should skip null values in comma format", + input: { items: ["a", null, "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a,c", + }, + { + description: "should skip undefined values in comma format", + input: { items: ["a", undefined, "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a,c", + }, + { + description: "should produce empty string for all-null array in comma format", + input: { items: [null, undefined] }, + options: { arrayFormat: "comma" }, + expected: "", + }, + { + description: "should encode commas within values while keeping separator commas literal", + input: { items: ["a,b", "c"] }, + options: { arrayFormat: "comma" }, + expected: "items=a%2Cb,c", + }, + ]; + + commaTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); + + describe("Options combinations", () => { + interface OptionsTestCase { + description: string; + input: any; + options?: { arrayFormat?: "repeat" | "indices" | "comma"; encode?: boolean }; + expected: string; + } + + const optionsTests: OptionsTestCase[] = [ + { + description: "should respect both arrayFormat and encode options", + input: { items: ["a & b", "c & d"] }, + options: { arrayFormat: "repeat", encode: false }, + expected: "items=a & b&items=c & d", + }, + { + description: "should use default options when none provided", + input: { items: ["a", "b"] }, + expected: "items%5B0%5D=a&items%5B1%5D=b", + }, + { + description: "should merge provided options with defaults", + input: { items: ["a", "b"], name: "John Doe" }, + options: { encode: false }, + expected: "items[0]=a&items[1]=b&name=John Doe", + }, + ]; + + optionsTests.forEach(({ description, input, options, expected }) => { + it(description, () => { + expect(toQueryString(input, options)).toBe(expected); + }); + }); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/.gitkeep b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts new file mode 100644 index 000000000000..93b3c6e5e544 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts @@ -0,0 +1,53 @@ +// This file was auto-generated by Fern from our API Definition. + +import { SeedTsFlattenRequestAnyAuthClient } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; + +describe("UsersClient", () => { + test("updateUser", async () => { + const server = mockServerPool.createServer(); + const client = new SeedTsFlattenRequestAnyAuthClient({ + maxRetries: 0, + bearerAuth: { token: "test" }, + apiKey: { apiKey: "test" }, + environment: server.baseUrl, + }); + const rawRequestBody = { id: "body-id", name: "Ada" }; + + server.mockEndpoint().put("/users/path-id").jsonBody(rawRequestBody).respondWith().statusCode(200).build(); + + const response = await client.users.updateUser({ + id: "path-id", + body: { + id: "body-id", + name: "Ada", + }, + }); + expect(response).toEqual(undefined); + }); + + test("updateUserProfile", async () => { + const server = mockServerPool.createServer(); + const client = new SeedTsFlattenRequestAnyAuthClient({ + maxRetries: 0, + bearerAuth: { token: "test" }, + apiKey: { apiKey: "test" }, + environment: server.baseUrl, + }); + const rawRequestBody = { id: "body-id", name: "Ada" }; + + server + .mockEndpoint() + .put("/users/path-id/profile") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .build(); + + const response = await client.users.updateUserProfile("path-id", { + id: "body-id", + name: "Ada", + }); + expect(response).toEqual(undefined); + }); +}); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.base.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.base.json new file mode 100644 index 000000000000..93a92c0630b5 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "extendedDiagnostics": true, + "strict": true, + "target": "ES6", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "isolatedModules": true, + "isolatedDeclarations": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.cjs.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.cjs.json new file mode 100644 index 000000000000..5c11446f5984 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist/cjs" + }, + "include": ["src"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.esm.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.esm.json new file mode 100644 index 000000000000..021e74d21dd3 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "dist/esm", + "verbatimModuleSyntax": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.json new file mode 100644 index 000000000000..d77fdf00d259 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.cjs.json" +} diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/vitest.config.mts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/vitest.config.mts new file mode 100644 index 000000000000..0dee5a752d39 --- /dev/null +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/vitest.config.mts @@ -0,0 +1,32 @@ +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { + typecheck: { + enabled: true, + tsconfig: "./tests/tsconfig.json", + }, + projects: [ + { + test: { + globals: true, + name: "unit", + environment: "node", + root: "./tests", + include: ["**/*.test.{js,ts,jsx,tsx}"], + exclude: ["wire/**"], + setupFiles: ["./setup.ts"], + }, + }, + { + test: { + globals: true, + name: "wire", + environment: "node", + root: "./tests/wire", + setupFiles: ["../setup.ts", "../mock-server/setup.ts"], + }, + }, + ], + passWithNoTests: true, + }, +}); From 8214279594e42e4bfae4b56777d753cb2e740a11 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 20:52:46 +0000 Subject: [PATCH 07/16] test(typescript): cover real fixture dynamic snippets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/__test__/AuthWrapperProperty.test.ts | 35 ++++++++ .../__test__/FlattenRequestParameters.test.ts | 83 +++++++++++++++++++ .../authWrapperPropertyDynamic.test.ts | 35 ++++++-- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts index 87daf6500c29..308b471014f9 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts @@ -8,6 +8,9 @@ const DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY = AbsoluteFilePath.of( `${__dirname}/../../../../../packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions` ); const IR_FILEPATH = AbsoluteFilePath.of(join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "exhaustive.json")); +const REAL_FIXTURE_IR_FILEPATH = AbsoluteFilePath.of( + join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "ts-flatten-request-any-auth.json") +); const REQUEST: FernIr.dynamic.EndpointSnippetRequest = { endpoint: { @@ -105,4 +108,36 @@ describe("auth wrapperProperty", () => { expect(response.snippet).toContain("token:"); expect(response.snippet).not.toContain("bearerAuth: {"); }); + + it("nests auth constructor options for a real multi-auth fixture", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: REAL_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({}) + }); + + const response = await generator.generate({ + endpoint: { + method: "PUT", + path: "/users/{id}" + }, + baseURL: undefined, + environment: undefined, + auth: { + type: "bearer", + token: "" + }, + pathParameters: { + id: "path-id" + }, + queryParameters: undefined, + headers: undefined, + requestBody: { + id: "body-id", + name: "Ada" + } + }); + + expect(response.snippet).toContain("bearerAuth: {"); + expect(response.snippet).toContain('token: ""'); + }); }); diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts index 136d928855c3..8590fa6ac023 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/FlattenRequestParameters.test.ts @@ -8,6 +8,9 @@ const DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY = AbsoluteFilePath.of( `${__dirname}/../../../../../packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions` ); const IR_FILEPATH = AbsoluteFilePath.of(join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "exhaustive.json")); +const REAL_FIXTURE_IR_FILEPATH = AbsoluteFilePath.of( + join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "ts-flatten-request-any-auth.json") +); const STRING_NAME: FernIr.dynamic.Name = { originalName: "string", @@ -59,6 +62,28 @@ const REQUEST: FernIr.dynamic.EndpointSnippetRequest = { } }; +const REAL_FIXTURE_REQUEST: FernIr.dynamic.EndpointSnippetRequest = { + endpoint: { + method: "PUT", + path: "/users/{id}" + }, + baseURL: undefined, + environment: undefined, + auth: { + type: "bearer", + token: "" + }, + pathParameters: { + id: "path-id" + }, + queryParameters: undefined, + headers: undefined, + requestBody: { + id: "body-id", + name: "Ada" + } +}; + describe("flattenRequestParameters", () => { it("flattens referenced object request bodies when enabled", async () => { const generator = buildDynamicSnippetsGenerator({ @@ -149,4 +174,62 @@ describe("flattenRequestParameters", () => { expect(response.snippet.match(/\bstring:/g)?.length).toBe(1); expect(response.snippet).toContain('string: "body"'); }); + + it("flattens a real referenced object body and drops the colliding request path parameter", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: REAL_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + flattenRequestParameters: true + } + }) + }); + + const response = await generator.generate(REAL_FIXTURE_REQUEST); + + expect(response.snippet).toContain('id: "body-id"'); + expect(response.snippet).toContain('name: "Ada"'); + expect(response.snippet).not.toContain("body:"); + expect(response.snippet).not.toContain('"path-id"'); + expect(response.snippet.match(/\bid:/g)?.length).toBe(1); + }); + + it("preserves the body and request path parameter when flattening is disabled", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: REAL_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + flattenRequestParameters: false + } + }) + }); + + const response = await generator.generate(REAL_FIXTURE_REQUEST); + + expect(response.snippet).toContain('id: "path-id"'); + expect(response.snippet).toContain("body: {"); + }); + + it("emits endpoint-level path parameters positionally for a real referenced body", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: REAL_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + flattenRequestParameters: true + } + }) + }); + + const response = await generator.generate({ + ...REAL_FIXTURE_REQUEST, + endpoint: { + method: "PUT", + path: "/users/{id}/profile" + } + }); + + expect(response.snippet).toContain('updateUserProfile("path-id", {'); + expect(response.snippet).toContain('id: "body-id"'); + expect(response.snippet).toContain('name: "Ada"'); + }); }); diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts index 75aabf4274ba..0a83b24d39f5 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/authWrapperPropertyDynamic.test.ts @@ -19,16 +19,41 @@ describe("dynamic auth wrapperProperty", () => { expect(endpoint?.auth?.wrapperProperty).toBeUndefined(); }); - it("sets wrapperProperty to the camelCase auth scheme key for ANY auth", async () => { + it.each([ + ["any-auth", "bearer", "Bearer"], + ["endpoint-security-auth", "bearer", "Bearer"] + ])("sets wrapperProperty for %s auth", async (fixtureName, expectedSafeName, expectedOriginalName) => { const ir = await generateIRFromPath({ - absolutePathToWorkspace: AbsoluteFilePath.of(path.join(TEST_DEFINITIONS_DIR, "fern/apis/any-auth")), - workspaceName: "dynamicAuthWrapperPropertyAny", + absolutePathToWorkspace: AbsoluteFilePath.of(path.join(TEST_DEFINITIONS_DIR, "fern/apis", fixtureName)), + workspaceName: `dynamicAuthWrapperProperty${fixtureName}`, audiences: { type: "all" } }); const dynamicIr = convertIrToDynamicSnippetsIr({ ir, smartCasing: true, disableExamples: true }); const endpoint = Object.values(dynamicIr.endpoints)[0]; - expect(endpoint?.auth?.wrapperProperty?.camelCase.safeName).toBe("bearer"); - expect(endpoint?.auth?.type).toBe("bearer"); + expect(endpoint?.auth?.wrapperProperty?.camelCase.safeName).toBe(expectedSafeName); + expect(endpoint?.auth?.wrapperProperty?.originalName).toBe(expectedOriginalName); + }); + + it("sets wrapperProperty for the real flattening fixture", async () => { + const ir = await generateIRFromPath({ + absolutePathToWorkspace: AbsoluteFilePath.of( + path.join(TEST_DEFINITIONS_DIR, "fern/apis/ts-flatten-request-any-auth") + ), + workspaceName: "dynamicAuthWrapperPropertyFlattening", + audiences: { type: "all" } + }); + const dynamicIr = convertIrToDynamicSnippetsIr({ ir, smartCasing: true, disableExamples: true }); + const endpoint = Object.values(dynamicIr.endpoints).find( + (candidate) => candidate.location.path === "/users/{id}" && candidate.location.method === "PUT" + ); + + expect(endpoint?.auth?.wrapperProperty?.camelCase.safeName).toBe("bearerAuth"); + expect(endpoint?.auth?.wrapperProperty?.originalName).toBe("BearerAuth"); + if (endpoint == null || endpoint.request.type !== "inlined" || endpoint.request.pathParameters == null) { + throw new Error("Expected the fixture endpoint to have an inlined request"); + } + expect(endpoint.request.body?.type).toBe("referenced"); + expect(endpoint.request.pathParameters.some((parameter) => parameter.name.wireValue === "id")).toBe(true); }); }); From f759dde1ad189a333a0979d5f6cfe8aa6d26ffbc Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 20:52:51 +0000 Subject: [PATCH 08/16] refactor(typescript): centralize auth wrapper property handling Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 8 ++++---- .../DynamicSnippetsConverter.ts | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 1e937e41e7cd..16cf735c981f 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -20,17 +20,17 @@ type AuthFields = | FernIr.dynamic.OAuth | FernIr.dynamic.InferredAuth; -type AuthWithWrapperProperty = AuthFields & { - wrapperProperty?: FernIr.dynamic.Name; +type AuthWithWrapperPropertyField = AuthFields & { + wrapperProperty?: FernIr.dynamic.Name | null; }; // TODO: remove once @fern-api/dynamic-ir-sdk >= 67.26.0 ships wrapperProperty on Auth -function hasAuthWrapperProperty(auth: AuthFields): auth is AuthWithWrapperProperty { +function hasWrapperPropertyField(auth: AuthFields): auth is AuthWithWrapperPropertyField { return "wrapperProperty" in auth; } function getAuthWrapperProperty(auth: AuthFields): FernIr.dynamic.Name | undefined { - return hasAuthWrapperProperty(auth) ? auth.wrapperProperty : undefined; + return hasWrapperPropertyField(auth) ? (auth.wrapperProperty ?? undefined) : undefined; } export class EndpointSnippetGenerator { diff --git a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts index c3e4407cb056..3612c6f371f2 100644 --- a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts +++ b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts @@ -9,6 +9,7 @@ import { assertNever } from "@fern-api/core-utils"; import { AliasTypeDeclaration, ApiAuth, + AuthScheme, ContainerType, DeclaredTypeName, dynamic as DynamicSnippets, @@ -400,6 +401,18 @@ export class DynamicSnippetsConverter { } } + private getAuthWrapperProperty(auth: ApiAuth, scheme: AuthScheme): DynamicSnippets.Name | undefined { + switch (auth.requirement) { + case "ANY": + case "ENDPOINT_SECURITY": + return this.fullCasingsGenerator.generateName(scheme.key); + case "ALL": + return undefined; + default: + assertNever(auth.requirement); + } + } + private convertPathParameters({ pathParameters }: { @@ -755,10 +768,7 @@ export class DynamicSnippetsConverter { return undefined; } const scheme = auth.schemes[0]; - const wrapperProperty = - auth.requirement === "ANY" || auth.requirement === "ENDPOINT_SECURITY" - ? this.fullCasingsGenerator.generateName(scheme.key) - : undefined; + const wrapperProperty = this.getAuthWrapperProperty(auth, scheme); switch (scheme.type) { case "basic": { const basicAuth = { From a7b956ff86e9f2fad004608677b7ff0f8b8b4ea3 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 20:53:52 +0000 Subject: [PATCH 09/16] test: fix fixture generators.yml branch names Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../fern/apis/ts-flatten-request-any-auth/generators.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml b/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml index 2e674b398233..ba8e0835050c 100644 --- a/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml +++ b/test-definitions/fern/apis/ts-flatten-request-any-auth/generators.yml @@ -9,7 +9,7 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/php-sdk-tests - branch: any-auth + branch: ts-flatten-request-any-auth go-sdk: generators: - name: fernapi/fern-go-sdk @@ -19,4 +19,4 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/go-sdk-tests - branch: any-auth + branch: ts-flatten-request-any-auth From c36a8d570fe6db5a87084f4c4c3f5e6039ffd984 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 21:04:24 +0000 Subject: [PATCH 10/16] fix(test): add response examples to TypeScript fixture Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ts-flatten-request-any-auth.json | 172 +++++++++++++++++- .../tests/wire/users.test.ts | 16 +- .../no-custom-config/tests/wire/users.test.ts | 16 +- .../definition/users.yml | 8 + 4 files changed, 202 insertions(+), 10 deletions(-) diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json index 7825adda72da..5e445f2887d3 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json @@ -316,7 +316,7 @@ "userSpecifiedExamples": [ { "example": { - "id": "f10ef3d8", + "id": "cf966c17", "name": null, "url": "/users/path-id", "rootPathParameters": [], @@ -429,7 +429,89 @@ "type": "ok", "value": { "type": "body", - "value": null + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "body-id" + } + } + }, + "jsonExample": "body-id" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "name", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "Ada" + } + } + }, + "jsonExample": "Ada" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "id": "body-id", + "name": "Ada" + } + } } }, "docs": null @@ -828,7 +910,7 @@ "userSpecifiedExamples": [ { "example": { - "id": "f10ef3d8", + "id": "cf966c17", "name": null, "url": "/users/path-id/profile", "rootPathParameters": [], @@ -941,7 +1023,89 @@ "type": "ok", "value": { "type": "body", - "value": null + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "body-id" + } + } + }, + "jsonExample": "body-id" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "name", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "Ada" + } + } + }, + "jsonExample": "Ada" + }, + "originalTypeDeclaration": { + "typeId": "type_users:UpdateUser", + "fernFilepath": { + "allParts": [ + "users" + ], + "packagePath": [], + "file": "users" + }, + "name": "UpdateUser", + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "id": "body-id", + "name": "Ada" + } + } } }, "docs": null diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts index fabc4a35cd41..0ae7aecddbaf 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts @@ -13,14 +13,22 @@ describe("UsersClient", () => { environment: server.baseUrl, }); const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawResponseBody = { id: "body-id", name: "Ada" }; - server.mockEndpoint().put("/users/path-id").jsonBody(rawRequestBody).respondWith().statusCode(200).build(); + server + .mockEndpoint() + .put("/users/path-id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); const response = await client.users.updateUser({ id: "body-id", name: "Ada", }); - expect(response).toEqual(undefined); + expect(response).toEqual(rawResponseBody); }); test("updateUserProfile", async () => { @@ -32,6 +40,7 @@ describe("UsersClient", () => { environment: server.baseUrl, }); const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawResponseBody = { id: "body-id", name: "Ada" }; server .mockEndpoint() @@ -39,12 +48,13 @@ describe("UsersClient", () => { .jsonBody(rawRequestBody) .respondWith() .statusCode(200) + .jsonBody(rawResponseBody) .build(); const response = await client.users.updateUserProfile("path-id", { id: "body-id", name: "Ada", }); - expect(response).toEqual(undefined); + expect(response).toEqual(rawResponseBody); }); }); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts index 93b3c6e5e544..6965f7a959ca 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts @@ -13,8 +13,16 @@ describe("UsersClient", () => { environment: server.baseUrl, }); const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawResponseBody = { id: "body-id", name: "Ada" }; - server.mockEndpoint().put("/users/path-id").jsonBody(rawRequestBody).respondWith().statusCode(200).build(); + server + .mockEndpoint() + .put("/users/path-id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); const response = await client.users.updateUser({ id: "path-id", @@ -23,7 +31,7 @@ describe("UsersClient", () => { name: "Ada", }, }); - expect(response).toEqual(undefined); + expect(response).toEqual(rawResponseBody); }); test("updateUserProfile", async () => { @@ -35,6 +43,7 @@ describe("UsersClient", () => { environment: server.baseUrl, }); const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawResponseBody = { id: "body-id", name: "Ada" }; server .mockEndpoint() @@ -42,12 +51,13 @@ describe("UsersClient", () => { .jsonBody(rawRequestBody) .respondWith() .statusCode(200) + .jsonBody(rawResponseBody) .build(); const response = await client.users.updateUserProfile("path-id", { id: "body-id", name: "Ada", }); - expect(response).toEqual(undefined); + expect(response).toEqual(rawResponseBody); }); }); diff --git a/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml index 35f34782e692..ab56246e061e 100644 --- a/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml +++ b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml @@ -23,6 +23,10 @@ service: request: id: body-id name: Ada + response: + body: + id: body-id + name: Ada updateUserProfile: path: /users/{id}/profile @@ -37,3 +41,7 @@ service: request: id: body-id name: Ada + response: + body: + id: body-id + name: Ada From 8e690b924d4ad47fccd81346fab9681f6a09a461 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 21:48:10 +0000 Subject: [PATCH 11/16] test(cli): refresh IR snapshots after main merge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/ir/__test__/test-definitions/cli-any-auth.json | 1 + .../ir/__test__/test-definitions/cli-multi-scheme-routing.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json index b09fe24dbbd7..7e446ddbca5f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-any-auth.json @@ -1143,6 +1143,7 @@ }, "environments": { "defaultEnvironment": "Production", + "baseUrlEnvVar": null, "environments": { "type": "singleBaseUrl", "environments": [ diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json index ac541824f0c9..a81a69e287f5 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/cli-multi-scheme-routing.json @@ -647,6 +647,7 @@ }, "environments": { "defaultEnvironment": "Default", + "baseUrlEnvVar": null, "environments": { "type": "singleBaseUrl", "environments": [ From 580895c51e854d0f4f4d53b606419e17782f2e70 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Tue, 15 Sep 2026 22:49:05 +0000 Subject: [PATCH 12/16] test: align ts-flatten-request-any-auth example ids for wire tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ts-flatten-request-any-auth.json | 28 +++++++++---------- .../flatten-request-parameters/README.md | 2 +- .../flatten-request-parameters/reference.md | 6 ++-- .../flatten-request-parameters/snippet.json | 4 +-- .../src/api/resources/users/client/Client.ts | 6 ++-- .../client/requests/UpdateUserRequest.ts | 2 +- .../tests/wire/users.test.ts | 14 +++++----- .../no-custom-config/README.md | 4 +-- .../no-custom-config/reference.md | 8 +++--- .../no-custom-config/snippet.json | 4 +-- .../src/api/resources/users/client/Client.ts | 8 +++--- .../client/requests/UpdateUserRequest.ts | 4 +-- .../no-custom-config/tests/wire/users.test.ts | 16 +++++------ .../definition/users.yml | 8 +++--- 14 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json index 5e445f2887d3..4d258703fcf1 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/ts-flatten-request-any-auth.json @@ -316,9 +316,9 @@ "userSpecifiedExamples": [ { "example": { - "id": "cf966c17", + "id": "ffa79b46", "name": null, - "url": "/users/path-id", + "url": "/users/user-1", "rootPathParameters": [], "endpointPathParameters": [ { @@ -329,11 +329,11 @@ "primitive": { "type": "string", "string": { - "original": "path-id" + "original": "user-1" } } }, - "jsonExample": "path-id" + "jsonExample": "user-1" } } ], @@ -368,11 +368,11 @@ "primitive": { "type": "string", "string": { - "original": "body-id" + "original": "user-1" } } }, - "jsonExample": "body-id" + "jsonExample": "user-1" }, "originalTypeDeclaration": { "typeId": "type_users:UpdateUser", @@ -421,7 +421,7 @@ } }, "jsonExample": { - "id": "body-id", + "id": "user-1", "name": "Ada" } }, @@ -910,9 +910,9 @@ "userSpecifiedExamples": [ { "example": { - "id": "cf966c17", + "id": "ffa79b46", "name": null, - "url": "/users/path-id/profile", + "url": "/users/user-1/profile", "rootPathParameters": [], "endpointPathParameters": [ { @@ -923,11 +923,11 @@ "primitive": { "type": "string", "string": { - "original": "path-id" + "original": "user-1" } } }, - "jsonExample": "path-id" + "jsonExample": "user-1" } } ], @@ -962,11 +962,11 @@ "primitive": { "type": "string", "string": { - "original": "body-id" + "original": "user-1" } } }, - "jsonExample": "body-id" + "jsonExample": "user-1" }, "originalTypeDeclaration": { "typeId": "type_users:UpdateUser", @@ -1015,7 +1015,7 @@ } }, "jsonExample": { - "id": "body-id", + "id": "user-1", "name": "Ada" } }, diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md index 03a80754ae1b..10a202f25e5b 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/README.md @@ -44,7 +44,7 @@ import { SeedTsFlattenRequestAnyAuthClient } from "@fern/ts-flatten-request-any- const client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: "YOUR_BASE_URL", token: "YOUR_TOKEN", apiKey: "YOUR_API_KEY" }); await client.users.updateUser({ - id: "body-id", + id: "user-1", name: "Ada" }); ``` diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md index 436da7e6fa20..452d88591721 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/reference.md @@ -14,7 +14,7 @@ ```typescript await client.users.updateUser({ - id: "body-id", + id: "user-1", name: "Ada" }); @@ -65,8 +65,8 @@ await client.users.updateUser({
```typescript -await client.users.updateUserProfile("path-id", { - id: "body-id", +await client.users.updateUserProfile("user-1", { + id: "user-1", name: "Ada" }); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json index f060dc83ec42..9cdb8f766e9e 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/snippet.json @@ -8,7 +8,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUser({\n id: \"body-id\",\n name: \"Ada\"\n});\n" + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUser({\n id: \"user-1\",\n name: \"Ada\"\n});\n" } }, { @@ -19,7 +19,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUserProfile(\"path-id\", {\n id: \"body-id\",\n name: \"Ada\"\n});\n" + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUserProfile(\"user-1\", {\n id: \"user-1\",\n name: \"Ada\"\n});\n" } } ], diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts index 5d553d7fc7eb..2e70e145dc65 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/Client.ts @@ -31,7 +31,7 @@ export class UsersClient { * * @example * await client.users.updateUser({ - * id: "body-id", + * id: "user-1", * name: "Ada" * }) */ @@ -97,8 +97,8 @@ export class UsersClient { * @throws {@link errors.SeedTsFlattenRequestAnyAuthTimeoutError} * * @example - * await client.users.updateUserProfile("path-id", { - * id: "body-id", + * await client.users.updateUserProfile("user-1", { + * id: "user-1", * name: "Ada" * }) */ diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts index 9947ae2f2317..1b7dd0602cfc 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/src/api/resources/users/client/requests/UpdateUserRequest.ts @@ -3,7 +3,7 @@ /** * @example * { - * id: "body-id", + * id: "user-1", * name: "Ada" * } */ diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts index 0ae7aecddbaf..41066996f8a8 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/flatten-request-parameters/tests/wire/users.test.ts @@ -12,12 +12,12 @@ describe("UsersClient", () => { apiKey: { apiKey: "test" }, environment: server.baseUrl, }); - const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawRequestBody = { id: "user-1", name: "Ada" }; const rawResponseBody = { id: "body-id", name: "Ada" }; server .mockEndpoint() - .put("/users/path-id") + .put("/users/user-1") .jsonBody(rawRequestBody) .respondWith() .statusCode(200) @@ -25,7 +25,7 @@ describe("UsersClient", () => { .build(); const response = await client.users.updateUser({ - id: "body-id", + id: "user-1", name: "Ada", }); expect(response).toEqual(rawResponseBody); @@ -39,20 +39,20 @@ describe("UsersClient", () => { apiKey: { apiKey: "test" }, environment: server.baseUrl, }); - const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawRequestBody = { id: "user-1", name: "Ada" }; const rawResponseBody = { id: "body-id", name: "Ada" }; server .mockEndpoint() - .put("/users/path-id/profile") + .put("/users/user-1/profile") .jsonBody(rawRequestBody) .respondWith() .statusCode(200) .jsonBody(rawResponseBody) .build(); - const response = await client.users.updateUserProfile("path-id", { - id: "body-id", + const response = await client.users.updateUserProfile("user-1", { + id: "user-1", name: "Ada", }); expect(response).toEqual(rawResponseBody); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md index 09fe92adb6b2..32bb6d434c13 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/README.md @@ -44,9 +44,9 @@ import { SeedTsFlattenRequestAnyAuthClient } from "@fern/ts-flatten-request-any- const client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: "YOUR_BASE_URL", token: "YOUR_TOKEN", apiKey: "YOUR_API_KEY" }); await client.users.updateUser({ - id: "path-id", + id: "user-1", body: { - id: "body-id", + id: "user-1", name: "Ada" } }); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md index 4d8d49e5de1a..3db4207de2fa 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/reference.md @@ -14,9 +14,9 @@ ```typescript await client.users.updateUser({ - id: "path-id", + id: "user-1", body: { - id: "body-id", + id: "user-1", name: "Ada" } }); @@ -68,8 +68,8 @@ await client.users.updateUser({
```typescript -await client.users.updateUserProfile("path-id", { - id: "body-id", +await client.users.updateUserProfile("user-1", { + id: "user-1", name: "Ada" }); diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json index f70a7de8cbc6..f90b603c09a1 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/snippet.json @@ -8,7 +8,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUser({\n id: \"path-id\",\n body: {\n id: \"body-id\",\n name: \"Ada\"\n }\n});\n" + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUser({\n id: \"user-1\",\n body: {\n id: \"user-1\",\n name: \"Ada\"\n }\n});\n" } }, { @@ -19,7 +19,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUserProfile(\"path-id\", {\n id: \"body-id\",\n name: \"Ada\"\n});\n" + "client": "import { SeedTsFlattenRequestAnyAuthClient } from \"@fern/ts-flatten-request-any-auth\";\n\nconst client = new SeedTsFlattenRequestAnyAuthClient({ baseUrl: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\", apiKey: \"YOUR_API_KEY\" });\nawait client.users.updateUserProfile(\"user-1\", {\n id: \"user-1\",\n name: \"Ada\"\n});\n" } } ], diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts index 1c3bfad7625b..8e5f11b5f25c 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/Client.ts @@ -31,9 +31,9 @@ export class UsersClient { * * @example * await client.users.updateUser({ - * id: "path-id", + * id: "user-1", * body: { - * id: "body-id", + * id: "user-1", * name: "Ada" * } * }) @@ -101,8 +101,8 @@ export class UsersClient { * @throws {@link errors.SeedTsFlattenRequestAnyAuthTimeoutError} * * @example - * await client.users.updateUserProfile("path-id", { - * id: "body-id", + * await client.users.updateUserProfile("user-1", { + * id: "user-1", * name: "Ada" * }) */ diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts index 823ba1676d92..6944179433c2 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/src/api/resources/users/client/requests/UpdateUserRequest.ts @@ -5,9 +5,9 @@ import type * as SeedTsFlattenRequestAnyAuth from "../../../../index.js"; /** * @example * { - * id: "path-id", + * id: "user-1", * body: { - * id: "body-id", + * id: "user-1", * name: "Ada" * } * } diff --git a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts index 6965f7a959ca..d6f7e9069ad2 100644 --- a/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts +++ b/seed/ts-sdk/ts-flatten-request-any-auth/no-custom-config/tests/wire/users.test.ts @@ -12,12 +12,12 @@ describe("UsersClient", () => { apiKey: { apiKey: "test" }, environment: server.baseUrl, }); - const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawRequestBody = { id: "user-1", name: "Ada" }; const rawResponseBody = { id: "body-id", name: "Ada" }; server .mockEndpoint() - .put("/users/path-id") + .put("/users/user-1") .jsonBody(rawRequestBody) .respondWith() .statusCode(200) @@ -25,9 +25,9 @@ describe("UsersClient", () => { .build(); const response = await client.users.updateUser({ - id: "path-id", + id: "user-1", body: { - id: "body-id", + id: "user-1", name: "Ada", }, }); @@ -42,20 +42,20 @@ describe("UsersClient", () => { apiKey: { apiKey: "test" }, environment: server.baseUrl, }); - const rawRequestBody = { id: "body-id", name: "Ada" }; + const rawRequestBody = { id: "user-1", name: "Ada" }; const rawResponseBody = { id: "body-id", name: "Ada" }; server .mockEndpoint() - .put("/users/path-id/profile") + .put("/users/user-1/profile") .jsonBody(rawRequestBody) .respondWith() .statusCode(200) .jsonBody(rawResponseBody) .build(); - const response = await client.users.updateUserProfile("path-id", { - id: "body-id", + const response = await client.users.updateUserProfile("user-1", { + id: "user-1", name: "Ada", }); expect(response).toEqual(rawResponseBody); diff --git a/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml index ab56246e061e..e5305c3efb21 100644 --- a/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml +++ b/test-definitions/fern/apis/ts-flatten-request-any-auth/definition/users.yml @@ -19,9 +19,9 @@ service: response: UpdateUser examples: - path-parameters: - id: path-id + id: user-1 request: - id: body-id + id: user-1 name: Ada response: body: @@ -37,9 +37,9 @@ service: response: UpdateUser examples: - path-parameters: - id: path-id + id: user-1 request: - id: body-id + id: user-1 name: Ada response: body: From e0a629134973366c5dd51339b4030ffccca1b7e2 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Wed, 16 Sep 2026 13:07:21 +0000 Subject: [PATCH 13/16] chore(typescript): point dynamic-ir-sdk compat TODO at IR 67.27.0 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../dynamic-snippets/src/EndpointSnippetGenerator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 16cf735c981f..5e4aa9606f97 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -24,7 +24,7 @@ type AuthWithWrapperPropertyField = AuthFields & { wrapperProperty?: FernIr.dynamic.Name | null; }; -// TODO: remove once @fern-api/dynamic-ir-sdk >= 67.26.0 ships wrapperProperty on Auth +// TODO: remove once @fern-api/dynamic-ir-sdk >= 67.27.0 ships wrapperProperty on Auth function hasWrapperPropertyField(auth: AuthFields): auth is AuthWithWrapperPropertyField { return "wrapperProperty" in auth; } From 1b0e80a7ed005a66e94df72499fed592d2ee08e0 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Wed, 16 Sep 2026 13:54:23 +0000 Subject: [PATCH 14/16] chore: rerun CI Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From e9d502f3949e404f7a39f8c18e6fa95eb52d567e Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Wed, 16 Sep 2026 15:06:01 +0000 Subject: [PATCH 15/16] fix(typescript): use camelCase scheme key for auth wrapper regardless of serde config Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 3 +- .../src/__test__/AuthWrapperProperty.test.ts | 97 ++++++++++++++++--- ...snippets-flatten-body-and-auth-wrapper.yml | 2 +- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 5e4aa9606f97..82b466082c94 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -378,7 +378,8 @@ export class EndpointSnippetGenerator { } return [ { - name: this.context.getPropertyName(wrapperProperty), + // SDK auth wrapper option is always camelCase(scheme key), regardless of serde/casing config + name: wrapperProperty.camelCase.unsafeName, value: ts.TypeLiteral.object({ fields }) } ]; diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts index 308b471014f9..90ff4c3bdff3 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts @@ -11,6 +11,9 @@ const IR_FILEPATH = AbsoluteFilePath.of(join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTO const REAL_FIXTURE_IR_FILEPATH = AbsoluteFilePath.of( join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "ts-flatten-request-any-auth.json") ); +const OAUTH_FIXTURE_IR_FILEPATH = AbsoluteFilePath.of( + join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, "java-endpoint-security-token-subpackage.json") +); const REQUEST: FernIr.dynamic.EndpointSnippetRequest = { endpoint: { @@ -51,6 +54,28 @@ const bearerAuthWrapperProperty: FernIr.dynamic.Name = { } }; +const REAL_FIXTURE_REQUEST: FernIr.dynamic.EndpointSnippetRequest = { + endpoint: { + method: "PUT", + path: "/users/{id}" + }, + baseURL: undefined, + environment: undefined, + auth: { + type: "bearer", + token: "" + }, + pathParameters: { + id: "path-id" + }, + queryParameters: undefined, + headers: undefined, + requestBody: { + id: "body-id", + name: "Ada" + } +}; + function addBearerAuthWrapperProperty( ir: FernIr.dynamic.DynamicIntermediateRepresentation ): FernIr.dynamic.DynamicIntermediateRepresentation { @@ -115,29 +140,75 @@ describe("auth wrapperProperty", () => { config: buildGeneratorConfig({}) }); + const response = await generator.generate({ + ...REAL_FIXTURE_REQUEST + }); + + expect(response.snippet).toContain("bearerAuth: {"); + expect(response.snippet).toContain('token: ""'); + }); + + it("uses camelCase auth wrapper options with noSerdeLayer", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: REAL_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + noSerdeLayer: true + } + }) + }); + + const response = await generator.generate(REAL_FIXTURE_REQUEST); + + expect(response.snippet).toContain("bearerAuth: {"); + expect(response.snippet).not.toContain("BearerAuth"); + }); + + it("uses camelCase auth wrapper options with retainOriginalCasing", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: REAL_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + retainOriginalCasing: true + } + }) + }); + + const response = await generator.generate(REAL_FIXTURE_REQUEST); + + expect(response.snippet).toContain("bearerAuth: {"); + expect(response.snippet).not.toContain("BearerAuth"); + }); + + it("uses the camelCase OAuth scheme key with noSerdeLayer", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: OAUTH_FIXTURE_IR_FILEPATH, + config: buildGeneratorConfig({ + customConfig: { + noSerdeLayer: true + } + }) + }); + const response = await generator.generate({ endpoint: { - method: "PUT", - path: "/users/{id}" + method: "GET", + path: "/users/mixed" }, baseURL: undefined, environment: undefined, auth: { - type: "bearer", - token: "" - }, - pathParameters: { - id: "path-id" + type: "oauth", + clientId: "", + clientSecret: "" }, + pathParameters: undefined, queryParameters: undefined, headers: undefined, - requestBody: { - id: "body-id", - name: "Ada" - } + requestBody: undefined }); - expect(response.snippet).toContain("bearerAuth: {"); - expect(response.snippet).toContain('token: ""'); + expect(response.snippet).toContain("oAuth: {"); + expect(response.snippet).not.toContain("OAuth: {"); }); }); diff --git a/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml b/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml index acd7e487d96e..60d4b98164ae 100644 --- a/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml +++ b/generators/typescript/sdk/changes/unreleased/dynamic-snippets-flatten-body-and-auth-wrapper.yml @@ -1,5 +1,5 @@ # yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json - summary: | - TypeScript dynamic snippets honor `flattenRequestParameters` by spreading referenced object bodies instead of emitting `body: {...}`, and nest multi-auth constructor options under the auth scheme key. + TypeScript dynamic snippets honor `flattenRequestParameters` by spreading referenced object bodies instead of emitting `body: {...}`, and nest multi-auth constructor options under the camelCase auth scheme key regardless of `noSerdeLayer` or `retainOriginalCasing`. type: fix From a5dbc35046650d012d69a6276aa820ef46333872 Mon Sep 17 00:00:00 2001 From: "dakshesh.daruri" Date: Wed, 16 Sep 2026 15:07:58 +0000 Subject: [PATCH 16/16] fix(typescript): match SDK oauth wrapper key casing in dynamic snippets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../dynamic-snippets/src/EndpointSnippetGenerator.ts | 7 ++++++- .../src/__test__/AuthWrapperProperty.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 82b466082c94..34b5c23f72b0 100644 --- a/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -371,6 +371,11 @@ export class EndpointSnippetGenerator { }); } + private getAuthWrapperPropertyName(wrapperProperty: FernIr.dynamic.Name): string { + // mirrors @fern-typescript/commons toCamelCase, which the SDK uses for auth wrapper option names + return wrapperProperty.camelCase.unsafeName === "oAuth" ? "oauth" : wrapperProperty.camelCase.unsafeName; + } + private wrapAuthFields({ auth, fields }: { auth: AuthFields; fields: ts.ObjectField[] }): ts.ObjectField[] { const wrapperProperty = getAuthWrapperProperty(auth); if (wrapperProperty == null) { @@ -379,7 +384,7 @@ export class EndpointSnippetGenerator { return [ { // SDK auth wrapper option is always camelCase(scheme key), regardless of serde/casing config - name: wrapperProperty.camelCase.unsafeName, + name: this.getAuthWrapperPropertyName(wrapperProperty), value: ts.TypeLiteral.object({ fields }) } ]; diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts index 90ff4c3bdff3..9db0c0de0a1f 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/AuthWrapperProperty.test.ts @@ -208,7 +208,7 @@ describe("auth wrapperProperty", () => { requestBody: undefined }); - expect(response.snippet).toContain("oAuth: {"); - expect(response.snippet).not.toContain("OAuth: {"); + expect(response.snippet).toContain("oauth: {"); + expect(response.snippet).not.toContain("oAuth"); }); });