Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading