├── src ├── libs │ ├── handler-resolver.ts │ ├── api-gateway.ts │ └── lambda.ts ├── model │ ├── User.ts │ ├── Shop.ts │ └── index.ts ├── services │ ├── index.ts │ ├── shop-service.ts │ └── user-service.ts └── functions │ ├── mail │ ├── routes.ts │ └── handler.ts │ ├── shop │ ├── routes.ts │ └── handler.ts │ └── user │ ├── routes.ts │ └── handler.ts ├── tsconfig.paths.json ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── package.json ├── README.md └── serverless.ts /src/libs/handler-resolver.ts: -------------------------------------------------------------------------------- 1 | export const handlerPath = (context: string) => `${context.split(process.cwd())[1].substring(1).replace(/\\/g, '/')}`; 2 | -------------------------------------------------------------------------------- /src/libs/api-gateway.ts: -------------------------------------------------------------------------------- 1 | export const formatJSONResponse = (response: Record) => ({ 2 | statusCode: 200, 3 | body: JSON.stringify(response), 4 | }); 5 | -------------------------------------------------------------------------------- /src/model/User.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | userId: string; 3 | email: string; 4 | isVerified: boolean; 5 | createdAt?: Date; 6 | updatedAt?: Date; 7 | } 8 | 9 | export default User; 10 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@functions/*": ["src/functions/*"], 6 | "@libs/*": ["src/libs/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # esbuild directories 9 | .esbuild 10 | .dynamodb 11 | .nvmrc 12 | 13 | # autogenerated files 14 | swagger 15 | -------------------------------------------------------------------------------- /src/model/Shop.ts: -------------------------------------------------------------------------------- 1 | interface Shop { 2 | shopId: string; 3 | name: string; 4 | address: string; 5 | location: [number, number]; 6 | email: string; 7 | phone: string; 8 | createdAt?: Date; 9 | updatedAt?: Date; 10 | } 11 | 12 | export default Shop; 13 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import dynamoDBClient from '../model'; 2 | import UserService from './user-service'; 3 | import ShopService from './shop-service'; 4 | 5 | const userService = new UserService(dynamoDBClient()); 6 | const shopService = new ShopService(dynamoDBClient()); 7 | 8 | export { 9 | userService, 10 | shopService, 11 | }; 12 | -------------------------------------------------------------------------------- /src/functions/mail/routes.ts: -------------------------------------------------------------------------------- 1 | import { handlerPath } from '@libs/handler-resolver'; 2 | 3 | type Route = { 4 | handler: string; 5 | events: any[]; 6 | } 7 | 8 | export const sendMail: Route = { 9 | /** 10 | * @function: api/src/functions/mail/handler.getAll 11 | */ 12 | handler: `${handlerPath(__dirname)}/handler.sendMail`, 13 | events: [ 14 | { 15 | http: { 16 | method: 'post', 17 | path: 'mail/', 18 | }, 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 3 | 4 | const dynamoDBClient = (): DocumentClient => { 5 | if (process.env.IS_OFFLINE) { 6 | return new AWS.DynamoDB.DocumentClient({ 7 | region: 'localhost', 8 | endpoint: 'http://0.0.0.0:8000', 9 | credentials: { 10 | accessKeyId: 'MockAccessKeyId', 11 | secretAccessKey: 'MockSecretAccessKey', 12 | }, 13 | }); 14 | } 15 | return new AWS.DynamoDB.DocumentClient(); 16 | }; 17 | 18 | export default dynamoDBClient; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "lib": ["ESNext"], 5 | "moduleResolution": "node", 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "target": "ES2020", 11 | "outDir": "lib" 12 | }, 13 | "include": ["src/**/*.ts", "serverless.ts"], 14 | "exclude": [ 15 | "node_modules/**/*", 16 | ".serverless/**/*", 17 | ".webpack/**/*", 18 | "_warmup/**/*", 19 | ".vscode/**/*" 20 | ], 21 | "ts-node": { 22 | "require": ["tsconfig-paths/register"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module', 13 | }, 14 | plugins: [ 15 | '@typescript-eslint', 16 | ], 17 | rules: { 18 | 'import/no-unresolved': 'off', 19 | 'import/extensions': 'off', 20 | 'import/no-import-module-exports': 'off', 21 | 'import/prefer-default-export': 'off', 22 | 'no-useless-constructor': 'off', 23 | 'no-unused-vars': 'off', 24 | 'no-empty-function': 'off', 25 | 'import/no-extraneous-dependencies': 'off', 26 | 'no-extra-semi': 'off', 27 | 'dot-notation': 'off', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/functions/shop/routes.ts: -------------------------------------------------------------------------------- 1 | import { handlerPath } from '@libs/handler-resolver'; 2 | 3 | type Route = { 4 | handler: string; 5 | events: any[]; 6 | } 7 | 8 | export const create: Route = { 9 | /** 10 | * @function: api/src/functions/shop/handler.create 11 | */ 12 | handler: `${handlerPath(__dirname)}/handler.create`, 13 | events: [ 14 | { 15 | http: { 16 | method: 'post', 17 | path: 'shop', 18 | }, 19 | }, 20 | ], 21 | }; 22 | 23 | export const getById: Route = { 24 | /** 25 | * @function: api/src/functions/shop/handler.getById 26 | */ 27 | handler: `${handlerPath(__dirname)}/handler.getById`, 28 | events: [ 29 | { 30 | http: { 31 | method: 'get', 32 | path: 'shop/{shopId}', 33 | }, 34 | }, 35 | ], 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /src/services/shop-service.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 2 | import Shop from '../model/Shop'; 3 | 4 | export default class ShopService { 5 | private TableName: string = 'ShopsTable'; 6 | 7 | constructor(private docClient: DocumentClient) { } 8 | 9 | async create(shop: Shop): Promise { 10 | await this.docClient.put({ 11 | TableName: this.TableName, 12 | Item: { 13 | ...shop, 14 | createdAt: new Date().toISOString(), 15 | updatedAt: new Date().toISOString(), 16 | }, 17 | }).promise(); 18 | 19 | return shop; 20 | } 21 | 22 | async getById(shopId: string): Promise { 23 | const shop = await this.docClient.get({ 24 | TableName: this.TableName, 25 | Key: { 26 | shopId, 27 | }, 28 | }).promise(); 29 | 30 | return shop.Item as Shop; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/libs/lambda.ts: -------------------------------------------------------------------------------- 1 | import middy from '@middy/core'; 2 | import middyJsonBodyParser from '@middy/http-json-body-parser'; 3 | import validator from '@middy/validator'; 4 | import { transpileSchema } from '@middy/validator/transpile'; 5 | 6 | export const middyfy = (schema, handler?) => { 7 | if (typeof schema === 'object') { 8 | return middy(handler).use(middyJsonBodyParser()).use( 9 | validator({ 10 | eventSchema: transpileSchema(schema), 11 | }), 12 | ).use({ 13 | onError: (request) => { 14 | const { error } = request; 15 | 16 | if (error['statusCode'] === 400) { 17 | request.response = { 18 | statusCode: 400, 19 | body: JSON.stringify({ 20 | message: error.message, 21 | validationErrors: error.cause, 22 | }), 23 | }; 24 | } else { 25 | request.response = { 26 | statusCode: 500, 27 | body: JSON.stringify({ 28 | message: error.message, 29 | }), 30 | }; 31 | } 32 | }, 33 | }); 34 | } 35 | return middy(schema).use(middyJsonBodyParser()); 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodeteam", 3 | "version": "1.0.0", 4 | "description": "Lambdas and DynamoDB", 5 | "main": "serverless.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "engines": { 10 | "node": ">=14.15.0" 11 | }, 12 | "dependencies": { 13 | "@aws-sdk/client-ses": "^3.478.0", 14 | "@middy/core": "^3.4.0", 15 | "@middy/http-json-body-parser": "^3.4.0", 16 | "@middy/validator": "^5.1.0", 17 | "aws-lambda": "^1.0.7", 18 | "aws-sdk": "^2.1522.0", 19 | "serverless-auto-swagger": "^2.12.0", 20 | "serverless-dynamodb": "^0.2.47", 21 | "uuid": "^9.0.1" 22 | }, 23 | "devDependencies": { 24 | "@serverless/typescript": "^3.0.0", 25 | "@types/aws-lambda": "^8.10.71", 26 | "@types/node": "^14.14.25", 27 | "@typescript-eslint/eslint-plugin": "^6.15.0", 28 | "@typescript-eslint/parser": "^6.15.0", 29 | "cz-conventional-changelog": "^3.3.0", 30 | "esbuild": "^0.14.11", 31 | "eslint": "^8.56.0", 32 | "eslint-config-airbnb-base": "^15.0.0", 33 | "eslint-plugin-import": "^2.29.1", 34 | "json-schema-to-ts": "^1.5.0", 35 | "serverless": "^3.0.0", 36 | "serverless-esbuild": "^1.23.3", 37 | "serverless-offline": "^13.3.2", 38 | "ts-node": "^10.9.2", 39 | "tsconfig-paths": "^3.9.0", 40 | "typescript": "^4.1.3" 41 | }, 42 | "config": { 43 | "commitizen": { 44 | "path": "./node_modules/cz-conventional-changelog" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/functions/mail/handler.ts: -------------------------------------------------------------------------------- 1 | import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses'; 2 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 3 | import { formatJSONResponse } from '@libs/api-gateway'; 4 | import { middyfy } from '@libs/lambda'; 5 | 6 | const ses = new SESClient({ 7 | region: 'us-west-2', 8 | credentials: { 9 | accessKeyId: 'MockAccessKeyId', 10 | secretAccessKey: 'MockSecretAccessKey', 11 | }, 12 | }); 13 | 14 | type APIGatewayProxyEventWithBody = Omit & { body: TBody }; 15 | 16 | /** 17 | * @function: ./src/functions/mail/handler.sendMail 18 | * @description: Send Mail 19 | * @example: curl -X POST http://localhost:3000/dev/mail -d '{"to": "test@test"}' 20 | */ 21 | const sendMail = middyfy( 22 | { 23 | type: 'object', 24 | required: ['body'], 25 | properties: { 26 | body: { 27 | type: 'object', 28 | required: ['to'], 29 | properties: { 30 | to: { type: 'string', format: 'email' }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | async (event: APIGatewayProxyEventWithBody): Promise => { 36 | const command = new SendEmailCommand({ 37 | Destination: { 38 | ToAddresses: [event.body.to], 39 | }, 40 | Message: { 41 | Body: { 42 | Text: { Data: 'Test' }, 43 | }, 44 | 45 | Subject: { Data: 'Test Email' }, 46 | }, 47 | Source: 'SourceEmailAddress', 48 | }); 49 | 50 | const response = await ses.send(command); 51 | 52 | return formatJSONResponse({ 53 | response, 54 | }); 55 | }, 56 | ); 57 | 58 | module.exports = { 59 | sendMail, 60 | }; 61 | -------------------------------------------------------------------------------- /src/functions/user/routes.ts: -------------------------------------------------------------------------------- 1 | import { handlerPath } from '@libs/handler-resolver'; 2 | 3 | type Route = { 4 | handler: string; 5 | events: any[]; 6 | } 7 | 8 | export const getAll: Route = { 9 | /** 10 | * @function: api/src/functions/user/handler.getAll 11 | */ 12 | handler: `${handlerPath(__dirname)}/handler.getAll`, 13 | events: [ 14 | { 15 | http: { 16 | method: 'get', 17 | path: 'user/', 18 | }, 19 | }, 20 | ], 21 | }; 22 | 23 | export const create: Route = { 24 | /** 25 | * @function: api/src/functions/user/handler.create 26 | */ 27 | handler: `${handlerPath(__dirname)}/handler.create`, 28 | events: [ 29 | { 30 | http: { 31 | method: 'post', 32 | path: 'user', 33 | }, 34 | }, 35 | ], 36 | }; 37 | 38 | export const getById: Route = { 39 | /** 40 | * @function: api/src/functions/user/handler.getById 41 | */ 42 | handler: `${handlerPath(__dirname)}/handler.getById`, 43 | events: [ 44 | { 45 | http: { 46 | method: 'get', 47 | path: 'user/{userId}', 48 | }, 49 | }, 50 | ], 51 | }; 52 | 53 | export const getByEmail: Route = { 54 | /** 55 | * @function: api/src/functions/user/handler.getByEmail 56 | */ 57 | handler: `${handlerPath(__dirname)}/handler.getByEmail`, 58 | events: [ 59 | { 60 | http: { 61 | method: 'get', 62 | path: 'user/email/{email}', 63 | }, 64 | }, 65 | ], 66 | }; 67 | 68 | export const setVerified: Route = { 69 | /** 70 | * @function: api/src/functions/user/handler.setVerified 71 | */ 72 | handler: `${handlerPath(__dirname)}/handler.setVerified`, 73 | events: [ 74 | { 75 | http: { 76 | method: 'patch', 77 | path: 'user/{userId}', 78 | }, 79 | }, 80 | ], 81 | }; 82 | -------------------------------------------------------------------------------- /src/services/user-service.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 2 | import User from '../model/User'; 3 | 4 | export default class UserService { 5 | private TableName: string = 'UsersTable'; 6 | 7 | constructor(private docClient: DocumentClient) { } 8 | 9 | async getAll(): Promise { 10 | const users = await this.docClient.scan({ 11 | TableName: this.TableName, 12 | }).promise(); 13 | 14 | return users.Items as User[]; 15 | } 16 | 17 | async getById(userId: string): Promise { 18 | const user = await this.docClient.get({ 19 | TableName: this.TableName, 20 | Key: { 21 | userId, 22 | }, 23 | }).promise(); 24 | 25 | return user.Item as User; 26 | } 27 | 28 | async getByEmail(email: string): Promise { 29 | const user = await this.docClient.query({ 30 | TableName: this.TableName, 31 | IndexName: 'email-index', 32 | KeyConditionExpression: '#email = :email', 33 | Limit: 1, 34 | ExpressionAttributeNames: { 35 | '#email': 'email', 36 | }, 37 | ExpressionAttributeValues: { 38 | ':email': email, 39 | }, 40 | }).promise(); 41 | 42 | return user.Items; 43 | } 44 | 45 | async setVerified(user: Pick): Promise> { 46 | await this.docClient.update({ 47 | TableName: this.TableName, 48 | Key: { 49 | userId: user.userId, 50 | }, 51 | UpdateExpression: 'set isVerified = :isVerified', 52 | ConditionExpression: 'attribute_exists(userId)', 53 | ExpressionAttributeValues: { 54 | ':isVerified': user.isVerified, 55 | }, 56 | ReturnValues: 'UPDATED_NEW', 57 | }).promise(); 58 | 59 | return user; 60 | } 61 | 62 | async create(user: User): Promise { 63 | await this.docClient.put({ 64 | TableName: this.TableName, 65 | Item: { 66 | ...user, 67 | createdAt: new Date().toISOString(), 68 | updatedAt: new Date().toISOString(), 69 | }, 70 | }).promise(); 71 | 72 | return user; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/functions/shop/handler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { formatJSONResponse } from '@libs/api-gateway'; 4 | import { middyfy } from '@libs/lambda'; 5 | import { shopService } from '../../services'; 6 | 7 | type APIGatewayProxyEventWithBody = Omit & { body: TBody }; 8 | 9 | /** 10 | * @function: ./src/functions/shop/handler.create 11 | * @description: Create a Shop 12 | * @returns: { shop: Shop } 13 | * @example: curl -X POST http://localhost:3000/dev/shop -d '{"email": "test@test"}' 14 | */ 15 | const create = middyfy( 16 | { 17 | type: 'object', 18 | required: ['body'], 19 | properties: { 20 | body: { 21 | type: 'object', 22 | required: ['email', 'phone', 'name', 'address', 'location'], 23 | properties: { 24 | email: { type: 'string', format: 'email' }, 25 | phone: { type: 'string' }, 26 | name: { type: 'string' }, 27 | address: { type: 'string' }, 28 | location: { type: 'array' }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | async (event: APIGatewayProxyEventWithBody): Promise => { 34 | const shop = await shopService.create({ 35 | shopId: uuidv4(), 36 | email: event.body.email, 37 | phone: event.body.phone, 38 | name: event.body.name, 39 | address: event.body.address, 40 | location: event.body.location, 41 | }); 42 | 43 | return formatJSONResponse({ 44 | shop, 45 | }); 46 | }, 47 | ); 48 | 49 | /** 50 | * @function: ./src/functions/shop/handler.getById 51 | * @description: Get a shop by id 52 | * @returns: { shop: Shop } 53 | * @example: curl -X GET http://localhost:3000/dev/shop/123 54 | */ 55 | const getById = middyfy( 56 | { 57 | type: 'object', 58 | required: ['pathParameters'], 59 | properties: { 60 | pathParameters: { 61 | type: 'object', 62 | required: ['shopId'], 63 | properties: { 64 | shopId: { type: 'string', format: 'uuid' }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | async (event: APIGatewayProxyEvent): Promise => { 70 | const shop = await shopService.getById(event.pathParameters.shopId); 71 | 72 | return formatJSONResponse({ 73 | shop, 74 | }); 75 | }, 76 | ); 77 | 78 | module.exports = { 79 | create, 80 | getById, 81 | }; 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | It is a serverless application that uses AWS lambda, dynamodb, and api gateway. It is written in typescript and uses the serverless framework. 3 | 4 | # Setup 5 | ## Prerequisites 6 | - nodejs 7 | - npm 8 | - serverless framework 9 | - aws cli 10 | - aws credentials 11 | - dynamodb local 12 | - dynamodb admin 13 | - dynamodb migrate 14 | - Java Runtime Engine (JRE) version 6.x or newer 15 | 16 | # Features 17 | 18 | - User 19 | - Create a user 20 | - Get a user 21 | - Update a user 22 | - Delete a user 23 | - Shop 24 | - Create a shop 25 | - Get a shop 26 | - Mail - send an email via [AWS SES](https://aws.amazon.com/ses/) 27 | 28 | ## Installation 29 | 30 | Setup npm dependencies 31 | 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | Setup serverless framework 37 | 38 | ```bash 39 | npm install -g serverless 40 | ``` 41 | 42 | Install dynamodb local 43 | 44 | DynamoDb Oflline Plugin Requires: 45 | - serverless@^1 46 | - Java Runtime Engine (JRE) version 6.x or newer 47 | 48 | ```bash 49 | npx sls dynamodb install 50 | ``` 51 | 52 | Install dynamodb admin 53 | 54 | ```bash 55 | npm install -g dynamodb-admin 56 | ``` 57 | after installation, run the following command to start dynamodb admin 58 | ```bash 59 | dynamodb-admin 60 | ``` 61 | _note: admin will be available at http://localhost:8001 62 | 63 | Install dynamodb migrate 64 | 65 | ```bash 66 | npm install -g dynamodb-migrate 67 | ``` 68 | 69 | ## Run 70 | ```bash 71 | npx serverless offline start --stage=dev 72 | ``` 73 | 74 | ## Deploy 75 | ```bash 76 | npx serverless deploy --stage= 77 | ``` 78 | 79 | ## Remove 80 | ```bash 81 | npx serverless remove --stage= 82 | ``` 83 | 84 | # API Documentation 85 | Will be available at http://localhost:3000/swagger 86 | 87 | # Project Structure 88 | ```bash 89 | src 90 | ├── functions 91 | │   ├── mail 92 | │   │   ├── handler.ts 93 | │   │   └── routes.ts 94 | │   ├── shop 95 | │   │   ├── handler.ts 96 | │   │   └── routes.ts 97 | │   └── user 98 | │   ├── handler.ts 99 | │   └── routes.ts 100 | ├── libs 101 | │   ├── api-gateway.ts 102 | │   ├── handler-resolver.ts 103 | │   └── lambda.ts 104 | ├── model 105 | │   ├── Shop.ts 106 | │   ├── User.ts 107 | │   └── index.ts 108 | └── services 109 | ├── index.ts 110 | ├── shop-service.ts 111 | └── user-service.ts 112 | ``` 113 | Each function has its own folder with a handler and routes file. The handler file contains the lambda function and the routes file contains the api gateway routes. 114 | Example of the handler file: 115 | ```typescript 116 | const create = middyfy( 117 | { 118 | type: 'object', 119 | required: ['body'], 120 | properties: { 121 | body: { 122 | type: 'object', 123 | required: ['email'], 124 | properties: { 125 | email: { type: 'string', format: 'email' }, 126 | }, 127 | }, 128 | }, 129 | }, 130 | async (event: APIGatewayProxyEventWithBody): Promise => { 131 | const user = await userService.create({ 132 | email: event.body.email, 133 | userId: uuidv4(), 134 | isVerified: false, 135 | }); 136 | 137 | return formatJSONResponse({ 138 | user, 139 | }); 140 | }, 141 | ); 142 | ``` 143 | `middify` - is a helper function that wraps the lambda function with params validation and error handling. 144 | 145 | # Contributing Guide 146 | ## Branching 147 | - `master` - production branch 148 | - `dev` - development branch 149 | - `feature/` - feature branch 150 | - `bugfix/` - bugfix branch 151 | - `hotfix/` - hotfix branch 152 | - `release/` - release branch 153 | - `docs/` - documentation branch 154 | - `test/` - test branch 155 | - `chore/` - chore branch 156 | - `refactor/` - refactor branch 157 | - `style/` - style branch 158 | - `ci/` - ci branch 159 | 160 | ## Commit Message 161 | ```bash 162 | [optional scope]: 163 | ``` 164 | Example: 165 | ```bash 166 | feat(api): send an email to the customer when a product is shipped 167 | ``` 168 | Commit message should be with the next format - conventionalcommits We are use commitizen for commit message formatting. 169 | -------------------------------------------------------------------------------- /src/functions/user/handler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { formatJSONResponse } from '@libs/api-gateway'; 4 | import { middyfy } from '@libs/lambda'; 5 | import { userService } from '../../services'; 6 | 7 | type APIGatewayProxyEventWithBody = Omit & { body: TBody }; 8 | 9 | /** 10 | * @function: ./src/functions/user/handler.getAll 11 | * @description: Get all users 12 | * @returns: { users: User[] } 13 | * @example: curl -X GET http://localhost:3000/dev/user/ 14 | */ 15 | const getAll = middyfy(async () => { 16 | const users = await userService.getAll(); 17 | 18 | return formatJSONResponse({ 19 | users, 20 | }); 21 | }); 22 | 23 | /** 24 | * @function: ./src/functions/user/handler.create 25 | * @description: Create a user 26 | * @returns: { user: User } 27 | * @example: curl -X POST http://localhost:3000/dev/user -d '{"email": "test@test"}' 28 | */ 29 | const create = middyfy( 30 | { 31 | type: 'object', 32 | required: ['body'], 33 | properties: { 34 | body: { 35 | type: 'object', 36 | required: ['email'], 37 | properties: { 38 | email: { type: 'string', format: 'email' }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | async (event: APIGatewayProxyEventWithBody): Promise => { 44 | const user = await userService.create({ 45 | email: event.body.email, 46 | userId: uuidv4(), 47 | isVerified: false, 48 | }); 49 | 50 | return formatJSONResponse({ 51 | user, 52 | }); 53 | }, 54 | ); 55 | 56 | /** 57 | * @function: ./src/functions/user/handler.getById 58 | * @description: Get a user by id 59 | * @returns: { user: User } 60 | * @example: curl -X GET http://localhost:3000/dev/user/123 61 | */ 62 | const getById = middyfy( 63 | { 64 | type: 'object', 65 | required: ['pathParameters'], 66 | properties: { 67 | pathParameters: { 68 | type: 'object', 69 | required: ['userId'], 70 | properties: { 71 | userId: { type: 'string', format: 'uuid' }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | async (event: APIGatewayProxyEvent): Promise => { 77 | const user = await userService.getById(event.pathParameters.userId); 78 | 79 | return formatJSONResponse({ 80 | user, 81 | }); 82 | }, 83 | ); 84 | 85 | /** 86 | * @function: ./src/functions/user/handler.getByEmail 87 | * @description: Get a user by email 88 | * @returns: { user: User } 89 | * @example: curl -X GET http://localhost:3000/dev/user/email/test@test 90 | */ 91 | const getByEmail = middyfy( 92 | { 93 | type: 'object', 94 | required: ['pathParameters'], 95 | properties: { 96 | pathParameters: { 97 | type: 'object', 98 | required: ['email'], 99 | properties: { 100 | email: { type: 'string', format: 'email' }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | async (event: APIGatewayProxyEvent): Promise => { 106 | const user = await userService.getByEmail(event.pathParameters.email); 107 | 108 | return formatJSONResponse({ 109 | user, 110 | }); 111 | }, 112 | ); 113 | 114 | /** 115 | * @function: ./src/functions/user/handler.update 116 | * @description: Update a user 117 | * @returns: { user: User } 118 | * @example: curl -X PUT http://localhost:3000/dev/user -d '{"email": "test@test"}' 119 | */ 120 | const setVerified = middyfy( 121 | { 122 | type: 'object', 123 | required: ['body'], 124 | properties: { 125 | body: { 126 | type: 'object', 127 | required: ['isVerified'], 128 | properties: { 129 | isVerified: { type: 'boolean' }, 130 | }, 131 | }, 132 | pathParameters: { 133 | type: 'object', 134 | required: ['userId'], 135 | properties: { 136 | userId: { type: 'string', format: 'uuid' }, 137 | }, 138 | }, 139 | }, 140 | }, 141 | async (event: APIGatewayProxyEventWithBody): Promise => { 142 | const user = await userService.setVerified({ 143 | userId: event.pathParameters.userId, 144 | isVerified: event.body.isVerified, 145 | }); 146 | 147 | return formatJSONResponse({ 148 | user, 149 | }); 150 | }, 151 | ); 152 | 153 | module.exports = { 154 | getAll, 155 | create, 156 | getById, 157 | getByEmail, 158 | setVerified, 159 | }; 160 | -------------------------------------------------------------------------------- /serverless.ts: -------------------------------------------------------------------------------- 1 | import type { AWS } from '@serverless/typescript'; 2 | 3 | // User 4 | import { 5 | getAll as usersGetAll, 6 | create as usersCreate, 7 | getById as usersGetById, 8 | getByEmail as usersGetByEmail, 9 | setVerified as setVerifiedUser, 10 | } from '@functions/user/routes'; 11 | 12 | // Shop 13 | import { 14 | create as shopsCreate, 15 | getById as shopsGetById, 16 | } from '@functions/shop/routes'; 17 | 18 | // Mail 19 | import { 20 | sendMail, 21 | } from '@functions/mail/routes'; 22 | 23 | const serverlessConfiguration: AWS = { 24 | service: 'NodeTeam', 25 | frameworkVersion: '3', 26 | plugins: ['serverless-esbuild', 'serverless-dynamodb', 'serverless-auto-swagger', 'serverless-offline'], 27 | provider: { 28 | name: 'aws', 29 | runtime: 'nodejs18.x', 30 | apiGateway: { 31 | minimumCompressionSize: 1024, 32 | shouldStartNameWithService: true, 33 | }, 34 | environment: { 35 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 36 | }, 37 | }, 38 | functions: { 39 | usersGetAll, 40 | usersCreate, 41 | usersGetById, 42 | usersGetByEmail, 43 | setVerifiedUser, 44 | shopsCreate, 45 | shopsGetById, 46 | sendMail, 47 | }, 48 | package: { individually: true }, 49 | custom: { 50 | esbuild: { 51 | bundle: true, 52 | minify: false, 53 | sourcemap: true, 54 | exclude: ['aws-sdk'], 55 | target: 'node18', 56 | define: { 'require.resolve': undefined }, 57 | platform: 'node', 58 | concurrency: 10, 59 | }, 60 | autoswagger: { 61 | title: 'NodeTeam', 62 | basePath: '/dev', 63 | }, 64 | 'serverless-dynamodb': { 65 | start: { 66 | port: 8000, 67 | docker: false, 68 | migrate: true, 69 | inMemory: false, 70 | dbPath: '../.dynamodb', 71 | }, 72 | }, 73 | }, 74 | resources: { 75 | Resources: { 76 | UsersTable: { 77 | Type: 'AWS::DynamoDB::Table', 78 | Properties: { 79 | TableName: 'UsersTable', 80 | AttributeDefinitions: [{ 81 | AttributeName: 'userId', 82 | AttributeType: 'S', 83 | }, { 84 | AttributeName: 'email', 85 | AttributeType: 'S', 86 | }], 87 | KeySchema: [{ 88 | AttributeName: 'userId', 89 | KeyType: 'HASH', 90 | }], 91 | GlobalSecondaryIndexes: [{ 92 | IndexName: 'email-index', 93 | KeySchema: [{ 94 | AttributeName: 'email', 95 | KeyType: 'HASH', 96 | }, { 97 | AttributeName: 'userId', 98 | KeyType: 'RANGE', 99 | }], 100 | AttributeDefinitions: [{ 101 | AttributeName: 'email', 102 | AttributeType: 'S', 103 | }], 104 | Projection: { 105 | ProjectionType: 'ALL', 106 | }, 107 | ProvisionedThroughput: { 108 | ReadCapacityUnits: 1, 109 | WriteCapacityUnits: 1, 110 | }, 111 | }], 112 | ProvisionedThroughput: { 113 | ReadCapacityUnits: 1, 114 | WriteCapacityUnits: 1, 115 | }, 116 | }, 117 | }, 118 | ShopsTable: { 119 | Type: 'AWS::DynamoDB::Table', 120 | Properties: { 121 | TableName: 'ShopsTable', 122 | AttributeDefinitions: [{ 123 | AttributeName: 'shopId', 124 | AttributeType: 'S', 125 | }, { 126 | AttributeName: 'name', 127 | AttributeType: 'S', 128 | }], 129 | KeySchema: [{ 130 | AttributeName: 'shopId', 131 | KeyType: 'HASH', 132 | }], 133 | GlobalSecondaryIndexes: [{ 134 | IndexName: 'name-index', 135 | KeySchema: [{ 136 | AttributeName: 'name', 137 | KeyType: 'HASH', 138 | }, { 139 | AttributeName: 'shopId', 140 | KeyType: 'RANGE', 141 | }], 142 | AttributeDefinitions: [{ 143 | AttributeName: 'name', 144 | AttributeType: 'S', 145 | }], 146 | Projection: { 147 | ProjectionType: 'ALL', 148 | }, 149 | ProvisionedThroughput: { 150 | ReadCapacityUnits: 1, 151 | WriteCapacityUnits: 1, 152 | }, 153 | }], 154 | ProvisionedThroughput: { 155 | ReadCapacityUnits: 1, 156 | WriteCapacityUnits: 1, 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | }; 163 | 164 | module.exports = serverlessConfiguration; 165 | --------------------------------------------------------------------------------