Skip to content

Commit

Permalink
feat(open-api-gateway): generate a router for using a single lambda f…
Browse files Browse the repository at this point in the history
…unction for all operations (ts)

The generated Typescript API client now includes a 'handlerRouter' which can be used to route
requests to the appropriate handler if users desire to deploy a single shared lambda for all API
integrations. This is type-safe, ensuring all operations are mapped to a handler produced by the
appropriate handler wrapper method. Additionally we supply a convenience method 'Operations.all' to
make it easy to share the same integration for all operations.

re #238
  • Loading branch information
cogwirrel committed Dec 8, 2022
1 parent 9837cd3 commit f4f6b57
Show file tree
Hide file tree
Showing 15 changed files with 1,589 additions and 78 deletions.
56 changes: 55 additions & 1 deletion packages/open-api-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,6 @@ required:
A sample construct is generated which provides a type-safe interface for creating an API Gateway API based on your OpenAPI specification. You'll get a type error if you forget to define an integration for an operation defined in your api.

```ts
import * as path from 'path';
import { Authorizers, Integrations } from '@aws-prototyping-sdk/open-api-gateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
Expand All @@ -553,6 +552,30 @@ export class SampleApi extends Api {
}
```

#### Sharing Integrations

If you would like to use the same integration for every operation, you can use the `Operations.all` method from your generated client to save repeating yourself, for example:

```ts
import { Operations } from 'my-api-typescript-client';
import { Authorizers, Integrations } from '@aws-prototyping-sdk/open-api-gateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { Api } from './api';
export class SampleApi extends Api {
constructor(scope: Construct, id: string) {
super(scope, id, {
defaultAuthorizer: Authorizers.iam(),
// Use the same integration for every operation.
integrations: Operations.all({
integration: Integrations.lambda(new NodejsFunction(scope, 'say-hello')),
}),
});
}
}
```

#### Authorizers

The `Api` construct allows you to define one or more authorizers for securing your API. An integration will use the `defaultAuthorizer` unless an `authorizer` is specified at the integration level. The following authorizers are supported:
Expand Down Expand Up @@ -717,6 +740,37 @@ export const handler = sayHelloHandler(async ({ input }) => {
});
```

##### Handler Router

The lambda handler wrappers can be used in isolation as handler methods for separate lambdas. If you would like to use a single lambda function to serve all requests, you can do so with the `handlerRouter`.

Note that this feature is currently only available in Typescript.

```ts
import { handlerRouter, sayHelloHandler, sayGoodbyeHandler } from "my-api-typescript-client";
import { corsInterceptor } from "./interceptors";
import { sayGoodbye } from "./handlers/say-goodbye";

const sayHello = sayHelloHandler(async ({ input }) => {
return {
statusCode: 200,
body: {
message: `Hello ${input.requestParameters.name}!`,
},
};
});

export const handler = handlerRouter({
// Interceptors declared in this list will apply to all operations
interceptors: [corsInterceptor],
// Assign handlers to each operation here
handlers: {
sayHello,
sayGoodbye,
},
});
```

#### Python

```python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ export const OperationLookup = {
{{/operations}}
};

export class Operations {
/**
* Return an OperationConfig with the same value for every operation
*/
public static all = <T>(value: T): OperationConfig<T> => Object.fromEntries(
Object.keys(OperationLookup).map((operationId) => [operationId, value])
) as unknown as OperationConfig<T>;
}

// Standard apigateway request parameters (query parameters or path parameters, multi or single value)
type ApiGatewayRequestParameters = { [key: string]: string | string[] | undefined };

Expand All @@ -66,8 +75,11 @@ const decodeRequestParameters = (parameters: ApiGatewayRequestParameters): ApiGa
*/
const parseBody = (body: string, demarshal: (body: string) => any, contentTypes: string[]): any => contentTypes.filter((contentType) => contentType !== 'application/json').length === 0 ? demarshal(body || '{}') : body;

type OperationIds ={{#operations}}{{#operation}} | '{{nickname}}'{{/operation}}{{/operations}};
type OperationApiGatewayProxyResult<T extends OperationIds> = APIGatewayProxyResult & { __operationId?: T };

// Api gateway lambda handler type
type ApiGatewayLambdaHandler = (event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>;
type OperationApiGatewayLambdaHandler<T extends OperationIds> = (event: APIGatewayProxyEvent, context: Context) => Promise<OperationApiGatewayProxyResult<T>>;

// Type of the response to be returned by an operation lambda handler
export interface OperationResponse<StatusCode extends number, Body> {
Expand Down Expand Up @@ -187,7 +199,7 @@ export type {{operationIdCamelCase}}ChainedHandlerFunction = ChainedLambdaHandle
export const {{nickname}}Handler = (
firstHandler: {{operationIdCamelCase}}ChainedHandlerFunction,
...remainingHandlers: {{operationIdCamelCase}}ChainedHandlerFunction[]
): ApiGatewayLambdaHandler => async (event: any, context: any): Promise<any> => {
): OperationApiGatewayLambdaHandler<'{{nickname}}'> => async (event: any, context: any, additionalInterceptors: {{operationIdCamelCase}}ChainedHandlerFunction[] = []): Promise<any> => {
const requestParameters = decodeRequestParameters({
...(event.pathParameters || {}),
...(event.queryStringParameters || {}),
Expand All @@ -212,7 +224,7 @@ export const {{nickname}}Handler = (
};
const body = parseBody(event.body, demarshal, [{{^consumes}}'application/json'{{/consumes}}{{#consumes}}{{#mediaType}}'{{{.}}}',{{/mediaType}}{{/consumes}}]) as {{operationIdCamelCase}}RequestBody;

const chain = buildHandlerChain(firstHandler, ...remainingHandlers);
const chain = buildHandlerChain(...additionalInterceptors, firstHandler, ...remainingHandlers);
const response = await chain.next({
input: {
requestParameters,
Expand Down Expand Up @@ -248,3 +260,58 @@ export const {{nickname}}Handler = (
};
{{/operation}}
{{/operations}}

export interface HandlerRouterHandlers {
{{#operations}}
{{#operation}}
readonly {{nickname}}: OperationApiGatewayLambdaHandler<'{{nickname}}'>;
{{/operation}}
{{/operations}}
}

export type AnyOperationRequestParameters = {{#operations}}{{#operation}}| {{operationIdCamelCase}}RequestParameters{{/operation}}{{/operations}};
export type AnyOperationRequestArrayParameters = {{#operations}}{{#operation}}| {{operationIdCamelCase}}RequestArrayParameters{{/operation}}{{/operations}};
export type AnyOperationRequestBodies = {{#operations}}{{#operation}}| {{operationIdCamelCase}}RequestBody{{/operation}}{{/operations}};
export type AnyOperationResponses = {{#operations}}{{#operation}}| {{operationIdCamelCase}}OperationResponses{{/operation}}{{/operations}};

export interface HandlerRouterProps<
RequestParameters,
RequestArrayParameters,
RequestBody,
Response extends AnyOperationResponses
> {
/**
* Interceptors to apply to all handlers
*/
readonly interceptors?: ChainedLambdaHandlerFunction<
RequestParameters,
RequestArrayParameters,
RequestBody,
Response
>[];
/**
* Handlers to register for each operation
*/
readonly handlers: HandlerRouterHandlers;
}

const concatMethodAndPath = (method: string, path: string) => `${method.toLowerCase()}||${path}`;

const OperationIdByMethodAndPath = Object.fromEntries(Object.entries(OperationLookup).map(
([operationId, methodAndPath]) => [concatMethodAndPath(methodAndPath.method, methodAndPath.path), operationId]
));

/**
* Returns a lambda handler which can be used to route requests to the appropriate typed lambda handler function.
*/
export const handlerRouter = (props: HandlerRouterProps<
AnyOperationRequestParameters,
AnyOperationRequestArrayParameters,
AnyOperationRequestBodies,
AnyOperationResponses
>): OperationApiGatewayLambdaHandler<OperationIds> => async (event, context) => {
const operationId = OperationIdByMethodAndPath[concatMethodAndPath(event.requestContext.httpMethod, event.requestContext.resourcePath)];
const handler = props.handlers[operationId];
return handler(event, context, props.interceptors);
};
Loading

0 comments on commit f4f6b57

Please sign in to comment.