├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── billing.js ├── create.js ├── delete.js ├── get.js ├── libs ├── billing-lib.js ├── debug-lib.js ├── dynamodb-lib.js ├── handler-lib.js └── response-lib.js ├── list.js ├── mocks ├── billing-event.json ├── create-event.json ├── delete-event.json ├── get-event.json ├── list-event.json └── update-event.json ├── package-lock.json ├── package.json ├── resources ├── api-gateway-errors.yml ├── cognito-identity-pool.yml ├── cognito-user-pool.yml ├── dynamodb-table.yml └── s3-bucket.yml ├── serverless.yml ├── tests └── billing.test.js └── update.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # vim 40 | .*.sw* 41 | Session.vim 42 | 43 | # Serverless 44 | .webpack 45 | .serverless 46 | 47 | # env 48 | env.yml 49 | .env 50 | 51 | # Jetbrains IDEs 52 | .idea 53 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anomaly Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Stack Demo API [![Seed Status](https://api.seed.run/serverless-stack/serverless-stack-demo-api/stages/prod/build_badge)](https://console.seed.run/serverless-stack/serverless-stack-demo-api) 2 | 3 | [Serverless Stack](http://serverless-stack.com) is a free comprehensive guide to creating full-stack serverless applications. We create a [note taking app](http://demo2.serverless-stack.com) from scratch. 4 | 5 | The main part of the guide uses [SST](https://github.com/serverless-stack/serverless-stack). We also have an alternative version that uses Serverless Framework. This repo is the source for the Serverless Framework version of the backend. There's a [frontend React client that connects to this as well](https://github.com/AnomalyInnovations/serverless-stack-demo-client). 6 | 7 | #### Usage 8 | 9 | To use this repo locally you need to have the [Serverless framework](https://serverless.com) installed. 10 | 11 | ``` bash 12 | $ npm install serverless -g 13 | ``` 14 | 15 | Clone this repo and install the NPM packages. 16 | 17 | ``` bash 18 | $ git clone https://github.com/AnomalyInnovations/serverless-stack-demo-api 19 | $ npm install 20 | ``` 21 | 22 | Run a single API on local. 23 | 24 | ``` bash 25 | $ serverless invoke local --function list --path event.json 26 | ``` 27 | 28 | Where, `event.json` contains the request event info and looks something like this. 29 | 30 | ``` json 31 | { 32 | "requestContext": { 33 | "authorizer": { 34 | "claims": { 35 | "sub": "USER-SUB-1234" 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | Finally, run this to deploy to your AWS account. 43 | 44 | ``` bash 45 | $ serverless deploy 46 | ``` 47 | 48 | This project refers to an `.env` file for secret environment variables that are not checking in to the repo. Make sure to create one before deploying - https://serverless-stack.com/chapters/load-secrets-from-env.html. 49 | 50 | --- 51 | 52 | This repo is maintained by [Anomaly Innovations](https://anoma.ly); makers of [Seed](https://seed.run) and [Serverless Stack](https://serverless-stack.com). 53 | 54 | [Email]: mailto:contact@anoma.ly 55 | -------------------------------------------------------------------------------- /billing.js: -------------------------------------------------------------------------------- 1 | import stripePackage from "stripe"; 2 | import handler from "./libs/handler-lib"; 3 | import { calculateCost } from "./libs/billing-lib"; 4 | 5 | export const main = handler(async (event, context) => { 6 | const { storage, source } = JSON.parse(event.body); 7 | const amount = calculateCost(storage); 8 | const description = "Scratch charge"; 9 | 10 | // Load our secret key from the environment variables 11 | const stripe = stripePackage(process.env.stripeSecretKey); 12 | 13 | await stripe.charges.create({ 14 | source, 15 | amount, 16 | description, 17 | currency: "usd" 18 | }); 19 | return { status: true }; 20 | }); 21 | -------------------------------------------------------------------------------- /create.js: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | import handler from "./libs/handler-lib"; 3 | import dynamoDb from "./libs/dynamodb-lib"; 4 | 5 | export const main = handler(async (event, context) => { 6 | const data = JSON.parse(event.body); 7 | const params = { 8 | TableName: process.env.tableName, 9 | // 'Item' contains the attributes of the item to be created 10 | // - 'userId': user identities are federated through the 11 | // Cognito Identity Pool, we will use the identity id 12 | // as the user id of the authenticated user 13 | // - 'noteId': a unique uuid 14 | // - 'content': parsed from request body 15 | // - 'attachment': parsed from request body 16 | // - 'createdAt': current Unix timestamp 17 | Item: { 18 | userId: event.requestContext.identity.cognitoIdentityId, 19 | noteId: uuid.v1(), 20 | content: data.content, 21 | attachment: data.attachment, 22 | createdAt: Date.now() 23 | } 24 | }; 25 | 26 | await dynamoDb.put(params); 27 | 28 | return params.Item; 29 | }); 30 | -------------------------------------------------------------------------------- /delete.js: -------------------------------------------------------------------------------- 1 | import handler from "./libs/handler-lib"; 2 | import dynamoDb from "./libs/dynamodb-lib"; 3 | 4 | export const main = handler(async (event, context) => { 5 | const params = { 6 | TableName: process.env.tableName, 7 | // 'Key' defines the partition key and sort key of the item to be removed 8 | // - 'userId': Identity Pool identity id of the authenticated user 9 | // - 'noteId': path parameter 10 | Key: { 11 | userId: event.requestContext.identity.cognitoIdentityId, 12 | noteId: event.pathParameters.id 13 | } 14 | }; 15 | 16 | await dynamoDb.delete(params); 17 | 18 | return { status: true }; 19 | }); 20 | -------------------------------------------------------------------------------- /get.js: -------------------------------------------------------------------------------- 1 | import handler from "./libs/handler-lib"; 2 | import dynamoDb from "./libs/dynamodb-lib"; 3 | 4 | export const main = handler(async (event, context) => { 5 | const params = { 6 | TableName: process.env.tableName, 7 | // 'Key' defines the partition key and sort key of the item to be retrieved 8 | // - 'userId': Identity Pool identity id of the authenticated user 9 | // - 'noteId': path parameter 10 | Key: { 11 | userId: event.requestContext.identity.cognitoIdentityId, 12 | noteId: event.pathParameters.id 13 | } 14 | }; 15 | 16 | const result = await dynamoDb.get(params); 17 | if ( ! result.Item) { 18 | throw new Error("Item not found."); 19 | } 20 | 21 | // Return the retrieved item 22 | return result.Item; 23 | }); 24 | -------------------------------------------------------------------------------- /libs/billing-lib.js: -------------------------------------------------------------------------------- 1 | export function calculateCost(storage) { 2 | const rate = storage <= 10 3 | ? 4 4 | : storage <= 100 5 | ? 2 6 | : 1; 7 | 8 | return rate * storage * 100; 9 | } 10 | -------------------------------------------------------------------------------- /libs/debug-lib.js: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | import AWS from "aws-sdk"; 3 | 4 | let logs; 5 | 6 | // Log AWS SDK calls 7 | AWS.config.logger = { log: debug }; 8 | 9 | export default function debug() { 10 | logs.push({ 11 | date: new Date(), 12 | string: util.format.apply(null, arguments), 13 | }); 14 | } 15 | 16 | export function init(event, context) { 17 | logs = []; 18 | 19 | // Log API event 20 | debug("API event", { 21 | body: event.body, 22 | pathParameters: event.pathParameters, 23 | queryStringParameters: event.queryStringParameters, 24 | }); 25 | } 26 | 27 | export function flush(e) { 28 | logs.forEach(({ date, string }) => console.debug(date, string)); 29 | console.error(e); 30 | } 31 | -------------------------------------------------------------------------------- /libs/dynamodb-lib.js: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | 3 | const client = new AWS.DynamoDB.DocumentClient(); 4 | 5 | export default { 6 | get : (params) => client.get(params).promise(), 7 | put : (params) => client.put(params).promise(), 8 | query : (params) => client.query(params).promise(), 9 | update: (params) => client.update(params).promise(), 10 | delete: (params) => client.delete(params).promise(), 11 | }; 12 | -------------------------------------------------------------------------------- /libs/handler-lib.js: -------------------------------------------------------------------------------- 1 | import * as debug from "./debug-lib"; 2 | 3 | export default function handler(lambda) { 4 | return async function (event, context) { 5 | let body, statusCode; 6 | 7 | // Start debugger 8 | debug.init(event, context); 9 | 10 | try { 11 | // Run the Lambda 12 | body = await lambda(event, context); 13 | statusCode = 200; 14 | } catch (e) { 15 | // Print debug messages 16 | debug.flush(e); 17 | 18 | body = { error: e.message }; 19 | statusCode = 500; 20 | } 21 | 22 | // Return HTTP response 23 | return { 24 | statusCode, 25 | body: JSON.stringify(body), 26 | headers: { 27 | "Access-Control-Allow-Origin": "*", 28 | "Access-Control-Allow-Credentials": true, 29 | }, 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /libs/response-lib.js: -------------------------------------------------------------------------------- 1 | export function success(body) { 2 | return buildResponse(200, body); 3 | } 4 | 5 | export function failure(body) { 6 | return buildResponse(500, body); 7 | } 8 | 9 | function buildResponse(statusCode, body) { 10 | return { 11 | statusCode: statusCode, 12 | headers: { 13 | "Access-Control-Allow-Origin": "*", 14 | "Access-Control-Allow-Credentials": true 15 | }, 16 | body: JSON.stringify(body) 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /list.js: -------------------------------------------------------------------------------- 1 | import handler from "./libs/handler-lib"; 2 | import dynamoDb from "./libs/dynamodb-lib"; 3 | 4 | export const main = handler(async (event, context) => { 5 | const params = { 6 | TableName: process.env.tableName, 7 | // 'KeyConditionExpression' defines the condition for the query 8 | // - 'userId = :userId': only return items with matching 'userId' 9 | // partition key 10 | // 'ExpressionAttributeValues' defines the value in the condition 11 | // - ':userId': defines 'userId' to be Identity Pool identity id 12 | // of the authenticated user 13 | KeyConditionExpression: "userId = :userId", 14 | ExpressionAttributeValues: { 15 | ":userId": event.requestContext.identity.cognitoIdentityId 16 | } 17 | }; 18 | 19 | const result = await dynamoDb.query(params); 20 | 21 | // Return the matching list of items in response body 22 | return result.Items; 23 | }); 24 | -------------------------------------------------------------------------------- /mocks/billing-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"source\":\"tok_visa\",\"storage\":21}", 3 | "requestContext": { 4 | "identity": { 5 | "cognitoIdentityId": "USER-SUB-1234" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mocks/create-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}", 3 | "requestContext": { 4 | "identity": { 5 | "cognitoIdentityId": "USER-SUB-1234" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mocks/delete-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "pathParameters": { 3 | "id": "2b82e2c0-793f-11ea-9602-6d514f529482" 4 | }, 5 | "requestContext": { 6 | "identity": { 7 | "cognitoIdentityId": "USER-SUB-1234" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mocks/get-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "pathParameters": { 3 | "id": "2b82e2c0-793f-11ea-9602-6d514f529482" 4 | }, 5 | "requestContext": { 6 | "identity": { 7 | "cognitoIdentityId": "USER-SUB-1234" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mocks/list-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestContext": { 3 | "identity": { 4 | "cognitoIdentityId": "USER-SUB-1234" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mocks/update-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"content\":\"new world\",\"attachment\":\"new.jpg\"}", 3 | "pathParameters": { 4 | "id": "2b82e2c0-793f-11ea-9602-6d514f529482" 5 | }, 6 | "requestContext": { 7 | "identity": { 8 | "cognitoIdentityId": "USER-SUB-1234" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-nodejs-starter", 3 | "version": "1.1.0", 4 | "description": "A Node.js starter for the Serverless Framework with async/await and unit test support", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "serverless-bundle test" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/AnomalyInnovations/serverless-nodejs-starter.git" 14 | }, 15 | "devDependencies": { 16 | "aws-sdk": "^2.655.0", 17 | "jest": "^25.2.7", 18 | "serverless-bundle": "^1.3.3", 19 | "serverless-dotenv-plugin": "^2.3.2", 20 | "serverless-offline": "^6.1.4" 21 | }, 22 | "dependencies": { 23 | "stripe": "^8.39.0", 24 | "uuid": "^7.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/api-gateway-errors.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | GatewayResponseDefault4XX: 3 | Type: 'AWS::ApiGateway::GatewayResponse' 4 | Properties: 5 | ResponseParameters: 6 | gatewayresponse.header.Access-Control-Allow-Origin: "'*'" 7 | gatewayresponse.header.Access-Control-Allow-Headers: "'*'" 8 | ResponseType: DEFAULT_4XX 9 | RestApiId: 10 | Ref: 'ApiGatewayRestApi' 11 | GatewayResponseDefault5XX: 12 | Type: 'AWS::ApiGateway::GatewayResponse' 13 | Properties: 14 | ResponseParameters: 15 | gatewayresponse.header.Access-Control-Allow-Origin: "'*'" 16 | gatewayresponse.header.Access-Control-Allow-Headers: "'*'" 17 | ResponseType: DEFAULT_5XX 18 | RestApiId: 19 | Ref: 'ApiGatewayRestApi' 20 | -------------------------------------------------------------------------------- /resources/cognito-identity-pool.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | # The federated identity for our user pool to auth with 3 | CognitoIdentityPool: 4 | Type: AWS::Cognito::IdentityPool 5 | Properties: 6 | # Generate a name based on the stage 7 | IdentityPoolName: ${self:custom.stage}IdentityPool 8 | # Don't allow unathenticated users 9 | AllowUnauthenticatedIdentities: false 10 | # Link to our User Pool 11 | CognitoIdentityProviders: 12 | - ClientId: 13 | Ref: CognitoUserPoolClient 14 | ProviderName: 15 | Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ] 16 | 17 | # IAM roles 18 | CognitoIdentityPoolRoles: 19 | Type: AWS::Cognito::IdentityPoolRoleAttachment 20 | Properties: 21 | IdentityPoolId: 22 | Ref: CognitoIdentityPool 23 | Roles: 24 | authenticated: 25 | Fn::GetAtt: [CognitoAuthRole, Arn] 26 | 27 | # IAM role used for authenticated users 28 | CognitoAuthRole: 29 | Type: AWS::IAM::Role 30 | Properties: 31 | Path: / 32 | AssumeRolePolicyDocument: 33 | Version: '2012-10-17' 34 | Statement: 35 | - Effect: 'Allow' 36 | Principal: 37 | Federated: 'cognito-identity.amazonaws.com' 38 | Action: 39 | - 'sts:AssumeRoleWithWebIdentity' 40 | Condition: 41 | StringEquals: 42 | 'cognito-identity.amazonaws.com:aud': 43 | Ref: CognitoIdentityPool 44 | 'ForAnyValue:StringLike': 45 | 'cognito-identity.amazonaws.com:amr': authenticated 46 | Policies: 47 | - PolicyName: 'CognitoAuthorizedPolicy' 48 | PolicyDocument: 49 | Version: '2012-10-17' 50 | Statement: 51 | - Effect: 'Allow' 52 | Action: 53 | - 'mobileanalytics:PutEvents' 54 | - 'cognito-sync:*' 55 | - 'cognito-identity:*' 56 | Resource: '*' 57 | 58 | # Allow users to invoke our API 59 | - Effect: 'Allow' 60 | Action: 61 | - 'execute-api:Invoke' 62 | Resource: 63 | Fn::Join: 64 | - '' 65 | - 66 | - 'arn:aws:execute-api:' 67 | - Ref: AWS::Region 68 | - ':' 69 | - Ref: AWS::AccountId 70 | - ':' 71 | - Ref: ApiGatewayRestApi 72 | - '/*' 73 | 74 | # Allow users to upload attachments to their 75 | # folder inside our S3 bucket 76 | - Effect: 'Allow' 77 | Action: 78 | - 's3:*' 79 | Resource: 80 | - Fn::Join: 81 | - '' 82 | - 83 | - Fn::GetAtt: [AttachmentsBucket, Arn] 84 | - '/private/' 85 | - '$' 86 | - '{cognito-identity.amazonaws.com:sub}/*' 87 | 88 | # Print out the Id of the Identity Pool that is created 89 | Outputs: 90 | IdentityPoolId: 91 | Value: 92 | Ref: CognitoIdentityPool 93 | -------------------------------------------------------------------------------- /resources/cognito-user-pool.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | CognitoUserPool: 3 | Type: AWS::Cognito::UserPool 4 | Properties: 5 | # Generate a name based on the stage 6 | UserPoolName: ${self:custom.stage}-user-pool 7 | # Set email as an alias 8 | UsernameAttributes: 9 | - email 10 | AutoVerifiedAttributes: 11 | - email 12 | 13 | CognitoUserPoolClient: 14 | Type: AWS::Cognito::UserPoolClient 15 | Properties: 16 | # Generate an app client name based on the stage 17 | ClientName: ${self:custom.stage}-user-pool-client 18 | UserPoolId: 19 | Ref: CognitoUserPool 20 | ExplicitAuthFlows: 21 | - ADMIN_NO_SRP_AUTH 22 | GenerateSecret: false 23 | 24 | # Print out the Id of the User Pool that is created 25 | Outputs: 26 | UserPoolId: 27 | Value: 28 | Ref: CognitoUserPool 29 | 30 | UserPoolClientId: 31 | Value: 32 | Ref: CognitoUserPoolClient 33 | -------------------------------------------------------------------------------- /resources/dynamodb-table.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | NotesTable: 3 | Type: AWS::DynamoDB::Table 4 | Properties: 5 | TableName: ${self:custom.tableName} 6 | AttributeDefinitions: 7 | - AttributeName: userId 8 | AttributeType: S 9 | - AttributeName: noteId 10 | AttributeType: S 11 | KeySchema: 12 | - AttributeName: userId 13 | KeyType: HASH 14 | - AttributeName: noteId 15 | KeyType: RANGE 16 | # Set the capacity to auto-scale 17 | BillingMode: PAY_PER_REQUEST 18 | -------------------------------------------------------------------------------- /resources/s3-bucket.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | AttachmentsBucket: 3 | Type: AWS::S3::Bucket 4 | Properties: 5 | # Set the CORS policy 6 | CorsConfiguration: 7 | CorsRules: 8 | - 9 | AllowedOrigins: 10 | - '*' 11 | AllowedHeaders: 12 | - '*' 13 | AllowedMethods: 14 | - GET 15 | - PUT 16 | - POST 17 | - DELETE 18 | - HEAD 19 | MaxAge: 3000 20 | 21 | # Print out the name of the bucket that is created 22 | Outputs: 23 | AttachmentsBucketName: 24 | Value: 25 | Ref: AttachmentsBucket 26 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: notes-app-2-api 2 | 3 | # Create an optimized package for our functions 4 | package: 5 | individually: true 6 | 7 | plugins: 8 | - serverless-bundle # Package our functions with Webpack 9 | - serverless-offline 10 | - serverless-dotenv-plugin # Load .env as environment variables 11 | 12 | custom: 13 | # Our stage is based on what is passed in when running serverless 14 | # commands. Or fallsback to what we have set in the provider section. 15 | stage: ${opt:stage, self:provider.stage} 16 | # Set the table name here so we can use it while testing locally 17 | tableName: ${self:custom.stage}-notes 18 | 19 | provider: 20 | name: aws 21 | runtime: nodejs12.x 22 | stage: dev 23 | region: us-east-1 24 | 25 | # These environment variables are made available to our functions 26 | # under process.env. 27 | environment: 28 | tableName: ${self:custom.tableName} 29 | stripeSecretKey: ${env:STRIPE_SECRET_KEY} 30 | 31 | iamRoleStatements: 32 | - Effect: Allow 33 | Action: 34 | - dynamodb:DescribeTable 35 | - dynamodb:Query 36 | - dynamodb:Scan 37 | - dynamodb:GetItem 38 | - dynamodb:PutItem 39 | - dynamodb:UpdateItem 40 | - dynamodb:DeleteItem 41 | # Restrict our IAM role permissions to 42 | # the specific table for the stage 43 | Resource: 44 | - "Fn::GetAtt": [ NotesTable, Arn ] 45 | 46 | functions: 47 | # Defines an HTTP API endpoint that calls the main function in create.js 48 | # - path: url path is /notes 49 | # - method: POST request 50 | # - cors: enabled CORS (Cross-Origin Resource Sharing) for browser cross 51 | # domain api call 52 | # - authorizer: authenticate using the AWS IAM role 53 | create: 54 | handler: create.main 55 | events: 56 | - http: 57 | path: notes 58 | method: post 59 | cors: true 60 | authorizer: aws_iam 61 | 62 | get: 63 | # Defines an HTTP API endpoint that calls the main function in get.js 64 | # - path: url path is /notes/{id} 65 | # - method: GET request 66 | handler: get.main 67 | events: 68 | - http: 69 | path: notes/{id} 70 | method: get 71 | cors: true 72 | authorizer: aws_iam 73 | 74 | list: 75 | # Defines an HTTP API endpoint that calls the main function in list.js 76 | # - path: url path is /notes 77 | # - method: GET request 78 | handler: list.main 79 | events: 80 | - http: 81 | path: notes 82 | method: get 83 | cors: true 84 | authorizer: aws_iam 85 | 86 | update: 87 | # Defines an HTTP API endpoint that calls the main function in update.js 88 | # - path: url path is /notes/{id} 89 | # - method: PUT request 90 | handler: update.main 91 | events: 92 | - http: 93 | path: notes/{id} 94 | method: put 95 | cors: true 96 | authorizer: aws_iam 97 | 98 | delete: 99 | # Defines an HTTP API endpoint that calls the main function in delete.js 100 | # - path: url path is /notes/{id} 101 | # - method: DELETE request 102 | handler: delete.main 103 | events: 104 | - http: 105 | path: notes/{id} 106 | method: delete 107 | cors: true 108 | authorizer: aws_iam 109 | 110 | billing: 111 | # Defines an HTTP API endpoint that calls the main function in billing.js 112 | # - path: url path is /billing 113 | # - method: POST request 114 | handler: billing.main 115 | events: 116 | - http: 117 | path: billing 118 | method: post 119 | cors: true 120 | authorizer: aws_iam 121 | 122 | # Create our resources with separate CloudFormation templates 123 | resources: 124 | # API Gateway Errors 125 | - ${file(resources/api-gateway-errors.yml)} 126 | # DynamoDB 127 | - ${file(resources/dynamodb-table.yml)} 128 | # S3 129 | - ${file(resources/s3-bucket.yml)} 130 | # Cognito 131 | - ${file(resources/cognito-user-pool.yml)} 132 | - ${file(resources/cognito-identity-pool.yml)} 133 | -------------------------------------------------------------------------------- /tests/billing.test.js: -------------------------------------------------------------------------------- 1 | import { calculateCost } from "../libs/billing-lib"; 2 | 3 | test("Lowest tier", () => { 4 | const storage = 10; 5 | 6 | const cost = 4000; 7 | const expectedCost = calculateCost(storage); 8 | 9 | expect(cost).toEqual(expectedCost); 10 | }); 11 | 12 | test("Middle tier", () => { 13 | const storage = 100; 14 | 15 | const cost = 20000; 16 | const expectedCost = calculateCost(storage); 17 | 18 | expect(cost).toEqual(expectedCost); 19 | }); 20 | 21 | test("Highest tier", () => { 22 | const storage = 101; 23 | 24 | const cost = 10100; 25 | const expectedCost = calculateCost(storage); 26 | 27 | expect(cost).toEqual(expectedCost); 28 | }); 29 | -------------------------------------------------------------------------------- /update.js: -------------------------------------------------------------------------------- 1 | import handler from "./libs/handler-lib"; 2 | import dynamoDb from "./libs/dynamodb-lib"; 3 | 4 | export const main = handler(async (event, context) => { 5 | const data = JSON.parse(event.body); 6 | const params = { 7 | TableName: process.env.tableName, 8 | // 'Key' defines the partition key and sort key of the item to be updated 9 | // - 'userId': Identity Pool identity id of the authenticated user 10 | // - 'noteId': path parameter 11 | Key: { 12 | userId: event.requestContext.identity.cognitoIdentityId, 13 | noteId: event.pathParameters.id 14 | }, 15 | // 'UpdateExpression' defines the attributes to be updated 16 | // 'ExpressionAttributeValues' defines the value in the update expression 17 | UpdateExpression: "SET content = :content, attachment = :attachment", 18 | ExpressionAttributeValues: { 19 | ":attachment": data.attachment || null, 20 | ":content": data.content || null 21 | }, 22 | // 'ReturnValues' specifies if and how to return the item's attributes, 23 | // where ALL_NEW returns all attributes of the item after the update; you 24 | // can inspect 'result' below to see how it works with different settings 25 | ReturnValues: "ALL_NEW" 26 | }; 27 | 28 | await dynamoDb.update(params); 29 | 30 | return { status: true }; 31 | }); 32 | --------------------------------------------------------------------------------