diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/post-process.ts b/packages/type-safe-api/scripts/type-safe-api/generators/post-process.ts index cd40c20ba..6e596eae1 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/post-process.ts +++ b/packages/type-safe-api/scripts/type-safe-api/generators/post-process.ts @@ -3,6 +3,7 @@ SPDX-License-Identifier: Apache-2.0 */ import * as fs from "fs"; import * as path from "path"; import kebabCase from "lodash/kebabCase"; +import camelCase from "lodash/camelCase"; import { parse } from "ts-command-line-args"; // Used to split OpenAPI generated files into multiple files in order to work around @@ -32,6 +33,8 @@ const applyReplacementFunction = (functionConfig: FunctionConfig): string => { switch (functionConfig.function) { case "kebabCase": return kebabCase(functionConfig.args[0]); + case "camelCase": + return camelCase(functionConfig.args[0]); default: throw new Error(`Unsupported TSAPI_FN function ${functionConfig.function}`); } @@ -127,6 +130,9 @@ void (async () => { // Delete the original file fs.rmSync(filePath); + } else { + // Apply the replacement functions directly + fs.writeFileSync(filePath, applyReplacementFunctions(contents)); } } }); diff --git a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-react-query-hooks/templates/hooks.handlebars b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-react-query-hooks/templates/hooks.handlebars index 7b4c1848a..357e06b29 100644 --- a/packages/type-safe-api/scripts/type-safe-api/generators/typescript-react-query-hooks/templates/hooks.handlebars +++ b/packages/type-safe-api/scripts/type-safe-api/generators/typescript-react-query-hooks/templates/hooks.handlebars @@ -54,7 +54,7 @@ export const use{{operationIdCamelCase}} = ( if (!api) { throw NO_API_ERROR; } - return useInfiniteQuery(["{{nickname}}"{{#allParams.0}}, params{{/allParams.0}}], ({ pageParam }) => api.{{nickname}}({ {{#allParams.0}}...params, {{/allParams.0}}{{vendorExtensions.x-paginated.inputToken}}: pageParam }), { + return useInfiniteQuery(["{{nickname}}"{{#allParams.0}}, params{{/allParams.0}}], ({ pageParam }) => api.{{nickname}}({ {{#allParams.0}}...params, {{/allParams.0}}###TSAPI_FN###{"function": "camelCase", "args": ["{{vendorExtensions.x-paginated.inputToken}}"]}###/TSAPI_FN###: pageParam }), { getNextPageParam: (response) => response.{{vendorExtensions.x-paginated.outputToken}}, context: {{classname}}DefaultContext, ...options as any, diff --git a/packages/type-safe-api/scripts/type-safe-api/parser/parse-openapi-spec.ts b/packages/type-safe-api/scripts/type-safe-api/parser/parse-openapi-spec.ts index 49003f2f8..ef29abb79 100644 --- a/packages/type-safe-api/scripts/type-safe-api/parser/parse-openapi-spec.ts +++ b/packages/type-safe-api/scripts/type-safe-api/parser/parse-openapi-spec.ts @@ -3,24 +3,42 @@ SPDX-License-Identifier: Apache-2.0 */ import SwaggerParser from "@apidevtools/swagger-parser"; import { writeFile } from "projen/lib/util"; import { parse } from "ts-command-line-args"; -import * as path from 'path'; import * as _ from "lodash"; import fs from "fs"; -import type { OpenAPIV3 } from "openapi-types"; // Smithy HTTP trait is used to map Smithy operations to their location in the spec const SMITHY_HTTP_TRAIT_ID = "smithy.api#http"; +// The OpenAPI vendor extension used for paginated operations +const PAGINATED_VENDOR_EXTENSION = "x-paginated"; + +// Traits that will "rename" members in the generated OpenAPI spec +const SMITHY_RENAME_TRAITS = [ + "smithy.api#httpQuery", + "smithy.api#httpHeader", +]; + // Maps traits to specific vendor extensions which we also support specifying in OpenAPI const TRAIT_TO_SUPPORTED_OPENAPI_VENDOR_EXTENSION: { [key: string]: string } = { - "smithy.api#paginated": "x-paginated", + "smithy.api#paginated": PAGINATED_VENDOR_EXTENSION, }; +interface SmithyMember { + readonly target: string; + readonly traits?: { [key: string]: any }; +} + +interface SmithyOperationInput { + readonly type: string; + readonly members?: { [key: string]: SmithyMember } +} + interface SmithyOperationDetails { readonly id: string; readonly method: string; readonly path: string; readonly traits: { [key: string]: any }; + readonly input?: SmithyOperationInput; } interface InvalidRequestParameter { @@ -80,6 +98,7 @@ void (async () => { method: shape.traits[SMITHY_HTTP_TRAIT_ID].method?.toLowerCase(), path: shape.traits[SMITHY_HTTP_TRAIT_ID].uri, traits: shape.traits, + input: smithyModel.shapes[shape.input?.target], })); // Apply all operation-level traits as vendor extensions to the relevant operation in the spec @@ -96,7 +115,20 @@ void (async () => { if (traitId.endsWith("#handler")) { vendorExtension = "x-handler"; } - spec.paths[operation.path][operation.method][vendorExtension] = value; + + let extensionValue = value; + + // The smithy paginated trait is written in terms of inputs which may have different names in openapi + // so we must map them here + if (vendorExtension === PAGINATED_VENDOR_EXTENSION) { + extensionValue = Object.fromEntries(Object.entries(value as {[key: string]: string}).map(([traitProperty, memberName]) => { + const member = operation.input?.members?.[memberName]; + const renamedMemberName = SMITHY_RENAME_TRAITS.map(trait => member?.traits?.[trait]).find(x => x) ?? memberName; + return [traitProperty, renamedMemberName]; + })); + } + + spec.paths[operation.path][operation.method][vendorExtension] = extensionValue; }); } }); diff --git a/packages/type-safe-api/src/project/codegen/library/typescript-react-query-hooks-library.ts b/packages/type-safe-api/src/project/codegen/library/typescript-react-query-hooks-library.ts index d43904021..644924346 100644 --- a/packages/type-safe-api/src/project/codegen/library/typescript-react-query-hooks-library.ts +++ b/packages/type-safe-api/src/project/codegen/library/typescript-react-query-hooks-library.ts @@ -90,7 +90,7 @@ export class TypescriptReactQueryHooksLibrary extends TypeScriptProject { } // Add dependencies on react-query and react - this.addDeps("@tanstack/react-query"); + this.addDeps("@tanstack/react-query@^4"); // Pin at 4 for now - requires generated code updates to upgrade to 5 this.addDevDeps("react", "@types/react"); this.addPeerDeps("react"); diff --git a/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap b/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap index a85463edd..2172e839a 100644 --- a/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap +++ b/packages/type-safe-api/test/project/__snapshots__/type-safe-api-project.test.ts.snap @@ -27858,6 +27858,7 @@ tsconfig.esm.json { "name": "@tanstack/react-query", "type": "runtime", + "version": "^4", }, ], }, @@ -28244,7 +28245,7 @@ tsconfig.esm.json "generated/libraries/typescript-react-query-hooks/package.json": { "//": "~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".", "dependencies": { - "@tanstack/react-query": "*", + "@tanstack/react-query": "^4", }, "devDependencies": { "@types/node": "^16", diff --git a/packages/type-safe-api/test/resources/smithy/rename-pagination/model.json b/packages/type-safe-api/test/resources/smithy/rename-pagination/model.json new file mode 100644 index 000000000..97f79d459 --- /dev/null +++ b/packages/type-safe-api/test/resources/smithy/rename-pagination/model.json @@ -0,0 +1,997 @@ +{ + "smithy": "2.0", + "metadata": { + "validators": [ + { + "configuration": { + "bindToTrait": "com.aws#handler", + "messageTemplate": " @{trait|com.aws#handler|language} is not supported by type-safe-api.\n Supported languages are \"typescript\", \"java\" and \"python\".\n", + "selector": " :not([@trait|com.aws#handler: @{language} = typescript, java, python])\n" + }, + "id": "SupportedLanguage", + "name": "EmitEachSelector" + }, + { + "configuration": { + "bindToTrait": "com.aws#handler", + "messageTemplate": " @@handler language @{trait|com.aws#handler|language} cannot be referenced unless a handler project is configured for this language.\n Configured handler project languages are: typescript.\n You can add this language by configuring TypeSafeApiProject in your .projenrc\n", + "selector": " [@trait|com.aws#handler: @{language} = typescript, java, python]\n :not([@trait|com.aws#handler: @{language} = typescript])\n" + }, + "id": "ConfiguredHandlerProject", + "name": "EmitEachSelector" + } + ] + }, + "shapes": { + "aws.api#ArnNamespace": { + "type": "string", + "traits": { + "smithy.api#documentation": "A string representing a service's ARN namespace.", + "smithy.api#pattern": "^[a-z0-9.\\-]{1,63}$", + "smithy.api#private": {} + } + }, + "aws.api#CloudFormationName": { + "type": "string", + "traits": { + "smithy.api#documentation": "A string representing a CloudFormation service name.", + "smithy.api#pattern": "^[A-Z][A-Za-z0-9]+$", + "smithy.api#private": {} + } + }, + "aws.api#TagOperationReference": { + "type": "string", + "traits": { + "smithy.api#documentation": "Points to an operation designated for a tagging APi", + "smithy.api#idRef": { + "failWhenMissing": true, + "selector": "resource > operation" + } + } + }, + "aws.api#TaggableApiConfig": { + "type": "structure", + "members": { + "tagApi": { + "target": "aws.api#TagOperationReference", + "traits": { + "smithy.api#documentation": "The `tagApi` property is a string value that references a non-instance\nor create operation that creates or updates tags on the resource.", + "smithy.api#required": {} + } + }, + "untagApi": { + "target": "aws.api#TagOperationReference", + "traits": { + "smithy.api#documentation": "The `untagApi` property is a string value that references a non-instance\noperation that removes tags on the resource.", + "smithy.api#required": {} + } + }, + "listTagsApi": { + "target": "aws.api#TagOperationReference", + "traits": { + "smithy.api#documentation": "The `listTagsApi` property is a string value that references a non-\ninstance operation which gets the current tags on the resource.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Structure representing the configuration of resource specific tagging APIs" + } + }, + "aws.api#arn": { + "type": "structure", + "members": { + "template": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Defines the ARN template. The provided string contains URI-template\nstyle label placeholders that contain the name of one of the identifiers\ndefined in the `identifiers` property of the resource. These labels can\nbe substituted at runtime with the actual identifiers of the resource.\nEvery identifier name of the resource MUST have corresponding label of\nthe same name. Note that percent-encoding **is not** performed on these\nplaceholder values; they are to be replaced literally. For relative ARN\ntemplates that have not set `absolute` to `true`, the template string\ncontains only the resource part of the ARN (for example,\n`foo/{MyResourceId}`). Relative ARNs MUST NOT start with \"/\".", + "smithy.api#required": {} + } + }, + "absolute": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "Set to true to indicate that the ARN template contains a fully-formed\nARN that does not need to be merged with the service. This type of ARN\nMUST be used when the identifier of a resource is an ARN or is based on\nthe ARN identifier of a parent resource." + } + }, + "noRegion": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "Set to true to specify that the ARN does not contain a region. If not\nset, or if set to false, the resolved ARN will contain a placeholder\nfor the region. This can only be set to true if `absolute` is not set\nor is false." + } + }, + "noAccount": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "Set to true to specify that the ARN does not contain an account ID. If\nnot set, or if set to false, the resolved ARN will contain a placeholder\nfor the customer account ID. This can only be set to true if absolute\nis not set or is false." + } + } + }, + "traits": { + "smithy.api#documentation": "Specifies an ARN template for the resource.", + "smithy.api#externalDocumentation": { + "Reference": "https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html" + }, + "smithy.api#trait": { + "selector": "resource" + } + } + }, + "aws.api#arnReference": { + "type": "structure", + "members": { + "type": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The AWS CloudFormation resource type contained in the ARN." + } + }, + "resource": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "An absolute shape ID that references the Smithy resource type contained\nin the ARN (e.g., `com.foo#SomeResource`). The targeted resource is not\nrequired to be found in the model, allowing for external shapes to be\nreferenced without needing to take on an additional dependency. If the\nshape is found in the model, it MUST target a resource shape, and the\nresource MUST be found within the closure of the referenced service\nshape." + } + }, + "service": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The Smithy service absolute shape ID that is referenced by the ARN. The\ntargeted service is not required to be found in the model, allowing for\nexternal shapes to be referenced without needing to take on an\nadditional dependency." + } + } + }, + "traits": { + "smithy.api#documentation": "Marks a string as containing an ARN.", + "smithy.api#trait": { + "selector": "string" + } + } + }, + "aws.api#clientDiscoveredEndpoint": { + "type": "structure", + "members": { + "required": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "This field denotes whether or not this operation requires the use of a\nspecific endpoint. If this field is false, the standard regional\nendpoint for a service can handle this request. The client will start\nsending requests to the standard regional endpoint while working to\ndiscover a more specific endpoint.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Indicates that the target operation should use the client's endpoint\ndiscovery logic.", + "smithy.api#trait": { + "selector": "operation" + } + } + }, + "aws.api#clientEndpointDiscovery": { + "type": "structure", + "members": { + "operation": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Indicates the operation that clients should use to discover endpoints\nfor the service.", + "smithy.api#idRef": { + "failWhenMissing": true, + "selector": "operation" + }, + "smithy.api#required": {} + } + }, + "error": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Indicates the error that tells clients that the endpoint they are using\nis no longer valid. This error MUST be bound to any operation bound to\nthe service which is marked with the aws.api#clientDiscoveredEndpoint\ntrait.", + "smithy.api#idRef": { + "failWhenMissing": true, + "selector": "structure[trait|error]" + }, + "smithy.api#recommended": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Configures endpoint discovery for the service.", + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.api#clientEndpointDiscoveryId": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "Indicates members of the operation input which should be use to discover\nendpoints.", + "smithy.api#trait": { + "selector": "operation[trait|aws.api#clientDiscoveredEndpoint] -[input]->\nstructure > :test(member[trait|required] > string)" + } + } + }, + "aws.api#controlPlane": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "Defines a service, resource, or operation as operating on the control plane.", + "smithy.api#trait": { + "selector": ":test(service, resource, operation)", + "conflicts": [ + "aws.api#dataPlane" + ] + } + } + }, + "aws.api#data": { + "type": "enum", + "members": { + "CUSTOMER_CONTENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Customer content means any software (including machine images), data,\ntext, audio, video or images that customers or any customer end user\ntransfers to AWS for processing, storage or hosting by AWS services in\nconnection with the customer’s accounts and any computational results\nthat customers or any customer end user derive from the foregoing\nthrough their use of AWS services.", + "smithy.api#enumValue": "content" + } + }, + "CUSTOMER_ACCOUNT_INFORMATION": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Account information means information about customers that customers\nprovide to AWS in connection with the creation or administration of\ncustomers’ accounts.", + "smithy.api#enumValue": "account" + } + }, + "SERVICE_ATTRIBUTES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Service Attributes means service usage data related to a customer’s\naccount, such as resource identifiers, metadata tags, security and\naccess roles, rules, usage policies, permissions, usage statistics,\nlogging data, and analytics.", + "smithy.api#enumValue": "usage" + } + }, + "TAG_DATA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Designates metadata tags applied to AWS resources.", + "smithy.api#enumValue": "tagging" + } + }, + "PERMISSIONS_DATA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Designates security and access roles, rules, usage policies, and\npermissions.", + "smithy.api#enumValue": "permissions" + } + } + }, + "traits": { + "smithy.api#documentation": "Designates the target as containing data of a known classification level.", + "smithy.api#trait": { + "selector": ":test(simpleType, list, structure, union, member)" + } + } + }, + "aws.api#dataPlane": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "Defines a service, resource, or operation as operating on the data plane.", + "smithy.api#trait": { + "selector": ":test(service, resource, operation)", + "conflicts": [ + "aws.api#controlPlane" + ] + } + } + }, + "aws.api#service": { + "type": "structure", + "members": { + "sdkId": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The `sdkId` property is a required string value that specifies the AWS\nSDK service ID (e.g., \"API Gateway\"). This value is used for generating\nclient names in SDKs and for linking between services.", + "smithy.api#required": {} + } + }, + "arnNamespace": { + "target": "aws.api#ArnNamespace", + "traits": { + "smithy.api#documentation": "The `arnNamespace` property is a string value that defines the ARN service\nnamespace of the service (e.g., \"apigateway\"). This value is used in\nARNs assigned to resources in the service. If not set, this value\ndefaults to the lowercase name of the service shape." + } + }, + "cloudFormationName": { + "target": "aws.api#CloudFormationName", + "traits": { + "smithy.api#documentation": "The `cloudFormationName` property is a string value that specifies the\nAWS CloudFormation service name (e.g., `ApiGateway`). When not set,\nthis value defaults to the name of the service shape. This value is\npart of the CloudFormation resource type name that is automatically\nassigned to resources in the service (e.g., `AWS::::resourceName`)." + } + }, + "cloudTrailEventSource": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The `cloudTrailEventSource` property is a string value that defines the\nAWS customer-facing eventSource property contained in CloudTrail event\nrecords emitted by the service. If not specified, this value defaults\nto the `arnNamespace` plus `.amazonaws.com`." + } + }, + "endpointPrefix": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The `endpointPrefix` property is a string value that identifies which\nendpoint in a given region should be used to connect to the service.\nFor example, most services in the AWS standard partition have endpoints\nwhich follow the format: `{endpointPrefix}.{region}.amazonaws.com`. A\nservice with the endpoint prefix example in the region us-west-2 might\nhave the endpoint example.us-west-2.amazonaws.com.\n\nThis value is not unique across services and is subject to change.\nTherefore, it MUST NOT be used for client naming or for any other\npurpose that requires a static, unique identifier. sdkId should be used\nfor those purposes. Additionally, this value can be used to attempt to\nresolve endpoints." + } + } + }, + "traits": { + "smithy.api#documentation": "An AWS service is defined using the `aws.api#service` trait. This trait\nprovides information about the service like the name used to generate AWS\nSDK client classes and the namespace used in ARNs.", + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.api#tagEnabled": { + "type": "structure", + "members": { + "disableDefaultOperations": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "The `disableDefaultOperations` property is a boolean value that specifies\nif the service does not have the standard tag operations supporting all\nresources on the service. Default value is `false`" + } + } + }, + "traits": { + "smithy.api#documentation": "Annotates a service as having tagging on 1 or more resources and associated\nAPIs to perform CRUD operations on those tags", + "smithy.api#trait": { + "selector": "service" + }, + "smithy.api#unstable": {} + } + }, + "aws.api#taggable": { + "type": "structure", + "members": { + "property": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The `property` property is a string value that identifies which\nresource property represents tags for the resource." + } + }, + "apiConfig": { + "target": "aws.api#TaggableApiConfig", + "traits": { + "smithy.api#documentation": "Specifies configuration for resource specific tagging APIs if the\nresource has them." + } + }, + "disableSystemTags": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "Flag indicating if the resource is not able to carry AWS system level.\nUsed by service principals. Default value is `false`" + } + } + }, + "traits": { + "smithy.api#documentation": "Indicates a resource supports CRUD operations for tags. Either through\nresource lifecycle or instance operations or tagging operations on the\nservice.", + "smithy.api#trait": { + "selector": "resource" + }, + "smithy.api#unstable": {} + } + }, + "aws.auth#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#private": {} + } + }, + "aws.auth#cognitoUserPools": { + "type": "structure", + "members": { + "providerArns": { + "target": "aws.auth#StringList", + "traits": { + "smithy.api#documentation": "A list of the Amazon Cognito user pool ARNs. Each element is of this\nformat: `arn:aws:cognito-idp:{region}:{account_id}:userpool/{user_pool_id}`.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#authDefinition": {}, + "smithy.api#documentation": "Configures an Amazon Cognito User Pools auth scheme.", + "smithy.api#internal": {}, + "smithy.api#tags": [ + "internal" + ], + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.auth#sigv4": { + "type": "structure", + "members": { + "name": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The signature version 4 service signing name to use in the credential\nscope when signing requests. This value SHOULD match the `arnNamespace`\nproperty of the `aws.api#service-trait`.", + "smithy.api#externalDocumentation": { + "Reference": "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html" + }, + "smithy.api#length": { + "min": 1 + }, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#authDefinition": { + "traits": [ + "aws.auth#unsignedPayload" + ] + }, + "smithy.api#documentation": "Signature Version 4 is the process to add authentication information to\nAWS requests sent by HTTP. For security, most requests to AWS must be\nsigned with an access key, which consists of an access key ID and secret\naccess key. These two keys are commonly referred to as your security\ncredentials.", + "smithy.api#externalDocumentation": { + "Reference": "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html" + }, + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.auth#unsignedPayload": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "Indicates that the request payload of a signed request is not to be used\nas part of the signature.", + "smithy.api#trait": { + "selector": "operation" + } + } + }, + "aws.customizations#s3UnwrappedXmlOutput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "Indicates the response body from S3 is not wrapped in the\naws-restxml-protocol operation-level XML node. Intended to only be used by\nAWS S3.", + "smithy.api#trait": { + "selector": "operation" + } + } + }, + "aws.protocols#ChecksumAlgorithm": { + "type": "enum", + "members": { + "CRC32C": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "CRC32C", + "smithy.api#enumValue": "CRC32C" + } + }, + "CRC32": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "CRC32", + "smithy.api#enumValue": "CRC32" + } + }, + "SHA1": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "SHA1", + "smithy.api#enumValue": "SHA1" + } + }, + "SHA256": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "SHA256", + "smithy.api#enumValue": "SHA256" + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "aws.protocols#ChecksumAlgorithmSet": { + "type": "list", + "member": { + "target": "aws.protocols#ChecksumAlgorithm" + }, + "traits": { + "smithy.api#length": { + "min": 1 + }, + "smithy.api#private": {}, + "smithy.api#uniqueItems": {} + } + }, + "aws.protocols#HttpConfiguration": { + "type": "structure", + "members": { + "http": { + "target": "aws.protocols#StringList", + "traits": { + "smithy.api#documentation": "The priority ordered list of supported HTTP protocol versions." + } + }, + "eventStreamHttp": { + "target": "aws.protocols#StringList", + "traits": { + "smithy.api#documentation": "The priority ordered list of supported HTTP protocol versions that\nare required when using event streams with the service. If not set,\nthis value defaults to the value of the `http` member. Any entry in\n`eventStreamHttp` MUST also appear in `http`." + } + } + }, + "traits": { + "smithy.api#documentation": "Contains HTTP protocol configuration for HTTP-based protocols.", + "smithy.api#mixin": { + "localTraits": [ + "smithy.api#private" + ] + }, + "smithy.api#private": {} + } + }, + "aws.protocols#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#private": {} + } + }, + "aws.protocols#awsJson1_0": { + "type": "structure", + "mixins": [ + { + "target": "aws.protocols#HttpConfiguration" + } + ], + "members": {}, + "traits": { + "smithy.api#documentation": "An RPC-based protocol that sends JSON payloads. This protocol does not use\nHTTP binding traits.", + "smithy.api#protocolDefinition": { + "traits": [ + "smithy.api#timestampFormat", + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel" + ] + }, + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.protocols#awsJson1_1": { + "type": "structure", + "mixins": [ + { + "target": "aws.protocols#HttpConfiguration" + } + ], + "members": {}, + "traits": { + "smithy.api#documentation": "An RPC-based protocol that sends JSON payloads. This protocol does not use\nHTTP binding traits.", + "smithy.api#protocolDefinition": { + "traits": [ + "smithy.api#timestampFormat", + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel" + ] + }, + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.protocols#awsQuery": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#deprecated": {}, + "smithy.api#documentation": "An RPC-based protocol that sends 'POST' requests in the body as\n`x-www-form-urlencoded` strings and responses in XML documents. This\nprotocol does not use HTTP binding traits.", + "smithy.api#protocolDefinition": { + "noInlineDocumentSupport": true, + "traits": [ + "aws.protocols#awsQueryError", + "smithy.api#xmlAttribute", + "smithy.api#xmlFlattened", + "smithy.api#xmlName", + "smithy.api#xmlNamespace", + "smithy.api#timestampFormat", + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel" + ] + }, + "smithy.api#trait": { + "selector": "service [trait|xmlNamespace]" + } + } + }, + "aws.protocols#awsQueryCompatible": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "Enable backward compatibility when migrating from awsQuery to awsJson protocol", + "smithy.api#trait": { + "selector": "service [trait|aws.protocols#awsJson1_0]" + } + } + }, + "aws.protocols#awsQueryError": { + "type": "structure", + "members": { + "code": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The value used to distinguish this error shape during serialization.", + "smithy.api#required": {} + } + }, + "httpResponseCode": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "The HTTP response code used on a response containing this error shape.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Provides the value in the 'Code' distinguishing field and HTTP response\ncode for an operation error.", + "smithy.api#trait": { + "selector": "structure [trait|error]", + "breakingChanges": [ + { + "change": "any" + } + ] + } + } + }, + "aws.protocols#ec2Query": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#deprecated": {}, + "smithy.api#documentation": "An RPC-based protocol that sends 'POST' requests in the body as Amazon EC2\nformatted `x-www-form-urlencoded` strings and responses in XML documents.\nThis protocol does not use HTTP binding traits.", + "smithy.api#protocolDefinition": { + "noInlineDocumentSupport": true, + "traits": [ + "aws.protocols#ec2QueryName", + "smithy.api#xmlAttribute", + "smithy.api#xmlFlattened", + "smithy.api#xmlName", + "smithy.api#xmlNamespace", + "smithy.api#timestampFormat", + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel" + ] + }, + "smithy.api#trait": { + "selector": "service [trait|xmlNamespace]" + } + } + }, + "aws.protocols#ec2QueryName": { + "type": "string", + "traits": { + "smithy.api#documentation": "Indicates the serialized name of a structure member when that structure is\nserialized for the input of an EC2 operation.", + "smithy.api#pattern": "^[a-zA-Z_][a-zA-Z_0-9-]*$", + "smithy.api#trait": { + "selector": "structure > member" + } + } + }, + "aws.protocols#httpChecksum": { + "type": "structure", + "members": { + "requestAlgorithmMember": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Defines a top-level operation input member that is used to configure\nrequest checksum behavior." + } + }, + "requestChecksumRequired": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#documentation": "Indicates an operation requires a checksum in its HTTP request." + } + }, + "requestValidationModeMember": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Defines a top-level operation input member used to opt-in to response\nchecksum validation." + } + }, + "responseAlgorithms": { + "target": "aws.protocols#ChecksumAlgorithmSet", + "traits": { + "smithy.api#documentation": "Defines the checksum algorithms clients should look for when performing\nHTTP response checksum validation." + } + } + }, + "traits": { + "smithy.api#documentation": "Indicates that an operation supports checksum validation.", + "smithy.api#trait": { + "selector": "operation" + }, + "smithy.api#unstable": {} + } + }, + "aws.protocols#restJson1": { + "type": "structure", + "mixins": [ + { + "target": "aws.protocols#HttpConfiguration" + } + ], + "members": {}, + "traits": { + "smithy.api#documentation": "A RESTful protocol that sends JSON in structured payloads.", + "smithy.api#protocolDefinition": { + "traits": [ + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel", + "smithy.api#http", + "smithy.api#httpError", + "smithy.api#httpHeader", + "smithy.api#httpLabel", + "smithy.api#httpPayload", + "smithy.api#httpPrefixHeaders", + "smithy.api#httpQuery", + "smithy.api#httpQueryParams", + "smithy.api#httpResponseCode", + "smithy.api#jsonName", + "smithy.api#timestampFormat" + ] + }, + "smithy.api#trait": { + "selector": "service" + } + } + }, + "aws.protocols#restXml": { + "type": "structure", + "mixins": [ + { + "target": "aws.protocols#HttpConfiguration" + } + ], + "members": { + "noErrorWrapping": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#deprecated": {}, + "smithy.api#documentation": "Disables the serialization wrapping of error properties in an 'Error'\nXML element." + } + } + }, + "traits": { + "smithy.api#deprecated": {}, + "smithy.api#documentation": "A RESTful protocol that sends XML in structured payloads.", + "smithy.api#protocolDefinition": { + "noInlineDocumentSupport": true, + "traits": [ + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel", + "smithy.api#http", + "smithy.api#httpError", + "smithy.api#httpHeader", + "smithy.api#httpLabel", + "smithy.api#httpPayload", + "smithy.api#httpPrefixHeaders", + "smithy.api#httpQuery", + "smithy.api#httpQueryParams", + "smithy.api#httpResponseCode", + "smithy.api#xmlAttribute", + "smithy.api#xmlFlattened", + "smithy.api#xmlName", + "smithy.api#xmlNamespace" + ] + }, + "smithy.api#trait": { + "selector": "service" + } + } + }, + "com.aws#BadRequestError": { + "type": "structure", + "members": { + "message": { + "target": "com.aws#ErrorMessage", + "traits": { + "smithy.api#documentation": "Message with details about the error", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "An error at the fault of the client sending invalid input", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.aws#ErrorMessage": { + "type": "string", + "traits": { + "smithy.api#documentation": "An error message" + } + }, + "com.aws#InternalFailureError": { + "type": "structure", + "members": { + "message": { + "target": "com.aws#ErrorMessage", + "traits": { + "smithy.api#documentation": "Message with details about the error", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "An internal failure at the fault of the server", + "smithy.api#error": "server", + "smithy.api#httpError": 500 + } + }, + "com.aws#Messages": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "com.aws#MyService": { + "type": "service", + "version": "1.0", + "operations": [ + { + "target": "com.aws#SayHello" + } + ], + "errors": [ + { + "target": "com.aws#BadRequestError" + }, + { + "target": "com.aws#InternalFailureError" + }, + { + "target": "com.aws#NotAuthorizedError" + } + ], + "traits": { + "aws.protocols#restJson1": {}, + "smithy.api#documentation": "A sample smithy api" + } + }, + "com.aws#NotAuthorizedError": { + "type": "structure", + "members": { + "message": { + "target": "com.aws#ErrorMessage", + "traits": { + "smithy.api#documentation": "Message with details about the error", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "An error due to the client not being authorized to access the resource", + "smithy.api#error": "client", + "smithy.api#httpError": 403 + } + }, + "com.aws#NotFoundError": { + "type": "structure", + "members": { + "message": { + "target": "com.aws#ErrorMessage", + "traits": { + "smithy.api#documentation": "Message with details about the error", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "An error due to the client attempting to access a missing resource", + "smithy.api#error": "client", + "smithy.api#httpError": 404 + } + }, + "com.aws#SayHello": { + "type": "operation", + "input": { + "target": "com.aws#SayHelloInput" + }, + "output": { + "target": "com.aws#SayHelloOutput" + }, + "errors": [ + { + "target": "com.aws#NotFoundError" + } + ], + "traits": { + "com.aws#handler": { + "language": "typescript" + }, + "smithy.api#http": { + "method": "GET", + "uri": "/hello" + }, + "smithy.api#paginated": { + "inputToken": "inToken", + "outputToken": "outToken", + "items": "messages", + "pageSize": "pageSize" + }, + "smithy.api#readonly": {} + } + }, + "com.aws#SayHelloInput": { + "type": "structure", + "members": { + "name": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpQuery": "name", + "smithy.api#required": {} + } + }, + "pageSize": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#httpHeader": "x-page-size" + } + }, + "inToken": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpQuery": "myInToken" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.aws#SayHelloOutput": { + "type": "structure", + "members": { + "messages": { + "target": "com.aws#Messages", + "traits": { + "smithy.api#required": {} + } + }, + "outToken": { + "target": "smithy.api#String" + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.aws#handler": { + "type": "structure", + "members": { + "language": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The language you will implement the lambda in.\nValid values: typescript, java, python", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Add this trait to an operation to generate a lambda handler stub for the operation.\nYou have configured handler projects for typescript", + "smithy.api#trait": { + "selector": "operation" + } + } + } + } +} diff --git a/packages/type-safe-api/test/resources/smithy/rename-pagination/openapi.json b/packages/type-safe-api/test/resources/smithy/rename-pagination/openapi.json new file mode 100644 index 000000000..eee12ae1a --- /dev/null +++ b/packages/type-safe-api/test/resources/smithy/rename-pagination/openapi.json @@ -0,0 +1,165 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "MyService", + "version": "1.0", + "description": "A sample smithy api" + }, + "paths": { + "/hello": { + "get": { + "operationId": "SayHello", + "parameters": [ + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "myInToken", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "x-page-size", + "in": "header", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "SayHello 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SayHelloResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "403": { + "description": "NotAuthorizedError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotAuthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalFailureError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalFailureErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BadRequestErrorResponseContent": { + "type": "object", + "description": "An error at the fault of the client sending invalid input", + "properties": { + "message": { + "type": "string", + "description": "Message with details about the error" + } + }, + "required": [ + "message" + ] + }, + "InternalFailureErrorResponseContent": { + "type": "object", + "description": "An internal failure at the fault of the server", + "properties": { + "message": { + "type": "string", + "description": "Message with details about the error" + } + }, + "required": [ + "message" + ] + }, + "NotAuthorizedErrorResponseContent": { + "type": "object", + "description": "An error due to the client not being authorized to access the resource", + "properties": { + "message": { + "type": "string", + "description": "Message with details about the error" + } + }, + "required": [ + "message" + ] + }, + "NotFoundErrorResponseContent": { + "type": "object", + "description": "An error due to the client attempting to access a missing resource", + "properties": { + "message": { + "type": "string", + "description": "Message with details about the error" + } + }, + "required": [ + "message" + ] + }, + "SayHelloResponseContent": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "string" + } + }, + "outToken": { + "type": "string" + } + }, + "required": [ + "messages" + ] + } + } + } +} diff --git a/packages/type-safe-api/test/scripts/parser/__snapshots__/parse-openapi-spec.test.ts.snap b/packages/type-safe-api/test/scripts/parser/__snapshots__/parse-openapi-spec.test.ts.snap index 75670fc6c..cd167b254 100644 --- a/packages/type-safe-api/test/scripts/parser/__snapshots__/parse-openapi-spec.test.ts.snap +++ b/packages/type-safe-api/test/scripts/parser/__snapshots__/parse-openapi-spec.test.ts.snap @@ -367,6 +367,190 @@ exports[`Parse OpenAPI Spec Script Unit Tests Injects @handler and @paginated tr } `; +exports[`Parse OpenAPI Spec Script Unit Tests Maps renamed @paginated traits for query and header parameters 1`] = ` +{ + ".api.json": { + "components": { + "schemas": { + "BadRequestErrorResponseContent": { + "description": "An error at the fault of the client sending invalid input", + "properties": { + "message": { + "description": "Message with details about the error", + "type": "string", + }, + }, + "required": [ + "message", + ], + "type": "object", + }, + "InternalFailureErrorResponseContent": { + "description": "An internal failure at the fault of the server", + "properties": { + "message": { + "description": "Message with details about the error", + "type": "string", + }, + }, + "required": [ + "message", + ], + "type": "object", + }, + "NotAuthorizedErrorResponseContent": { + "description": "An error due to the client not being authorized to access the resource", + "properties": { + "message": { + "description": "Message with details about the error", + "type": "string", + }, + }, + "required": [ + "message", + ], + "type": "object", + }, + "NotFoundErrorResponseContent": { + "description": "An error due to the client attempting to access a missing resource", + "properties": { + "message": { + "description": "Message with details about the error", + "type": "string", + }, + }, + "required": [ + "message", + ], + "type": "object", + }, + "SayHelloResponseContent": { + "properties": { + "messages": { + "items": { + "type": "string", + }, + "type": "array", + }, + "outToken": { + "type": "string", + }, + }, + "required": [ + "messages", + ], + "type": "object", + }, + }, + }, + "info": { + "description": "A sample smithy api", + "title": "MyService", + "version": "1.0", + }, + "openapi": "3.0.2", + "paths": { + "/hello": { + "get": { + "operationId": "SayHello", + "parameters": [ + { + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string", + }, + }, + { + "in": "query", + "name": "myInToken", + "schema": { + "type": "string", + }, + }, + { + "in": "header", + "name": "x-page-size", + "schema": { + "format": "int32", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SayHelloResponseContent", + }, + }, + }, + "description": "SayHello 200 response", + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent", + }, + }, + }, + "description": "BadRequestError 400 response", + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotAuthorizedErrorResponseContent", + }, + }, + }, + "description": "NotAuthorizedError 403 response", + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent", + }, + }, + }, + "description": "NotFoundError 404 response", + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalFailureErrorResponseContent", + }, + }, + }, + "description": "InternalFailureError 500 response", + }, + }, + "x-handler": { + "language": "typescript", + }, + "x-paginated": { + "inputToken": "myInToken", + "items": "messages", + "outputToken": "outToken", + "pageSize": "x-page-size", + }, + "x-smithy.api#http": { + "method": "GET", + "uri": "/hello", + }, + "x-smithy.api#readonly": {}, + }, + }, + }, + }, +} +`; + exports[`Parse OpenAPI Spec Script Unit Tests Permits parameter references (and circular references) 1`] = ` { ".api.json": { diff --git a/packages/type-safe-api/test/scripts/parser/parse-openapi-spec.test.ts b/packages/type-safe-api/test/scripts/parser/parse-openapi-spec.test.ts index bffc94ea6..5b16b0c14 100644 --- a/packages/type-safe-api/test/scripts/parser/parse-openapi-spec.test.ts +++ b/packages/type-safe-api/test/scripts/parser/parse-openapi-spec.test.ts @@ -41,6 +41,25 @@ describe("Parse OpenAPI Spec Script Unit Tests", () => { ).toMatchSnapshot(); }); + it("Maps renamed @paginated traits for query and header parameters", () => { + expect( + withTmpDirSnapshot(os.tmpdir(), (tmpDir) => { + const specPath = + "../../resources/smithy/rename-pagination/openapi.json"; + const smithyJsonModelPath = + "../../resources/smithy/rename-pagination/model.json"; + const outputPath = path.join( + path.relative(path.resolve(__dirname), tmpDir), + ".api.json" + ); + const command = `../../../scripts/type-safe-api/parser/parse-openapi-spec --spec-path ${specPath} --output-path ${outputPath} --smithy-json-path ${smithyJsonModelPath}`; + exec(command, { + cwd: path.resolve(__dirname), + }); + }) + ).toMatchSnapshot(); + }); + it("Throws for unsupported request parameter types", () => { withTmpDirSnapshot(os.tmpdir(), (tmpDir) => { const specPath = "../../resources/specs/invalid-request-parameters.yaml";