Skip to content

Commit

Permalink
fix(core): support endpoint resolver accepting both input and config(#…
Browse files Browse the repository at this point in the history
  • Loading branch information
AllanZhengYP authored Nov 7, 2024
1 parent c12f2e0 commit c86f3ba
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 9 deletions.
72 changes: 66 additions & 6 deletions packages/core/src/clients/internal/composeServiceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,9 +62,9 @@ export const composeServiceApi = <
return async (
config: OptionalizeKey<
TransferHandlerOptions &
ServiceClientOptions &
Partial<DefaultConfig> &
InferEndpointResolverOptionType<DefaultConfig>,
ServiceClientOptions & // Required configs(e.g. endpointResolver, region) to serialize input shapes into requests
InferEndpointResolverOptionType<DefaultConfig> & // Required inputs for endpointResolver
Partial<DefaultConfig>, // Properties defined in default configs, we need to allow overwriting them when invoking the service API handler
DefaultConfig
>,
input: Input,
Expand All @@ -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,
Expand All @@ -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<InputType, InputDefaultsType>;
* OutputType equals to:
* {
* a?: string;
* b?: number;
* c: string;
* }
*/
type OptionalizeKey<InputType, InputDefaultsType> = {
[KeyWithDefaultValue in keyof InputDefaultsType]?: KeyWithDefaultValue extends keyof InputType
? InputType[KeyWithDefaultValue]
Expand All @@ -67,7 +127,7 @@ type OptionalizeKey<InputType, InputDefaultsType> = {
};

type InferEndpointResolverOptionType<T> = T extends {
endpointResolver(options: infer EndpointOptions): any;
endpointResolver(options: infer EndpointOptions, input: any): any;
}
? EndpointOptions
: never;
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit c86f3ba

Please sign in to comment.