From 4f69eeed3561b2fb3be605086d056713df327727 Mon Sep 17 00:00:00 2001 From: Deep Singhvi Date: Tue, 27 Feb 2024 18:00:29 -0500 Subject: [PATCH] (fix, typescript): serialize optional deep object query params correctly in the TypeScript SDK (#3071) * (fix): ts sdk properly serializes optional objects * add changelog and version bump for ts sdk * Update CHANGELOG.md * fix test --- generators/typescript/sdk/CHANGELOG.md | 26 +++++ generators/typescript/sdk/VERSION | 2 +- .../endpoints/utils/GeneratedQueryParams.ts | 22 +++-- .../__snapshots__/query-parameters.txt | 99 +++++++++++++++++++ seed/csharp-model/query-parameters/ir.json | 97 ++++++++++++++++++ seed/csharp-sdk/query-parameters/ir.json | 97 ++++++++++++++++++ seed/go-fiber/query-parameters/user.go | 1 + seed/go-sdk/query-parameters/user.go | 1 + .../src/seed/resources/user/client.py | 8 ++ .../src/api/resources/user/client/Client.ts | 25 ++++- .../user/client/requests/GetUsersRequest.ts | 1 + .../src/api/resources/user/client/Client.ts | 20 +++- .../user/client/requests/GetUsersRequest.ts | 1 + .../apis/query-parameters/definition/user.yml | 1 + 14 files changed, 389 insertions(+), 12 deletions(-) diff --git a/generators/typescript/sdk/CHANGELOG.md b/generators/typescript/sdk/CHANGELOG.md index 40dd3612726..3ac50bb07e4 100644 --- a/generators/typescript/sdk/CHANGELOG.md +++ b/generators/typescript/sdk/CHANGELOG.md @@ -5,6 +5,32 @@ 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). +## [0.12.1] - 2024-02-27 + +- Fix: Optional objects in deep query parameters were previously being incorrectly + serialized. Before this change, optional objects were just being JSON.stringified + which would send the incorrect contents over the wire. + + ```ts + // Before + if (foo != null) { + _queryParams["foo"] = JSON.stringify(foo); + } + + // After + if (foo != null) { + _queryParams["foo"] = foo; + } + + // After (with serde layer) + if (foo != null) { + _queryParams["foo"] = serializers.Foo.jsonOrThrow(foo, { + skipValidation: false, + breadcrumbs: ["request", "foo"] + }) + } + ``` + ## [0.12.0] - 2024-02-26 - Feature: support deep object query parameter serialization. If, query parameters are diff --git a/generators/typescript/sdk/VERSION b/generators/typescript/sdk/VERSION index ac454c6a1fc..34a83616bb5 100644 --- a/generators/typescript/sdk/VERSION +++ b/generators/typescript/sdk/VERSION @@ -1 +1 @@ -0.12.0 +0.12.1 diff --git a/generators/typescript/sdk/client-class-generator/src/endpoints/utils/GeneratedQueryParams.ts b/generators/typescript/sdk/client-class-generator/src/endpoints/utils/GeneratedQueryParams.ts index 1b6818be9d1..4406bc3191e 100644 --- a/generators/typescript/sdk/client-class-generator/src/endpoints/utils/GeneratedQueryParams.ts +++ b/generators/typescript/sdk/client-class-generator/src/endpoints/utils/GeneratedQueryParams.ts @@ -234,15 +234,23 @@ export class GeneratedQueryParams { private getObjectType(typeReference: TypeReference, context: SdkContext): DeclaredTypeName | undefined { switch (typeReference.type) { - case "named": { - const typeDeclaration = context.type.getTypeDeclaration(typeReference); - switch (typeDeclaration.shape.type) { - case "object": - return typeReference; - case "alias": { - return this.getObjectType(typeDeclaration.shape.aliasOf, context); + case "named": + { + const typeDeclaration = context.type.getTypeDeclaration(typeReference); + switch (typeDeclaration.shape.type) { + case "object": + return typeReference; + case "alias": { + return this.getObjectType(typeDeclaration.shape.aliasOf, context); + } } } + break; + case "container": { + switch (typeReference.container.type) { + case "optional": + return this.getObjectType(typeReference.container.optional, context); + } } } return undefined; diff --git a/packages/cli/generation/ir-generator/src/__test__/__snapshots__/query-parameters.txt b/packages/cli/generation/ir-generator/src/__test__/__snapshots__/query-parameters.txt index d8f0b276542..53f04dc314f 100644 --- a/packages/cli/generation/ir-generator/src/__test__/__snapshots__/query-parameters.txt +++ b/packages/cli/generation/ir-generator/src/__test__/__snapshots__/query-parameters.txt @@ -569,6 +569,105 @@ exports[`generate IR 1`] = ` "typeId": "type_user:NestedUser", }, }, + { + "allowMultiple": false, + "availability": null, + "docs": null, + "name": { + "name": { + "camelCase": { + "safeName": "optionalUser", + "unsafeName": "optionalUser", + }, + "originalName": "optionalUser", + "pascalCase": { + "safeName": "OptionalUser", + "unsafeName": "OptionalUser", + }, + "screamingSnakeCase": { + "safeName": "OPTIONAL_USER", + "unsafeName": "OPTIONAL_USER", + }, + "snakeCase": { + "safeName": "optional_user", + "unsafeName": "optional_user", + }, + }, + "wireValue": "optionalUser", + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "named", + "fernFilepath": { + "allParts": [ + { + "camelCase": { + "safeName": "user", + "unsafeName": "user", + }, + "originalName": "user", + "pascalCase": { + "safeName": "User", + "unsafeName": "User", + }, + "screamingSnakeCase": { + "safeName": "USER", + "unsafeName": "USER", + }, + "snakeCase": { + "safeName": "user", + "unsafeName": "user", + }, + }, + ], + "file": { + "camelCase": { + "safeName": "user", + "unsafeName": "user", + }, + "originalName": "user", + "pascalCase": { + "safeName": "User", + "unsafeName": "User", + }, + "screamingSnakeCase": { + "safeName": "USER", + "unsafeName": "USER", + }, + "snakeCase": { + "safeName": "user", + "unsafeName": "user", + }, + }, + "packagePath": [], + }, + "name": { + "camelCase": { + "safeName": "user", + "unsafeName": "user", + }, + "originalName": "User", + "pascalCase": { + "safeName": "User", + "unsafeName": "User", + }, + "screamingSnakeCase": { + "safeName": "USER", + "unsafeName": "USER", + }, + "snakeCase": { + "safeName": "user", + "unsafeName": "user", + }, + }, + "typeId": "type_user:User", + }, + }, + }, + }, { "allowMultiple": true, "availability": null, diff --git a/seed/csharp-model/query-parameters/ir.json b/seed/csharp-model/query-parameters/ir.json index d28bee23c51..4c240f255ea 100644 --- a/seed/csharp-model/query-parameters/ir.json +++ b/seed/csharp-model/query-parameters/ir.json @@ -847,6 +847,103 @@ }, "allowMultiple": false }, + { + "name": { + "name": { + "originalName": "optionalUser", + "camelCase": { + "unsafeName": "optionalUser", + "safeName": "optionalUser" + }, + "snakeCase": { + "unsafeName": "optional_user", + "safeName": "optional_user" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_USER", + "safeName": "OPTIONAL_USER" + }, + "pascalCase": { + "unsafeName": "OptionalUser", + "safeName": "OptionalUser" + } + }, + "wireValue": "optionalUser" + }, + "valueType": { + "container": { + "optional": { + "type": "named", + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + }, + "type": "optional" + }, + "type": "container" + }, + "allowMultiple": false + }, { "name": { "name": { diff --git a/seed/csharp-sdk/query-parameters/ir.json b/seed/csharp-sdk/query-parameters/ir.json index d28bee23c51..4c240f255ea 100644 --- a/seed/csharp-sdk/query-parameters/ir.json +++ b/seed/csharp-sdk/query-parameters/ir.json @@ -847,6 +847,103 @@ }, "allowMultiple": false }, + { + "name": { + "name": { + "originalName": "optionalUser", + "camelCase": { + "unsafeName": "optionalUser", + "safeName": "optionalUser" + }, + "snakeCase": { + "unsafeName": "optional_user", + "safeName": "optional_user" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_USER", + "safeName": "OPTIONAL_USER" + }, + "pascalCase": { + "unsafeName": "OptionalUser", + "safeName": "OptionalUser" + } + }, + "wireValue": "optionalUser" + }, + "valueType": { + "container": { + "optional": { + "type": "named", + "name": { + "originalName": "User", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + ], + "packagePath": [], + "file": { + "originalName": "user", + "camelCase": { + "unsafeName": "user", + "safeName": "user" + }, + "snakeCase": { + "unsafeName": "user", + "safeName": "user" + }, + "screamingSnakeCase": { + "unsafeName": "USER", + "safeName": "USER" + }, + "pascalCase": { + "unsafeName": "User", + "safeName": "User" + } + } + }, + "typeId": "type_user:User" + }, + "type": "optional" + }, + "type": "container" + }, + "allowMultiple": false + }, { "name": { "name": { diff --git a/seed/go-fiber/query-parameters/user.go b/seed/go-fiber/query-parameters/user.go index b09646ac027..9254278912b 100644 --- a/seed/go-fiber/query-parameters/user.go +++ b/seed/go-fiber/query-parameters/user.go @@ -19,6 +19,7 @@ type GetUsersRequest struct { KeyValue map[string]string `query:"keyValue"` OptionalString *string `query:"optionalString"` NestedUser *NestedUser `query:"nestedUser"` + OptionalUser *User `query:"optionalUser"` ExcludeUser []*User `query:"excludeUser"` Filter []string `query:"filter"` } diff --git a/seed/go-sdk/query-parameters/user.go b/seed/go-sdk/query-parameters/user.go index 5be56a955fe..2d32aea03a3 100644 --- a/seed/go-sdk/query-parameters/user.go +++ b/seed/go-sdk/query-parameters/user.go @@ -20,6 +20,7 @@ type GetUsersRequest struct { KeyValue map[string]string `json:"-" url:"keyValue,omitempty"` OptionalString *string `json:"-" url:"optionalString,omitempty"` NestedUser *NestedUser `json:"-" url:"nestedUser,omitempty"` + OptionalUser *User `json:"-" url:"optionalUser,omitempty"` ExcludeUser []*User `json:"-" url:"excludeUser,omitempty"` Filter []string `json:"-" url:"filter"` } diff --git a/seed/python-sdk/query-parameters/src/seed/resources/user/client.py b/seed/python-sdk/query-parameters/src/seed/resources/user/client.py index bba8693476b..2642c848371 100644 --- a/seed/python-sdk/query-parameters/src/seed/resources/user/client.py +++ b/seed/python-sdk/query-parameters/src/seed/resources/user/client.py @@ -37,6 +37,7 @@ def get_username( key_value: typing.Dict[str, str], optional_string: typing.Optional[str] = None, nested_user: NestedUser, + optional_user: typing.Optional[User] = None, exclude_user: typing.Union[User, typing.Sequence[User]], filter: typing.Union[str, typing.Sequence[str]], request_options: typing.Optional[RequestOptions] = None, @@ -61,6 +62,8 @@ def get_username( - nested_user: NestedUser. + - optional_user: typing.Optional[User]. + - exclude_user: typing.Union[User, typing.Sequence[User]]. - filter: typing.Union[str, typing.Sequence[str]]. @@ -82,6 +85,7 @@ def get_username( "keyValue": jsonable_encoder(key_value), "optionalString": optional_string, "nestedUser": jsonable_encoder(nested_user), + "optionalUser": jsonable_encoder(optional_user), "excludeUser": jsonable_encoder(exclude_user), "filter": filter, **( @@ -129,6 +133,7 @@ async def get_username( key_value: typing.Dict[str, str], optional_string: typing.Optional[str] = None, nested_user: NestedUser, + optional_user: typing.Optional[User] = None, exclude_user: typing.Union[User, typing.Sequence[User]], filter: typing.Union[str, typing.Sequence[str]], request_options: typing.Optional[RequestOptions] = None, @@ -153,6 +158,8 @@ async def get_username( - nested_user: NestedUser. + - optional_user: typing.Optional[User]. + - exclude_user: typing.Union[User, typing.Sequence[User]]. - filter: typing.Union[str, typing.Sequence[str]]. @@ -174,6 +181,7 @@ async def get_username( "keyValue": jsonable_encoder(key_value), "optionalString": optional_string, "nestedUser": jsonable_encoder(nested_user), + "optionalUser": jsonable_encoder(optional_user), "excludeUser": jsonable_encoder(exclude_user), "filter": filter, **( diff --git a/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/Client.ts b/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/Client.ts index 94154c04ece..961afd64b49 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/Client.ts +++ b/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/Client.ts @@ -26,8 +26,20 @@ export class User { request: SeedQueryParameters.GetUsersRequest, requestOptions?: User.RequestOptions ): Promise { - const { limit, id, date, deadline, bytes, user, keyValue, optionalString, nestedUser, excludeUser, filter } = - request; + const { + limit, + id, + date, + deadline, + bytes, + user, + keyValue, + optionalString, + nestedUser, + optionalUser, + excludeUser, + filter, + } = request; const _queryParams: Record = {}; _queryParams["limit"] = limit.toString(); _queryParams["id"] = id; @@ -51,6 +63,15 @@ export class User { allowUnrecognizedEnumValues: true, breadcrumbsPrefix: ["request", "nestedUser"], }); + if (optionalUser != null) { + _queryParams["optionalUser"] = await serializers.User.jsonOrThrow(optionalUser, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["request", "optionalUser"], + }); + } + if (Array.isArray(excludeUser)) { _queryParams["excludeUser"] = await Promise.all( excludeUser.map( diff --git a/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/requests/GetUsersRequest.ts b/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/requests/GetUsersRequest.ts index b72897bb6fb..b8c0e282547 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/requests/GetUsersRequest.ts +++ b/seed/ts-sdk/query-parameters/no-custom-config/src/api/resources/user/client/requests/GetUsersRequest.ts @@ -14,6 +14,7 @@ export interface GetUsersRequest { keyValue: Record; optionalString?: string; nestedUser: SeedQueryParameters.NestedUser; + optionalUser?: SeedQueryParameters.User; excludeUser: SeedQueryParameters.User | SeedQueryParameters.User[]; filter: string | string[]; } diff --git a/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/Client.ts b/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/Client.ts index 8814ef03518..d899a404027 100644 --- a/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/Client.ts +++ b/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/Client.ts @@ -25,8 +25,20 @@ export class User { request: SeedQueryParameters.GetUsersRequest, requestOptions?: User.RequestOptions ): Promise { - const { limit, id, date, deadline, bytes, user, keyValue, optionalString, nestedUser, excludeUser, filter } = - request; + const { + limit, + id, + date, + deadline, + bytes, + user, + keyValue, + optionalString, + nestedUser, + optionalUser, + excludeUser, + filter, + } = request; const _queryParams: Record = {}; _queryParams["limit"] = limit.toString(); _queryParams["id"] = id; @@ -40,6 +52,10 @@ export class User { } _queryParams["nestedUser"] = nestedUser; + if (optionalUser != null) { + _queryParams["optionalUser"] = optionalUser; + } + if (Array.isArray(excludeUser)) { _queryParams["excludeUser"] = excludeUser.map((item) => item); } else { diff --git a/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/requests/GetUsersRequest.ts b/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/requests/GetUsersRequest.ts index 3f260bce22e..349c91e1adb 100644 --- a/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/requests/GetUsersRequest.ts +++ b/seed/ts-sdk/query-parameters/no-serde-layer-query/src/api/resources/user/client/requests/GetUsersRequest.ts @@ -14,6 +14,7 @@ export interface GetUsersRequest { keyValue: Record; optionalString?: string; nestedUser: SeedQueryParameters.NestedUser; + optionalUser?: SeedQueryParameters.User; excludeUser: SeedQueryParameters.User | SeedQueryParameters.User[]; filter: string | string[]; } diff --git a/test-definitions/fern/apis/query-parameters/definition/user.yml b/test-definitions/fern/apis/query-parameters/definition/user.yml index 5cf896f3004..6587ab788ea 100644 --- a/test-definitions/fern/apis/query-parameters/definition/user.yml +++ b/test-definitions/fern/apis/query-parameters/definition/user.yml @@ -27,6 +27,7 @@ service: keyValue: map optionalString: optional nestedUser: NestedUser + optionalUser: optional excludeUser: type: User allow-multiple: true