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

fix(api-cors-lambda-crud-dynamodb example): Implement use of NodejsFunction, replaced deprecated API #468

Merged
merged 6 commits into from
Jul 10, 2021
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
16 changes: 9 additions & 7 deletions typescript/api-cors-lambda-crud-dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ npm install
npm run build
```

This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template.
This will install the necessary CDK, then this example's dependencies, then the lambda functions' dependencies, and then build your TypeScript files and your CloudFormation template.

## Deploy

Expand All @@ -36,11 +36,11 @@ After the deployment you will see the API's URL, which represents the url you ca
The whole component contains:

- An API, with CORS enabled on all HTTTP Methods. (Use with caution, for production apps you will want to enable only a certain domain origin to be able to query your API.)
- Lambda pointing to `src/create.ts`, containing code for __storing__ an item into the DynamoDB table.
- Lambda pointing to `src/delete-one.ts`, containing code for __deleting__ an item from the DynamoDB table.
- Lambda pointing to `src/get-all.ts`, containing code for __getting all items__ from the DynamoDB table.
- Lambda pointing to `src/get-one.ts`, containing code for __getting an item__ from the DynamoDB table.
- Lambda pointing to `src/update-one.ts`, containing code for __updating an item__ in the DynamoDB table.
- Lambda pointing to `lambdas/create.ts`, containing code for __storing__ an item into the DynamoDB table.
- Lambda pointing to `lambdas/delete-one.ts`, containing code for __deleting__ an item from the DynamoDB table.
- Lambda pointing to `lambdas/get-all.ts`, containing code for __getting all items__ from the DynamoDB table.
- Lambda pointing to `lambdas/get-one.ts`, containing code for __getting an item__ from the DynamoDB table.
- Lambda pointing to `lambdas/update-one.ts`, containing code for __updating an item__ in the DynamoDB table.
- A DynamoDB table `items` that stores the data.
- Five `LambdaIntegrations` that connect these Lambdas to the API.

Expand All @@ -49,8 +49,9 @@ The whole component contains:
The [`cdk.json`](./cdk.json) file in the root of this repository includes
instructions for the CDK toolkit on how to execute this program.

After building your TypeScript code, you will be able to run the CDK toolkits commands as usual:
After building your TypeScript code, you will be able to run the CDK toolkit commands as usual:

```bash
$ cdk ls
<list all stacks in this program>

Expand All @@ -62,3 +63,4 @@ After building your TypeScript code, you will be able to run the CDK toolkits co

$ cdk diff
<shows diff against deployed stack>
```
2 changes: 1 addition & 1 deletion typescript/api-cors-lambda-crud-dynamodb/cdk.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"app": "node index"
"app": "index.js"
}
138 changes: 65 additions & 73 deletions typescript/api-cors-lambda-crud-dynamodb/index.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,100 @@
import apigateway = require('@aws-cdk/aws-apigateway');
import dynamodb = require('@aws-cdk/aws-dynamodb');
import lambda = require('@aws-cdk/aws-lambda');
import cdk = require('@aws-cdk/core');

export class ApiLambdaCrudDynamoDBStack extends cdk.Stack {
constructor(app: cdk.App, id: string) {
import { IResource, LambdaIntegration, MockIntegration, PassthroughBehavior, RestApi } from '@aws-cdk/aws-apigateway';
import { AttributeType, Table } from '@aws-cdk/aws-dynamodb';
import { Runtime } from '@aws-cdk/aws-lambda';
import { App, Stack, RemovalPolicy } from '@aws-cdk/core';
import { NodejsFunction, NodejsFunctionProps } from '@aws-cdk/aws-lambda-nodejs';
import { join } from 'path'

export class ApiLambdaCrudDynamoDBStack extends Stack {
constructor(app: App, id: string) {
super(app, id);

const dynamoTable = new dynamodb.Table(this, 'items', {
const dynamoTable = new Table(this, 'items', {
partitionKey: {
name: 'itemId',
type: dynamodb.AttributeType.STRING
type: AttributeType.STRING
},
tableName: 'items',

// The default removal policy is RETAIN, which means that cdk destroy will not attempt to delete
// the new table, and it will remain in your account until manually deleted. By setting the policy to
// DESTROY, cdk destroy will delete the table (even if it has data in it)
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
/**
* The default removal policy is RETAIN, which means that cdk destroy will not attempt to delete
* the new table, and it will remain in your account until manually deleted. By setting the policy to
* DESTROY, cdk destroy will delete the table (even if it has data in it)
*/
removalPolicy: RemovalPolicy.DESTROY, // NOT recommended for production code
});

const getOneLambda = new lambda.Function(this, 'getOneItemFunction', {
code: new lambda.AssetCode('src'),
handler: 'get-one.handler',
runtime: lambda.Runtime.NODEJS_10_X,
const nodeJsFunctionProps: NodejsFunctionProps = {
bundling: {
externalModules: [
'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
],
},
depsLockFilePath: join(__dirname, 'lambdas', 'package-lock.json'),
environment: {
PRIMARY_KEY: 'itemId',
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: 'itemId'
}
});
},
runtime: Runtime.NODEJS_14_X,
}

const getAllLambda = new lambda.Function(this, 'getAllItemsFunction', {
code: new lambda.AssetCode('src'),
handler: 'get-all.handler',
runtime: lambda.Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: 'itemId'
}
// Create a Lambda function for each of the CRUD operations
const getOneLambda = new NodejsFunction(this, 'getOneItemFunction', {
entry: join(__dirname, 'lambdas', 'get-one.ts'),
...nodeJsFunctionProps,
});

const createOne = new lambda.Function(this, 'createItemFunction', {
code: new lambda.AssetCode('src'),
handler: 'create.handler',
runtime: lambda.Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: 'itemId'
}
const getAllLambda = new NodejsFunction(this, 'getAllItemsFunction', {
entry: join(__dirname, 'lambdas', 'get-all.ts'),
...nodeJsFunctionProps,
});

const updateOne = new lambda.Function(this, 'updateItemFunction', {
code: new lambda.AssetCode('src'),
handler: 'update-one.handler',
runtime: lambda.Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: 'itemId'
}
const createOneLambda = new NodejsFunction(this, 'createItemFunction', {
entry: join(__dirname, 'lambdas', 'create.ts'),
...nodeJsFunctionProps,
});

const deleteOne = new lambda.Function(this, 'deleteItemFunction', {
code: new lambda.AssetCode('src'),
handler: 'delete-one.handler',
runtime: lambda.Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: 'itemId'
}
const updateOneLambda = new NodejsFunction(this, 'updateItemFunction', {
entry: join(__dirname, 'lambdas', 'update-one.ts'),
...nodeJsFunctionProps,
});

const deleteOneLambda = new NodejsFunction(this, 'deleteItemFunction', {
entry: join(__dirname, 'lambdas', 'delete-one.ts'),
...nodeJsFunctionProps,
});

// Grant the Lambda function read access to the DynamoDB table
dynamoTable.grantReadWriteData(getAllLambda);
dynamoTable.grantReadWriteData(getOneLambda);
dynamoTable.grantReadWriteData(createOne);
dynamoTable.grantReadWriteData(updateOne);
dynamoTable.grantReadWriteData(deleteOne);
dynamoTable.grantReadWriteData(createOneLambda);
dynamoTable.grantReadWriteData(updateOneLambda);
dynamoTable.grantReadWriteData(deleteOneLambda);

// Integrate the Lambda functions with the API Gateway resource
const getAllIntegration = new LambdaIntegration(getAllLambda);
const createOneIntegration = new LambdaIntegration(createOneLambda);
const getOneIntegration = new LambdaIntegration(getOneLambda);
const updateOneIntegration = new LambdaIntegration(updateOneLambda);
const deleteOneIntegration = new LambdaIntegration(deleteOneLambda);

const api = new apigateway.RestApi(this, 'itemsApi', {

// Create an API Gateway resource for each of the CRUD operations
const api = new RestApi(this, 'itemsApi', {
restApiName: 'Items Service'
});

const items = api.root.addResource('items');
const getAllIntegration = new apigateway.LambdaIntegration(getAllLambda);
items.addMethod('GET', getAllIntegration);

const createOneIntegration = new apigateway.LambdaIntegration(createOne);
items.addMethod('POST', createOneIntegration);
addCorsOptions(items);

const singleItem = items.addResource('{id}');
const getOneIntegration = new apigateway.LambdaIntegration(getOneLambda);
singleItem.addMethod('GET', getOneIntegration);

const updateOneIntegration = new apigateway.LambdaIntegration(updateOne);
singleItem.addMethod('PATCH', updateOneIntegration);

const deleteOneIntegration = new apigateway.LambdaIntegration(deleteOne);
singleItem.addMethod('DELETE', deleteOneIntegration);
addCorsOptions(singleItem);
}
}

export function addCorsOptions(apiResource: apigateway.IResource) {
apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({
export function addCorsOptions(apiResource: IResource) {
apiResource.addMethod('OPTIONS', new MockIntegration({
integrationResponses: [{
statusCode: '200',
responseParameters: {
Expand All @@ -112,7 +104,7 @@ export function addCorsOptions(apiResource: apigateway.IResource) {
'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
},
}],
passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": "{\"statusCode\": 200}"
},
Expand All @@ -124,11 +116,11 @@ export function addCorsOptions(apiResource: apigateway.IResource) {
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Credentials': true,
'method.response.header.Access-Control-Allow-Origin': true,
},
},
}]
})
}

const app = new cdk.App();
const app = new App();
new ApiLambdaCrudDynamoDBStack(app, 'ApiLambdaCrudDynamoDBExample');
app.synth();
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
const uuidv4 = require('uuid/v4');
import * as AWS from 'aws-sdk';
ryparker marked this conversation as resolved.
Show resolved Hide resolved
import { v4 as uuidv4 } from 'uuid';

const TABLE_NAME = process.env.TABLE_NAME || '';
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';

const db = new AWS.DynamoDB.DocumentClient();

const RESERVED_RESPONSE = `Error: You're using AWS reserved keywords as attributes`,
DYNAMODB_EXECUTION_ERROR = `Error: Execution update, caused a Dynamodb error, please take a look at your CloudWatch Logs.`;

export const handler = async (event: any = {}) : Promise <any> => {
export const handler = async (event: any = {}): Promise<any> => {

if (!event.body) {
return { statusCode: 400, body: 'invalid request, you are missing the parameter body' };
Expand All @@ -24,7 +26,7 @@ export const handler = async (event: any = {}) : Promise <any> => {
return { statusCode: 201, body: '' };
} catch (dbError) {
const errorResponse = dbError.code === 'ValidationException' && dbError.message.includes('reserved keyword') ?
DYNAMODB_EXECUTION_ERROR : RESERVED_RESPONSE;
DYNAMODB_EXECUTION_ERROR : RESERVED_RESPONSE;
return { statusCode: 500, body: errorResponse };
}
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
import * as AWS from 'aws-sdk';

const TABLE_NAME = process.env.TABLE_NAME || '';
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';

export const handler = async (event: any = {}) : Promise <any> => {
const db = new AWS.DynamoDB.DocumentClient();

export const handler = async (event: any = {}): Promise<any> => {

const requestedItemId = event.pathParameters.id;
if (!requestedItemId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
import * as AWS from 'aws-sdk';

const TABLE_NAME = process.env.TABLE_NAME || '';

export const handler = async () : Promise <any> => {
const db = new AWS.DynamoDB.DocumentClient();

export const handler = async (): Promise<any> => {

const params = {
TableName: TABLE_NAME
Expand All @@ -12,6 +14,6 @@ export const handler = async () : Promise <any> => {
const response = await db.scan(params).promise();
return { statusCode: 200, body: JSON.stringify(response.Items) };
} catch (dbError) {
return { statusCode: 500, body: JSON.stringify(dbError)};
return { statusCode: 500, body: JSON.stringify(dbError) };
}
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
import * as AWS from 'aws-sdk';

const TABLE_NAME = process.env.TABLE_NAME || '';
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';

export const handler = async (event: any = {}) : Promise <any> => {
const db = new AWS.DynamoDB.DocumentClient();

export const handler = async (event: any = {}): Promise<any> => {

const requestedItemId = event.pathParameters.id;
if (!requestedItemId) {
Expand Down
15 changes: 15 additions & 0 deletions typescript/api-cors-lambda-crud-dynamodb/lambdas/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "lambda-crud-dynamodb",
"version": "1.0.0",
"description": "Lambdas to do CRUD operations on DynamoDB",
"private": true,
"license": "MIT",
"devDependencies": {
"@types/node": "*",
"@types/uuid": "*"
},
"dependencies": {
"aws-sdk": "*",
"uuid": "*"
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange this should have a newline....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason I cannot see the newline in GitHub's review UI. However when I click "edit file" I see there is already a new line. LMK if you don't see it during your review @NGL321.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some digging into this and it looks like every file does have a newline char added however you'll only see it in your local editor or when editing the file via GitHub's "edit file" UI. If you want the newline to render in GitHubs "view file" view, then you'll have to add 2 newlines to the bottom of the file.

Is this alright as is? @NGL321

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"include": [
"*.ts"
]
}
Loading