From c86f3ba2ccba4310531de5cb25bc0f6a78082b79 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Thu, 7 Nov 2024 10:45:57 -0800 Subject: [PATCH] fix(core): support endpoint resolver accepting both input and config(#13985) --- .../src/clients/internal/composeServiceApi.ts | 72 +++++++++++++++++-- .../s3/utils/client/s3control/base.ts | 5 +- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/packages/core/src/clients/internal/composeServiceApi.ts b/packages/core/src/clients/internal/composeServiceApi.ts index 259a0ee7cd6..4b788ec852d 100644 --- a/packages/core/src/clients/internal/composeServiceApi.ts +++ b/packages/core/src/clients/internal/composeServiceApi.ts @@ -5,6 +5,42 @@ import { ServiceClientOptions } from '../types/aws'; import { Endpoint, TransferHandler } from '../types/core'; import { HttpRequest, HttpResponse } from '../types/http'; +/** + * Compose a service API handler that accepts input as defined shape and responds conforming to defined output shape. + * A service API handler is composed with: + * * A transfer handler + * * A serializer function + * * A deserializer function + * * A default config object + * + * The returned service API handler, when called, will trigger the following workflow: + * 1. When calling the service API handler function, the default config object is merged into the input config + * object to assign the default values of some omitted configs, resulting to a resolved config object. + * 2. The `endpointResolver` function from the default config object will be invoked with the resolved config object and + * API input object resulting to an endpoint instance. + * 3. The serializer function is invoked with API input object and the endpoint instance resulting to an HTTP request + * instance. + * 4. The HTTP request instance and the resolved config object is passed to the transfer handler function. + * 5. The transfer handler function resolves to an HTTP response instance(can be either successful or failed status code). + * 6. The deserializer function is invoked with the HTTP response instance resulting to the API output object, and + * return to the caller. + * + * + * @param transferHandler Async function for dispatching HTTP requests and returning HTTP response. + * @param serializer Async function for converting object in defined input shape into HTTP request targeting a given + * endpoint. + * @param deserializer Async function for converting HTTP response into output object in defined output shape, or error + * shape. + * @param defaultConfig object containing default options to be consumed by transfer handler, serializer and + * deserializer. + * @returns a async service API handler function that accepts a config object and input object in defined shape, returns + * an output object in defined shape. It may also throw error instance in defined shape in deserializer. The config + * object type is composed with options type of transferHandler, endpointResolver function as well as endpointResolver + * function's input options type, region string. The config object property will be marked as optional if it's also + * defined in defaultConfig. + * + * @internal + */ export const composeServiceApi = < TransferHandlerOptions, Input, @@ -26,9 +62,9 @@ export const composeServiceApi = < return async ( config: OptionalizeKey< TransferHandlerOptions & - ServiceClientOptions & - Partial & - InferEndpointResolverOptionType, + ServiceClientOptions & // Required configs(e.g. endpointResolver, region) to serialize input shapes into requests + InferEndpointResolverOptionType & // Required inputs for endpointResolver + Partial, // Properties defined in default configs, we need to allow overwriting them when invoking the service API handler DefaultConfig >, input: Input, @@ -37,8 +73,8 @@ export const composeServiceApi = < ...defaultConfig, ...config, } as unknown as TransferHandlerOptions & ServiceClientOptions; - // We may want to allow different endpoints from given config(other than region) and input. - // Currently S3 supports additional `useAccelerateEndpoint` option to use accelerate endpoint. + // We need to allow different endpoints based on both given config(other than region) and input. + // However for most of non-S3 services, region is the only input for endpoint resolver. const endpoint = await resolvedConfig.endpointResolver( resolvedConfig, input, @@ -55,6 +91,30 @@ export const composeServiceApi = < }; }; +/** + * Type helper to make a given key optional in a given type. For all the keys in the `InputDefaultsType`, if its value + * is assignable to the value of the same key in `InputType`, we will mark the key in `InputType` is optional. If + * the `InputType` and `InputDefaultsType` has the same key but un-assignable types, the resulting type is `never` to + * trigger a type error down the line. + * + * @example + * type InputType = { + * a: string; + * b: number; + * c: string; + * }; + * type InputDefaultsType = { + * a: string; + * b: number; + * }; + * type OutputType = OptionalizeKey; + * OutputType equals to: + * { + * a?: string; + * b?: number; + * c: string; + * } + */ type OptionalizeKey = { [KeyWithDefaultValue in keyof InputDefaultsType]?: KeyWithDefaultValue extends keyof InputType ? InputType[KeyWithDefaultValue] @@ -67,7 +127,7 @@ type OptionalizeKey = { }; type InferEndpointResolverOptionType = T extends { - endpointResolver(options: infer EndpointOptions): any; + endpointResolver(options: infer EndpointOptions, input: any): any; } ? EndpointOptions : never; diff --git a/packages/storage/src/providers/s3/utils/client/s3control/base.ts b/packages/storage/src/providers/s3/utils/client/s3control/base.ts index 455c9ac4cbc..721ec7e3b9a 100644 --- a/packages/storage/src/providers/s3/utils/client/s3control/base.ts +++ b/packages/storage/src/providers/s3/utils/client/s3control/base.ts @@ -51,11 +51,10 @@ export type S3EndpointResolverOptions = EndpointResolverOptions & { */ const endpointResolver = ( options: S3EndpointResolverOptions, - apiInput?: { AccountId: string }, + apiInput: { AccountId: string }, ) => { const { region, customEndpoint } = options; - // TODO(ashwinkumar6): make accountId a required param - const { AccountId: accountId } = apiInput ?? {}; + const { AccountId: accountId } = apiInput; let endpoint: URL; if (customEndpoint) {