Skip to content

Commit

Permalink
[Boyscout] Makes references more easily available; Adds missing middl…
Browse files Browse the repository at this point in the history
…wares to readme (#46)

* Makes references more easily available; Adds missing middlwares to  readme

* Adds missing index.ts

* Adds middlware to table of contents

* Adds missing description for 'doNotWaitForEmptyEventLoop'

* Adds description about sequential property

* Reorders middlewares in readme; Moves misplaced description
  • Loading branch information
RobinBuschmann authored Apr 9, 2024
1 parent 99fd5e1 commit 50dbda4
Show file tree
Hide file tree
Showing 10 changed files with 537 additions and 216 deletions.
219 changes: 167 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@
- [Testing](#testing)
- [`compose`](#compose)
- [Middlewares](#middlewares)
- [`handleCors`](#handlecors)
- [`inject`](#inject)
- [`parseCookie`](#parsecookie)
- [`doNotWaitForEmptyEventLoop`](#donotwaitforemptyeventloop)
- [`serializeJson`](#serializejson)
- [`parseJson`](#parsejson)
- [`registerHttpErrorHandler`](#registerhttperrorhandler)
- [`sanitizeHeaders`](#sanitizeheaders)
- [`handleCors`](#handlecors)
- [`decodeParameters`](#decodeparameters)
- [`verifyJwt`](#verifyjwt)
- [`verifyXsrfToken`](#verifyxsrftoken)
- [`parseCookie`](#parsecookie)
- [`forEachSqsRecord`](#foreachsqsrecord)
- [Publish](#publish)
<br>

Expand Down Expand Up @@ -117,43 +120,9 @@ handler.actual === actual; // true

## Middlewares

### `handleCors`

`handleCors` is a middleware that creates a preflight response to `OPTIONS` requests and adds CORS headers to any other
request.

> Requires [`sanitizeHeaders`](#sanitizeHeaders) middleware
```typescript
import { sanitizeHeaders } from 'thirty/sanitizeHeaders';
import { handleCors } from 'thirty/handleCors';

export const handler = compose(
eventType<APIGatewayProxyEvent>(),
sanitizeHeaders(),
handleCors(),
)(async event => {
// ...
});
```

#### [`CorsOptions`](src/handleCors/index.ts)

The above example would create an error response that would look like:

```json
{
"statusCode": 400,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"error\":\"Parameter x missing\"}"
}
```

### `inject`

`inject` is a middleware that provides lightweight dependency injection with the possibility of circular dependencies.
`inject` is a middleware that provides lightweight dependency injection.

In order to create a dependency injection container, just define an object, where its properties refer to factory
methods.
Expand All @@ -173,7 +142,7 @@ export const handler = compose(
});
```

Each factory gets a reference to the created dependency container:
Each factory gets access all dependencies defined in the container:

```typescript
export type AuthServiceDeps = { userService: UserService };
Expand Down Expand Up @@ -201,33 +170,85 @@ it('should return created user', async () => {
});
```

### `parseCookie`
### `doNotWaitForEmptyEventLoop`

`parseCookie` is a middleware that parses the event cookie header and extends the event object by a cookie object:
Sets `context.callbackWaitsForEmptyEventLoop` to false.

```typescript
import { parseCookie } from 'thirty/parseCookie';
From [official documentation](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html):
> callbackWaitsForEmptyEventLoop – Set to false to send the response right away when the callback runs, instead of waiting for the Node.js event loop to be empty. If this is false, any outstanding events continue to run during the next invocation.
export const handler = compose(
eventType<{ someType: string }>(),
parseCookie(),
```ts
const handler = compose(
types<APIGatewayEvent, Promise<APIGatewayProxyResult>>(),
doNotWaitForEmptyEventLoop(),
)(async event => {
event.cookie;

});
```

### `parseJson`

`parseJson` is a middleware that parses the event body and extends the event object by a `jsonBody` object:
`parseJson` is a middleware that parses the request body and extends the event object by a `jsonBody` object:

```typescript
import { parseCookie } from 'thirty/parseCookie';
import { compose, types, of } from 'thirty/core';
import { parseJson } from 'thirty/parseJson';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

type SomeDto = { description: string };

export const handler = compose(
eventType<{ someType: string }>(),
parseJson(),
types<APIGatewayProxyEvent, Promise<APIGatewayProxyResult>>(),
parseJson(of<SomeDto>),
)(async event => {
const { description } = event.jsonBody;

return {
statusCode: 200,
body: JSON.stringify({ id: uuid(), description }),
};
});
```

### `serializeJson`

Before that middleware you had to serialize your response body's manually and parse it back again in your tests in order to assert response bodys - especially partially.
```ts
type User = {id: string; name: string};
```
```ts
const handler = compose(
types<APIGatewayEvent, Promise<APIGatewayProxyResult>>(),
)(async event => {
return {
statusCode: 200,
body: JSON.stringify({id: 'USER_1', name: 'Marty'} as User),
};
});

const response = await handler.actual({...});
expect(response.statusCode).toEqual(200);
expect(JSON.parse(response.body)).toEqual(expect.objectContaining({
id: 'USER_1'
}));

```
But `serializeJson` makes type-safe and testing less verbose:
```ts
const handler = compose(
types<APIGatewayEvent, Promise<APIGatewayProxyResult>>(),
serializeJson(of<User>),
)(async event => {
event.jsonBody;
return {
statusCode: 200,
body: {id: 'USER_1', name: 'Marty'},
};
});

const response = await handler.actual({...});
expect(response).toEqual({
statusCode: 200,
body: expect.objectContaining({ id: 'USER_1' })
});
```

Expand All @@ -249,6 +270,17 @@ export const handler = compose(
throw new BadRequestError('Parameter x missing');
});
```
The above example would create an error response that would look like:

```json
{
"statusCode": 400,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"error\":\"Parameter x missing\"}"
}
```

#### [`HttpErrorHandlerOptions`](src/registerHttpErrorHandler/index.ts)

Expand All @@ -269,6 +301,28 @@ export const handler = compose(
});
```

### `handleCors`

`handleCors` is a middleware that creates a preflight response to `OPTIONS` requests and adds CORS headers to any other
request.

> Requires [`sanitizeHeaders`](#sanitizeHeaders) middleware
```typescript
import { sanitizeHeaders } from 'thirty/sanitizeHeaders';
import { handleCors } from 'thirty/handleCors';

export const handler = compose(
eventType<APIGatewayProxyEvent>(),
sanitizeHeaders(),
handleCors(),
)(async event => {
// ...
});
```

#### [`CorsOptions`](src/handleCors/index.ts)

### `decodeParameters`

`decodeParameters` is a middleware that decodes all parameter values with `decodeURIComponent` and stores them in
Expand All @@ -279,7 +333,7 @@ import { decodeParameters } from 'thirty/decodeParameters';

export const handler = compose(
eventType<{ someType: string }>(),
decodeParameters(),
decodeParameters(),
)(async event => {
event.decodeParameters;
event.decodedQueryParameters;
Expand Down Expand Up @@ -355,6 +409,67 @@ export const handler = compose(
});
```

### `parseCookie`

`parseCookie` is a middleware that parses the event cookie header and extends the event object by a `cookie` object:

```typescript
import { parseCookie } from 'thirty/parseCookie';
export const handler = compose(
eventType<{ someType: string }>(),
parseCookie(),
)(async event => {
event.cookie;
});
```

### `forEachSqsRecord`

Consider the following setup not using that middleware:
```ts
type SomeMesssage = {id: string; text: string};
```
```ts
const handler = compose(
types<SQSEvent, Promise<SQSBatchResponse>>(),
)(async event => {
return {
batchItemFailures: (
await Promise.all(
event.Records.map((record) => {
try {
const message: SomeMessage = JSON.parse(record.body);
// process message
} catch (e) {
return {
itemIdentifier: record.messageId,
};
}
}),
)
).filter((maybeItemFailure): maybeItemFailure is SQSBatchItemFailure => !!maybeItemFailure),
};
});
```
You have to do a lot of boilerplate code, which makes the actual business code of processing one message hard to read. `forEachSqsRecord` lets you process one message without any of that boilerplate:
```ts
const handler = compose(
types<SQSEvent, Promise<SQSBatchResponse>>(),
forEachSqsRecord({
batchItemFailures: true,
bodyType: of<SomeMessage>,
})
)(async event => {
const message = event.record.body;
// process message
});
```

Use `sequential` set to `true` in order to iterate over the records in order. If one record fails to be processed, the
processing of any upcoming records will be stopped, stopped too. If `batchItemFailures` is also set to `true`,
all unprocessed records will be added to list of `batchItemFailures`.

## Publish

In order to publish a new version to npm, create a new release on github.
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './compose';
export * from './Next';
export * from './eventType';
export * from './Middleware';
export * from './TypeRef';
Loading

0 comments on commit 50dbda4

Please sign in to comment.