Skip to content

Commit

Permalink
feat(type-safe-api): coerce request parameters into defined types for…
Browse files Browse the repository at this point in the history
… typescript (#551)

Previously request parameters were always strings no matter the type defined in the model. Now we
add code to the typescript lambda handler wrappers to convert to appropriate types. Supports the
Smithy supported request parameters number/string/bool/date and arrays of those. Responds with a 400
error if the wrong type is passed, and includes headers from any response header interceptors in
this validation response.

BREAKING CHANGE: Removed RequestArrayParameters from input and type signature of
handlers/interceptors. number/bool/date request parameters will be coerced to their type defined in
the model. useIntegerType: true is specified by default for smithy to openapi conversion as this is
recommended and more intuitive behaviour than the default (where integers are still represented as
doubles).
  • Loading branch information
cogwirrel authored Sep 1, 2023
1 parent 09f1358 commit 7804491
Show file tree
Hide file tree
Showing 7 changed files with 1,029 additions and 589 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ export class CloudscapeReactTsWebsiteProject extends ReactTypeScriptProject {
this.addDevDeps("@types/swagger-ui-react");
this.addDeps("swagger-ui-react", "aws4fetch");

tsApi.model.postCompileTask.exec(
`cp .api.json ${path.relative(
tsApi.model.outdir,
this.outdir
)}/public/api.json`
);
const targetApiSpecPath = `${path.relative(
tsApi.model.outdir,
this.outdir
)}/public/api.json`;
tsApi.model.postCompileTask.exec(`rm -f ${targetApiSpecPath}`);
tsApi.model.postCompileTask.exec(`cp .api.json ${targetApiSpecPath}`);
}

private buildSampleDirEntries(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ export const buildTryCatchInterceptor = <TStatus extends number, ErrorResponseBo
errorResponseBody: ErrorResponseBody,
) => async <
RequestParameters,
RequestArrayParameters,
RequestBody,
Response extends OperationResponse<number, any>,
>(
request: ChainedRequestInput<
RequestParameters,
RequestArrayParameters,
RequestBody,
Response
>,
Expand Down Expand Up @@ -77,22 +75,28 @@ const DEFAULT_CORS_HEADERS: { [key: string]: string } = {
* Create an interceptor for adding headers to the response
* @param additionalHeaders headers to add to the response
*/
export const buildResponseHeaderInterceptor = (additionalHeaders: { [key: string]: string }) => async <
RequestParameters,
RequestArrayParameters,
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>,
): Promise<Response> => {
const result = await request.chain.next(request);
return {
...result,
headers: {
...additionalHeaders,
...result.headers,
},
export const buildResponseHeaderInterceptor = (additionalHeaders: { [key: string]: string }) => {
const interceptor = async <
RequestParameters,
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestBody, Response>,
): Promise<Response> => {
const result = await request.chain.next(request);
return {
...result,
headers: {
...additionalHeaders,
...result.headers,
},
};
};

// Any error responses returned during request validation will include the headers
(interceptor as any).__type_safe_api_response_headers = additionalHeaders;

return interceptor;
};

/**
Expand Down Expand Up @@ -124,7 +128,7 @@ export class LoggingInterceptor {
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>,
request: ChainedRequestInput<RequestParameters, RequestBody, Response>,
): Promise<Response> => {
logger.addContext(request.context);
logger.appendKeys({ operationId: request.interceptorContext.operationId });
Expand All @@ -142,7 +146,7 @@ export class LoggingInterceptor {
RequestArrayParameters,
RequestBody,
Response extends OperationResponse<number, any>
>(request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>): Logger => {
>(request: ChainedRequestInput<RequestParameters, RequestBody, Response>): Logger => {
if (!request.interceptorContext.logger) {
throw new Error('No logger found, did you configure the LoggingInterceptor?');
}
Expand Down Expand Up @@ -176,11 +180,10 @@ export interface TracingInterceptorOptions {
*/
export const buildTracingInterceptor = (options?: TracingInterceptorOptions) => async <
RequestParameters,
RequestArrayParameters,
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>,
request: ChainedRequestInput<RequestParameters, RequestBody, Response>,
): Promise<Response> => {
const handler = request.interceptorContext.operationId ?? process.env._HANDLER ?? 'index.handler';
const segment = tracer.getSegment();
Expand Down Expand Up @@ -233,7 +236,7 @@ export class TracingInterceptor {
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>,
request: ChainedRequestInput<RequestParameters, RequestBody, Response>,
): Tracer => {
if (!request.interceptorContext.tracer) {
throw new Error('No tracer found, did you configure the TracingInterceptor?');
Expand Down Expand Up @@ -265,7 +268,7 @@ export class MetricsInterceptor {
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>,
request: ChainedRequestInput<RequestParameters, RequestBody, Response>,
): Promise<Response> => {
metrics.addDimension("operationId", request.interceptorContext.operationId);
request.interceptorContext.metrics = metrics;
Expand All @@ -286,7 +289,7 @@ export class MetricsInterceptor {
RequestBody,
Response extends OperationResponse<number, any>
>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>,
request: ChainedRequestInput<RequestParameters, RequestBody, Response>,
): Metrics => {
if (!request.interceptorContext.metrics) {
throw new Error('No metrics logger found, did you configure the MetricsInterceptor?');
Expand Down
Loading

0 comments on commit 7804491

Please sign in to comment.