Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLSCMN-3 Documentation #6

Merged
merged 23 commits into from
Nov 26, 2023
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
# @leanstacks/serverless-common

A suite of common components used to compose serverless application components for the LeanStacks organization.

## License

[MIT License](./LICENSE)

## Requirements

This library requires the following:

- Node >= 18.17.x

## Install

To install this library, issue the following command in your AWS Serverless project:

```
npm install @leanstacks/serverless-common
```

## Documentation

For more detailed information regarding specific components see the [documentation](/docs/DOCS.md).
12 changes: 12 additions & 0 deletions docs/DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
:house: [Home](/README.md)

# @leanstacks/serverless-common Documentation

The documentation is organized into sections by module.

## Table of Contents

1. [Handlers](/docs/utils/MIDDYFY.md)
1. [Configuration](/docs/services/CONFIG.md)
1. [DynamoDB Client](/docs/services/DYNAMO.md)
1. [Errors](/docs/errors/ERRORS.md)
151 changes: 151 additions & 0 deletions docs/errors/ERRORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
:house: [Home](/README.md) | :books: [Docs](../DOCS.md)

# Errors

This document describes how to use the family of `Error` classes within the `@leanstacks/serverless-common` package.

## How it works

The `@leanstacks/serverless-common` package provides a family of error classes which may be utilized within AWS Lambda functions to ensure that a consistent response is returned when exception scenarios occur.

## Using the errors

There are two base error classes: `ServiceError` and `HttpError`. Both extend `Error` and, therefore, both have `name` and `message` properties. Additional properties are included to provide more information.

### `ServiceError`

A `ServiceError` may be used in any type of AWS Lambda function. The error has the following properties:

- `name` -- The string "ServiceError".
- `message` -- A string message describing the error.
- `statusCode` -- Optional. A numeric [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
- `code` -- Optional. A numeric value to convey further meaning to the API client regarding the cause of the error

A `ServiceError` may be constructed with either a string or an Error as the first parameter. The optional second parameter is the numeric `code`. And, the optional third parameter is the `statusCode`.

_Example: Throw a `ServiceError` with a message._

```ts
new ServiceError('Object not found in S3 bucket');
```

_Example: Throw a `ServiceError` with an error and code._

```ts
try {
...
} catch (err) {
throw new ServiceError(err, 1001);
}
```

### `HttpError`

A `HttpError` and, more often, it's sub-classes are used in API Gateway Lambda functions. The error has the following properties:

- `name` -- The string "HttpError".
- `message` -- A string message describing the error.
- `statusCode` -- A numeric [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)

A `HttpError` may be constructed with either a string or an Error as the first parameter and a status code as the second parameter.

_Example: Throw a `HttpError` with a message and status code._

```ts
new HttpError('Not found', 404);
```

_Example: Throw a `HttpError` with an error and status code._

```ts
try {
...
} catch (err) {
throw new HttpError(err, 500);
}
```

However, it is more common to use one of the `HttpError` sub-classes:

- `BadRequestError`
- `ForbiddenError`
- `NotFoundError`

### `BadRequestError`

A `BadRequestError` extends `HttpError`. The error `name` is "BadRequestError" and the `statusCode` is 400.

The `BadRequestError` constructor accepts an optional string `message` parameter. When provided, this value is set to the error `message` property, otherwise, the default HTTP status message for HTTP 400 is used.

_Example: Throw a `BadRequestError` with the default message._

```ts
new BadRequestError();
```

_Example: Throw a `BadRequestError` with a custom message._

```ts
try {
...
} catch (err) {
throw new BadRequestError(err.message);
}
```

### `ForbiddenError`

A `ForbiddenError` extends `HttpError`. The error `name` is "ForbiddenError" and the `statusCode` is 403.

The `ForbiddenError` constructor accepts an optional string `message` parameter. When provided, this value is set to the error `message` property, otherwise, the default HTTP status message for HTTP 403 is used.

_Example: Throw a `ForbiddenError` with the default message._

```ts
new ForbiddenError();
```

_Example: Throw a `ForbiddenError` with a custom message._

```ts
try {
...
} catch (err) {
throw new ForbiddenError(err.message);
}
```

### `NotFoundError`

A `NotFoundError` extends `HttpError`. The error `name` is "NotFoundError" and the `statusCode` is 404.

The `NotFoundError` constructor accepts an optional string `message` parameter. When provided, this value is set to the error `message` property, otherwise, the default HTTP status message for HTTP 404 is used.

_Example: Throw a `NotFoundError` with the default message._

```ts
new NotFoundError();
```

_Example: Throw a `NotFoundError` with a custom message._

```ts
try {
...
} catch (err) {
throw new NotFoundError(err.message);
}
```

## Example API client response

When any of the error family of classes is thrown from an API Gateway Lambda function, the `http-error-handler` middleware intercepts the error and, using the information within the error, creates an informative response for the API client. The response body payload is similar to the example below.

```json
{
"name": "NotFoundError",
"message": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.",
"code": 404,
"statusCode": 404
}
```
80 changes: 80 additions & 0 deletions docs/services/CONFIG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
:house: [Home](/README.md) | :books: [Docs](../DOCS.md)

# Configuration

This document describes how to configure a serverless application component leveraging
shared components from the `serverless-common` package.

## How it works

AWS provides configuration to Lambda functions through the `process.env` object, similar to
any Node.js application. The `serverless-common` package provides the means for serverless
components to validate and access a typed configuration object.

AWS supplies a default set of attributes to every Lambda function, e.g. `AWS_REGION`, and
you may define additional custom attributes for your functions.

## Using `LambdaConfig`, the default configuration

When a serverless component does not declare any additional environment variables, but
needs access to the base configuration supplied to all Lambda functions, the
`serverless-common` package provides the `lambdaConfigValues` object of type `LambdaConfig`.
The example below illustrates how to use `lambdaConfigValues`.

Simply import `lambdaConfigValues` into any module which requires access to the configuration.
This ready to use object is of type [`LambdaConfig`](/src/services/config.service.ts).

```ts
// some-component.ts
import { lambdaConfigValues as config } from '@leanstacks/serverless-common';

console.log(`The region is ${config.AWS_REGON}`);
```

## Extending `LambdaConfig` with custom configuration attributes

When a Lambda function has custom configuration attributes, simply extend the `LambdaConfig` type
and [Joi](https://joi.dev/) validation schema. The example below illustrates how to extend `LambdaConfig`.

Create a configuration module in your serverless project. Import the `LambdaConfig` type, the
`lambdaConfigSchema` Joi schema for that type, and the `validateConfig` utility function.

```ts
// my-config.ts
import { LambdaConfig, lambdaConfigSchema, validateConfig } from '@leanstacks/serverless-common';

// extend LambdaConfig
type MyConfig = LambdaConfig & {
TABLE_NAME: string;
QUEUE_NAME: string;
EXPIRES_IN_DAYS: number;
};

// extend lambdaConfigSchema
const myConfigSchema = lambdaConfigSchema.keys({
TABLE_NAME: Joi.string().required(),
QUEUE_NAME: Joi.string().required(),
EXPIRES_IN_DAYS: Joi.number().default(30),
});

// validate and process custom configuration
const config: MyConfig = validateConfig<MyConfig>(myConfigSchema);
export default config;
```

Now the exported `config` object of type `MyConfig` may be used in any other module within the
serverless component. For example...

```ts
// some-component.ts
import config from 'my-config';

console.log(`The table name is ${config.TABLE_NAME}`);
```

## Performance considerations

The configuration attributes are validated by Joi. To ensure that performance is not negatively
impacted, this validation occurs just once, when the configuration module
is first loaded by the module loader. When modules subsequently import the configuration
object, the pre-processed object is loaded without executing the validation process again.
63 changes: 63 additions & 0 deletions docs/services/DYNAMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
:house: [Home](/README.md) | :books: [Docs](../DOCS.md)

# DynamoDB Client Service

This document describes how to use the DynamoDB Client service component, `DynamoService`, to perform actions on the items within a table.

## How it works

The AWS SDK provides components which facilitate operations on DynamoDB tables. These components must be configured and instantiated before use. This boilerplate setup logic is encapsulated within the `DynamoService` component.

## Using `DynamoService`

The `DynamoService` wraps the `DynamoDBClient` and `DynamoDBDocumentClient` from the AWS SDK, exposing the functions which you need to interact with DynamoDB tables. Those functions include:

- batchWrite
- delete
- get
- put
- query
- scan
- update

`DynamoService` leverages the family of `CommandInput` types from the AWS SDK for the function parameters and return types. For example, the signature of the `get` function is:

```ts
get(input: GetCommandInput): Promise<GetCommandOutput>
```

For more information about the CommandInput and CommandOutput objects see the [`@aws-sdk/lib-dynamodb`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/) official documentation.

In the example below, the `find` function accepts an identifier and uses `DynamoService.get` to search the table for the item.

First, we get the `DynamoService` instance. `DynamoService` is a singleton to improve performance across invocations of the Lambda function.

```ts
const dynamoService: DynamoService = DynamoService.getInstance();
```

Then the service is used to fetch an item from the table by the table key.

```ts
async find(taskListId: string): Promise<TaskList | null> {
const output = await this.dynamoService.get({
TableName: config.TASK_LIST_TABLE,
Key: {
taskListId,
},
});

if (output.Item) {
return output.Item as TaskList;
} else {
// not found
return null;
}
}
```

Notice the shape of the input object to the `get` function is the `GetCommandInput` from the AWS SDK. Furthermore, the shape of the returned object is the `GetCommandOutput`. The same pattern applies to the other functions of `DynamoService`.

## Performance considerations

The Dynamo components are constructed and configured once, when the dynamo module is first loaded by the module loader. Furthermore, the components are preserved between function invocations. When other modules subsequently import the DynamoService, the component is loaded without constructing the client components again.
Loading