├── .gitignore ├── .npmignore ├── README.md ├── bin └── appsync-cdk-course-plan.ts ├── cdk.json ├── codegen.yml ├── functions ├── createBook.ts ├── getBookById.ts ├── listBooks.ts └── updateBook.ts ├── graphql ├── appsync.graphql └── schema.graphql ├── jest.config.js ├── lessonPlan.md ├── lesson_01 └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_02 └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_03 ├── graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_04 ├── graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_05 ├── functions │ └── listBooks.ts ├── graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_06 ├── functions │ └── listBooks.ts ├── graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_07 ├── functions │ └── listBooks.ts ├── graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_08 ├── codegen.yml ├── functions │ └── listBooks.ts ├── graphql │ ├── appsync.graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_09 ├── functions │ ├── createBook.ts │ └── listBooks.ts ├── graphql │ ├── appsync.graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_10 ├── functions │ ├── createBook.ts │ ├── getBookById.ts │ └── listBooks.ts ├── graphql │ ├── appsync.graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_11 ├── functions │ ├── createBook.ts │ ├── getBookById.ts │ └── listBooks.ts ├── graphql │ ├── appsync.graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_12 ├── functions │ ├── createBook.ts │ ├── getBookById.ts │ └── listBooks.ts ├── graphql │ ├── appsync.graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lesson_13 ├── functions │ ├── createBook.ts │ ├── getBookById.ts │ ├── listBooks.ts │ └── updateBook.ts ├── graphql │ ├── appsync.graphql │ └── schema.graphql └── lib │ └── appsync-cdk-course-plan-stack.ts ├── lib └── appsync-cdk-course-plan-stack.ts ├── package-lock.json ├── package.json ├── test └── appsync-cdk-course-plan.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project! 2 | 3 | This is a blank project for TypeScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /bin/appsync-cdk-course-plan.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import { AppsyncCdkCoursePlanStack } from '../lib/appsync-cdk-course-plan-stack'; 5 | 6 | const app = new cdk.App(); 7 | new AppsyncCdkCoursePlanStack(app, 'AppsyncCdkCoursePlanStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); 22 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/appsync-cdk-course-plan.ts", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, 15 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 16 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 3 | - graphql/schema.graphql 4 | - graphql/appsync.graphql 5 | 6 | config: 7 | scalars: 8 | AWSJSON: string 9 | AWSDate: string 10 | AWSTime: string 11 | AWSDateTime: string 12 | AWSTimestamp: number 13 | AWSEmail: string 14 | AWSURL: string 15 | AWSPhone: string 16 | AWSIPAddress: string 17 | 18 | generates: 19 | types/books.d.ts: 20 | plugins: 21 | - typescript 22 | -------------------------------------------------------------------------------- /functions/createBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, MutationCreateBookArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | MutationCreateBookArgs, 9 | Book | null 10 | > = async (event) => { 11 | const book = event.arguments.book; 12 | 13 | try { 14 | if (!process.env.BOOKS_TABLE) { 15 | console.log("BOOKS_TABLE was not specified"); 16 | return null; 17 | } 18 | 19 | await docClient 20 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 21 | .promise(); 22 | 23 | return book; 24 | } catch (err) { 25 | console.error("[Error] DynamoDB error: ", err); 26 | return null; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /functions/getBookById.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, QueryGetBookByIdArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | QueryGetBookByIdArgs, 9 | Book | null 10 | > = async (event) => { 11 | try { 12 | if (!process.env.BOOKS_TABLE) { 13 | console.log("BOOKS_TABLE was not specified"); 14 | return null; 15 | } 16 | 17 | const { Item } = await docClient 18 | .get({ 19 | TableName: process.env.BOOKS_TABLE, 20 | Key: { id: event.arguments.bookId }, 21 | }) 22 | .promise(); 23 | 24 | return Item as Book; 25 | } catch (err) { 26 | console.error("[Error] DynamoDB error: ", err); 27 | return null; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /functions/updateBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book, MutationUpdateBookArgs } from "../types/books"; 4 | import dynoexpr from "@tuplo/dynoexpr"; 5 | 6 | const documentClient = new DynamoDB.DocumentClient(); 7 | 8 | export const handler: AppSyncResolverHandler< 9 | MutationUpdateBookArgs, 10 | Book | null 11 | > = async (event) => { 12 | try { 13 | const book = event.arguments.book; 14 | if (!process.env.BOOKS_TABLE) { 15 | console.error("Error: BOOKS_TABLE was not specified"); 16 | 17 | return null; 18 | } 19 | 20 | const params = dynoexpr({ 21 | TableName: process.env.BOOKS_TABLE, 22 | Key: { id: book.id }, 23 | ReturnValues: "ALL_NEW", 24 | Update: { 25 | ...(book.title !== undefined ? { title: book.title } : {}), 26 | ...(book.rating !== undefined ? { rating: book.rating } : {}), 27 | ...(book.completed !== undefined ? { completed: book.completed } : {}), 28 | }, 29 | }); 30 | 31 | console.log("params", params); 32 | 33 | const result = await documentClient.update(params).promise(); 34 | 35 | console.log("result", result); 36 | 37 | return result.Attributes as Book; 38 | } catch (error) { 39 | console.error("Whoops", error); 40 | 41 | return null; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | getBookById(bookId: ID!): Book 12 | } 13 | 14 | type Mutation { 15 | createBook(book: BookInput!): Book 16 | updateBook(book: UpdateBookInput!): Book 17 | } 18 | 19 | input BookInput { 20 | id: ID! 21 | title: String! 22 | } 23 | 24 | input UpdateBookInput { 25 | id: ID! 26 | title: String 27 | completed: Boolean 28 | rating: Int 29 | reviews: [String] 30 | } 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lessonPlan.md: -------------------------------------------------------------------------------- 1 | # Build a GraphQL API with AWS CDK and AppSync 2 | 3 | ## Lesson 1. 4 | 5 | **Create an AWS CDK stack from scratch** 6 | 7 | Description: 8 | _Before we start working on our GraphQL API, we need to create a project first. In this lesson, we're going to learn how to create a brand new CDK project from scratch._ 9 | 10 | Link to Configuring AWS CLI page: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html 11 | 12 | Steps: 13 | 14 | - `npm install aws-cdk` 15 | - `cdk --version` 16 | - `aws s3 ls` 17 | - `aws sts get-caller-identity` ("You shouldn't share your account number") 18 | - `mkdir my-graphql-api` 19 | - `cdk init app --language=typescript` 20 | 21 | ## Lesson 2. 22 | 23 | **Deploy a CDK stack to AWS** 24 | 25 | Description: 26 | _Even though our stack is empty, we should deploy it to AWS to verify that everything is set up properly. In this lesson, we're going to learn how to use `cdk deploy` command in order to ship our brand new stack to AWS._ 27 | 28 | Steps: 29 | 30 | - `npm run watch` 31 | - Show that it's generating JS files (`const hello: string = "world";`) 32 | - `cdk bootstrap` (mention that it's necessary only for new regions/accounts) 33 | - `cdk deploy` 34 | - Go to CloudFormation, verify that the stack is there 35 | - Take a look at CloudFormation template 36 | 37 | ## Lesson 3 38 | 39 | **Create an AppSync GraphQL API** 40 | 41 | Description: _It's time to start learning AppSync & GraphQL. In this lesson, we're going to create a simple schema for a bookstore website and create a GraphQL API using AppSync._ 42 | 43 | Steps: 44 | 45 | - Create a schema 46 | 47 | `graphql/schema.graphql` 48 | 49 | ```graphql 50 | type Book { 51 | id: ID! 52 | title: String! 53 | completed: Boolean 54 | rating: Int 55 | reviews: [String] 56 | } 57 | 58 | type Query { 59 | listBooks: [Book] 60 | } 61 | ``` 62 | 63 | - `npm install @aws-cdk/aws-appsync` 64 | - Create an AppSync-powered GraphQL API: 65 | 66 | ```ts 67 | const api = new appsync.GraphqlApi(this, "Api", { 68 | name: "my-api", 69 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 70 | }); 71 | ``` 72 | 73 | - Go to AWS Console, verify that the API is there in CloudFormation/AppSync console 74 | - Open GraphQL playground, send a request, it'll fail due to lack of auth headers 75 | - Specify the `x-api-key` header and copy the value from the Console 76 | 77 | ## Lesson 4 78 | 79 | **Add an API key to an AppSync API** 80 | 81 | Description: _All AppSync-powered APIs need to have some sort of authentication (otherwise there'd be a risk of DDoS attack, which might be costly). In this lesson, we're going to learn how to add a non-default API key authorization to our AppSync API._ 82 | 83 | Steps: 84 | 85 | - Add an authorization config to our API: 86 | 87 | ```ts 88 | authorizationConfig: { 89 | defaultAuthorization: { 90 | authorizationType: appsync.AuthorizationType.API_KEY, 91 | apiKeyConfig: { 92 | description: "An API key for my revolutionary bookstore app", 93 | name: "My API Key", 94 | expires: cdk.Expiration.after(cdk.Duration.days(365)), // Mention that by default it's 7 days 95 | }, 96 | }, 97 | }, 98 | ``` 99 | 100 | - To make our life easier, let's add two CloudFormation outputs: 101 | 102 | ```ts 103 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 104 | value: api.graphqlUrl, 105 | }); 106 | 107 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 108 | value: api.apiKey || "", 109 | }); 110 | ``` 111 | 112 | - Call `listBooks` query with our brand new API key: 113 | 114 | ```graphql 115 | query ListBooks { 116 | listBooks { 117 | id 118 | name 119 | } 120 | } 121 | ``` 122 | 123 | ## Lesson 5 124 | 125 | **Add a Lambda data source to an AppSync API** 126 | 127 | Description: 128 | 129 | _It's time to start writing business logic. In this lesson we're going to learn how to create an AWS Lambda data source and connect it to a GraphQL API powered by AppSync._ 130 | 131 | Steps: 132 | 133 | - `npm install @aws-cdk/aws-lambda` 134 | 135 | ```ts 136 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 137 | handler: "listBooks.handler", 138 | runtime: lambda.Runtime.NODEJS_14_X, 139 | code: lambda.Code.fromAsset("functions"), 140 | }); 141 | ``` 142 | 143 | - Create a `functions/listBooks.ts` file with the following content: 144 | 145 | ```ts 146 | export const handler = async () => { 147 | return [ 148 | { 149 | id: "abc-123", 150 | title: "My Awesome Book", 151 | completed: true, 152 | rating: 10, 153 | reviews: ["The best book ever written"], 154 | }, 155 | { 156 | id: "def-456", 157 | title: "A Terrible Book", 158 | completed: true, 159 | rating: 2, 160 | reviews: ["What did I just read?!"], 161 | }, 162 | ]; 163 | }; 164 | ``` 165 | 166 | - Next add a lambda data source and create a GraphQL resolver: 167 | 168 | ```ts 169 | const listBookDataSource = api.addLambdaDataSource( 170 | "listBookDataSource", 171 | listBooksLambda, 172 | ); 173 | 174 | listBookDataSource.createResolver({ 175 | typeName: "Query", 176 | fieldName: "listBooks", 177 | }); 178 | ``` 179 | 180 | - Run `cdk diff` to understand what is about to be deployed 181 | - Deploy & verify that a resolver has been attached 182 | - Test the API 183 | 184 | ## Lesson 6 185 | 186 | **Create a DynamoDB table to store books** 187 | 188 | Description: _Our API definitely shouldn't rely on hardcoded data. In this lesson, we're going to learn how to create a DynamoDB table, and allow a Lambda function to access its data._ 189 | 190 | Steps: 191 | 192 | - `npm install @aws-cdk/aws-dynamodb` 193 | - Add a books table: 194 | 195 | ```ts 196 | const booksTable = new dynamodb.Table(this, "BooksTable", { 197 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 198 | partitionKey: { 199 | name: "id", 200 | type: dynamodb.AttributeType.STRING, 201 | }, 202 | }); 203 | ``` 204 | 205 | - Add environment variable to a Lambda function: 206 | 207 | ```ts 208 | environment: { 209 | BOOKS_TABLE: booksTable.tableName, 210 | }, 211 | ``` 212 | 213 | - Grant read data: 214 | 215 | ```ts 216 | booksTable.grantReadData(listBooksLambda); 217 | ``` 218 | 219 | ^ Mention the principle of least priviledge 220 | 221 | - Deploy the stack and verify that the table was successfully created 222 | 223 | ## Lesson 7 224 | 225 | **Read data from a DynamoDB table in a Lambda resolver** 226 | 227 | Description: _It's time to wire things together. In this lesson, we're going to learn how to use DynamoDB DocumentClient in order to read data from a table in `listBooks` function. In addition to that we'll learn how to use `@types/aws-lambda` package._ 228 | 229 | Steps: 230 | 231 | - `npm install @types/aws-lambda` & `npm install aws-sdk` 232 | - `import { AppSyncResolverHandler } from "aws-lambda";` 233 | - Use it like this: `export const handler: AppSyncResolverHandler =` 234 | - Create the Book type: 235 | 236 | ```ts 237 | type Book = { 238 | id: string; 239 | title: string; 240 | completed?: boolean; 241 | rating?: number; 242 | reviews?: string[]; 243 | }; 244 | ``` 245 | 246 | - Implement the function: 247 | 248 | ```ts 249 | import { AppSyncResolverHandler } from "aws-lambda"; 250 | import { DynamoDB } from "aws-sdk"; 251 | 252 | type Book = { 253 | id: string; 254 | title: string; 255 | completed?: boolean; 256 | rating?: number; 257 | reviews?: string[]; 258 | }; 259 | 260 | const docClient = new DynamoDB.DocumentClient(); 261 | 262 | export const handler: AppSyncResolverHandler = 263 | async () => { 264 | try { 265 | if (!process.env.BOOKS_TABLE) { 266 | console.log("BOOKS_TABLE was not specified"); 267 | return null; 268 | } 269 | 270 | const data = await docClient 271 | .scan({ TableName: process.env.BOOKS_TABLE }) 272 | .promise(); 273 | 274 | return data.Items as Book[]; 275 | } catch (err) { 276 | console.error("[Error] DynamoDB error: ", err); 277 | return null; 278 | } 279 | }; 280 | ``` 281 | 282 | - Deploy and call the API, an empty list of books should be returned 283 | - Go to DynamoDB console and add two books by hand 284 | 285 | ```json 286 | { 287 | "data": { 288 | "listBooks": [ 289 | { 290 | "id": "456", 291 | "title": "A Philosophy of Software Design", 292 | "completed": true, 293 | "rating": 11, 294 | "reviews": [ 295 | "A must-read for every developer", 296 | "The best book on software design ever written" 297 | ] 298 | }, 299 | { 300 | "id": "123", 301 | "title": "Atomic Habits", 302 | "completed": true, 303 | "rating": 10, 304 | "reviews": ["10/10, changed my life!", "Fantastic book"] 305 | } 306 | ] 307 | } 308 | } 309 | ``` 310 | 311 | - Call the API again, list all books 312 | 313 | ## Lesson 8 314 | 315 | **Generate TypeScript types from a GraphQL schema** 316 | 317 | Description: _Creating TypeScript types from a GraphQL schema by hand is prone to errors - what if we forgot to update the types after changing the schema? In this lesson, we're going to learn how to autogenerate TypeScript types from a schema and use them in a Lambda function._ 318 | 319 | Note: let's make sure to include the link to this excellent blogpost: https://benoitboure.com/how-to-use-typescript-with-appsync-lambda-resolvers 320 | 321 | Steps: 322 | 323 | - `npm install @graphql-codegen/cli @graphql-codegen/typescript` 324 | - Create a `codegen.yml` file: 325 | 326 | ```yaml 327 | overwrite: true 328 | schema: 329 | - graphql/schema.graphql 330 | 331 | generates: 332 | types/books.d.ts: 333 | plugins: 334 | - typescript 335 | ``` 336 | 337 | - Create `appsync.graphql` file with the following content, mention that those are scalars that are built-in in AppSync: 338 | 339 | ```yaml 340 | scalar AWSDate 341 | scalar AWSTime 342 | scalar AWSDateTime 343 | scalar AWSTimestamp 344 | scalar AWSEmail 345 | scalar AWSJSON 346 | scalar AWSURL 347 | scalar AWSPhone 348 | scalar AWSIPAddress 349 | ``` 350 | 351 | - Modify `codegen.yml`: 352 | 353 | ``` 354 | overwrite: true 355 | schema: 356 | - graphql/schema.graphql 357 | - graphql/appsync.graphql 358 | 359 | config: 360 | scalars: 361 | AWSJSON: string 362 | AWSDate: string 363 | AWSTime: string 364 | AWSDateTime: string 365 | AWSTimestamp: number 366 | AWSEmail: string 367 | AWSURL: string 368 | AWSPhone: string 369 | AWSIPAddress: string 370 | 371 | generates: 372 | types/books.d.ts: 373 | plugins: 374 | - typescript 375 | ``` 376 | 377 | - Add a `"codegen": "graphql-codegen"` command to `package.json` 378 | - Run `npm run codegen` and check the contents of `types/books.d.ts` 379 | - Replace types in `listBooks` lambda with autogenerated ones 380 | - Deploy and verify that the behaviour is unchanged 381 | 382 | ## Lesson 9 383 | 384 | **Add a GraphQL mutation to create a book** 385 | 386 | Description: _Our API should allow users to add books to their list. In this lesson we're going to learn how to create a GraphQL mutation with AppSync by using `putItem` method in DynamoDB._ 387 | 388 | Steps: 389 | 390 | - Update the GraphQL schema to include a mutation: 391 | 392 | ``` 393 | type Mutation { 394 | createBook(book: BookInput!): Book 395 | } 396 | ``` 397 | 398 | - Add a `BookInput`: 399 | 400 | (We cannot create a book with reviews, ratings and completed flag by default) 401 | 402 | ```graphql 403 | input BookInput { 404 | id: ID! 405 | title: String! 406 | } 407 | ``` 408 | 409 | - Create a new Lambda function: 410 | 411 | ```ts 412 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 413 | runtime: lambda.Runtime.NODEJS_14_X, 414 | handler: "createBook.handler", 415 | code: lambda.Code.fromAsset("functions"), 416 | memorySize: 1024, 417 | timeout: Duration.seconds(10), 418 | environment: { 419 | BOOKS_TABLE: booksTable.tableName, 420 | }, 421 | }); 422 | 423 | booksTable.grantReadWriteData(createBookLambda); 424 | ``` 425 | 426 | - Add a new Lambda data source: 427 | 428 | ```ts 429 | const createBookDataSource = api.addLambdaDataSource( 430 | "createBookDataSource", 431 | createBookLambda, 432 | ); 433 | 434 | createBookDataSource.createResolver({ 435 | typeName: "Mutation", 436 | fieldName: "createBook", 437 | }); 438 | ``` 439 | 440 | - Create a new Lambda function: 441 | `functions/createBook.ts` 442 | 443 | - Regenerate the types to create a `MutationCreateBookArgs` 444 | 445 | - Before we implement a mutation, let's talk about the `events` object 446 | 447 | ```ts 448 | import { AppSyncResolverHandler } from "aws-lambda"; 449 | import { Book, MutationCreateBookArgs } from "../types/books"; 450 | import { DynamoDB } from "aws-sdk"; 451 | 452 | const docClient = new DynamoDB.DocumentClient(); 453 | 454 | export const handler: AppSyncResolverHandler< 455 | MutationCreateBookArgs, 456 | Book | null 457 | > = async (event) => { 458 | console.log("event", event); 459 | 460 | return null; 461 | }; 462 | ``` 463 | 464 | - Deploy the function 465 | - Run a mutation 466 | - Go to Lambda logs and point the learner to `arguments` 467 | - Implement the rest of the function: 468 | 469 | ```ts 470 | import { AppSyncResolverHandler } from "aws-lambda"; 471 | import { Book, MutationCreateBookArgs } from "../types/books"; 472 | import { DynamoDB } from "aws-sdk"; 473 | 474 | const docClient = new DynamoDB.DocumentClient(); 475 | 476 | export const handler: AppSyncResolverHandler< 477 | MutationCreateBookArgs, 478 | Book | null 479 | > = async (event) => { 480 | const book = event.arguments.book; 481 | 482 | try { 483 | if (!process.env.BOOKS_TABLE) { 484 | console.log("BOOKS_TABLE was not specified"); 485 | return null; 486 | } 487 | 488 | await docClient 489 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 490 | .promise(); 491 | 492 | return book; 493 | } catch (err) { 494 | console.error("[Error] DynamoDB error: ", err); 495 | return null; 496 | } 497 | }; 498 | ``` 499 | 500 | ## Lesson 10 501 | 502 | **Investigate and fix Lambda function latency** 503 | 504 | Description: _In this quick lesson we're going to learn how to measure the time it takes to execute a Lambda resolver. In addition to that we'll learn how to increase Lambda function memory in order to improve its performance._ 505 | 506 | Steps: 507 | 508 | - Show a cold start/regular execution of `createBook` function and how it's taking a bit long 509 | - Increase `memorySize` to 1024 510 | - Deploy, test 511 | 512 | ## Lesson 11 513 | 514 | **Query a book by its ID with AppSync** 515 | 516 | Description: _Apart from getting all of our books, we'd like to be able to query a single book by its ID. In this lesson we're going to learn how to implement a `getBookById` and attach it to our GraphQL API._ 517 | 518 | Steps: 519 | 520 | - Add a `getBookById` query to schema 521 | 522 | ```graphql 523 | type Query { 524 | listBooks: [Book] 525 | getBookById(bookId: ID!): Book 526 | } 527 | ``` 528 | 529 | - Regenerate types 530 | - Add a Lambda function to stack and grant it ability to read data from DDB 531 | 532 | ```ts 533 | const getBookByIdLambda = new lambda.Function(this, "getBookById", { 534 | runtime: lambda.Runtime.NODEJS_14_X, 535 | handler: "getBookById.handler", 536 | code: lambda.Code.fromAsset("functions"), 537 | environment: { 538 | BOOKS_TABLE: booksTable.tableName, 539 | }, 540 | }); 541 | 542 | booksTable.grantReadData(getBookByIdLambda); 543 | ``` 544 | 545 | - Create a Lambda data source: 546 | 547 | ```ts 548 | const getBookByIdDataSource = api.addLambdaDataSource( 549 | "getBookByIdDataSource", 550 | getBookByIdLambda, 551 | ); 552 | 553 | getBookByIdDataSource.createResolver({ 554 | typeName: "Query", 555 | fieldName: "getBookById", 556 | }); 557 | ``` 558 | 559 | - Implement a function (note the `QueryGetBookByIdArgs` type): 560 | 561 | ```ts 562 | import { AppSyncResolverHandler } from "aws-lambda"; 563 | import { Book, QueryGetBookByIdArgs } from "../types/books"; 564 | import { DynamoDB } from "aws-sdk"; 565 | 566 | const docClient = new DynamoDB.DocumentClient(); 567 | 568 | export const handler: AppSyncResolverHandler< 569 | QueryGetBookByIdArgs, 570 | Book | null 571 | > = async (event) => { 572 | try { 573 | if (!process.env.BOOKS_TABLE) { 574 | console.log("BOOKS_TABLE was not specified"); 575 | return null; 576 | } 577 | 578 | const { Item } = await docClient 579 | .get({ 580 | TableName: process.env.BOOKS_TABLE, 581 | Key: { id: event.arguments.bookId }, 582 | }) 583 | .promise(); 584 | 585 | return Item as Book; 586 | } catch (err) { 587 | console.error("[Error] DynamoDB error: ", err); 588 | return null; 589 | } 590 | }; 591 | ``` 592 | 593 | - Deploy and test 594 | 595 | ## Lesson 12 596 | 597 | **Refactor a CDK stack and change Lambda function architecture to ARM** 598 | 599 | Description: _Our CDK stack code is getting rather repetitive. In this quick lesson, we're going to refactor it a little bit and introduce a new architecture for our Lambda function - [a recently announced ARM support.](https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/)_ 600 | 601 | Steps: 602 | 603 | - Refactor lambda functions to use the following: 604 | 605 | ```ts 606 | const commonLambdaProps: Omit = { 607 | runtime: lambda.Runtime.NODEJS_14_X, 608 | code: lambda.Code.fromAsset("functions"), 609 | memorySize: 1024, 610 | architectures: [lambda.Architecture.ARM_64], 611 | timeout: cdk.Duration.seconds(10), 612 | environment: { 613 | BOOKS_TABLE: booksTable.tableName, 614 | }, 615 | }; 616 | ``` 617 | 618 | ## Lesson 13 619 | 620 | **Use logging and X-Ray to debug performance issues in AppSync API** 621 | 622 | Description: _GraphQL is excellent because it allows us to call multiple APIs with a single query. This fact can also be its downfall - what happens if one of the underlying APIs is slow? The whole query is slowed down as a result, and this can be difficult to debug._ 623 | 624 | _In this quick lesson we're going to learn how to enable logging and AWS X-Ray in order to get a better visibility into our AppSync API._ 625 | 626 | Steps: 627 | 628 | - Enable logging & X-ray: 629 | 630 | ```ts 631 | logConfig: { 632 | fieldLogLevel: appsync.FieldLogLevel.ALL, 633 | }, 634 | xRayEnabled: true 635 | ``` 636 | 637 | - Add a wait function to one of the resolvers 638 | 639 | ```ts 640 | const wait = (timeoutMs: number) => 641 | new Promise((resolve) => setTimeout(resolve, timeoutMs)); 642 | ``` 643 | 644 | ## Lesson 14 645 | 646 | **Add an updateBook mutation to AppSync GraphQL API** 647 | 648 | Description: _Whenever we create a book, there are no reviews, rating or `completed` flag. In this lesson, we're going to learn how to implement an `updateBook` mutation in order to be able to update the properties of an already existing book. In addition to that, we're going to learn how to use [dynoexpr](https://github.com/tuplo/dynoexpr) in order to implement that functionality without introducing unnecessary complexity_ 649 | 650 | Steps: 651 | 652 | - Let's add an ability to update a book 653 | - Update the schema: 654 | 655 | ```graphql 656 | type Mutation { 657 | createBook(book: BookInput!): Book 658 | updateBook(book: UpdateBookInput!): Book 659 | } 660 | ... 661 | 662 | input UpdateBookInput { 663 | id: ID! 664 | title: String 665 | completed: Boolean 666 | rating: Int 667 | reviews: [String] 668 | } 669 | ``` 670 | 671 | - Regenerate types 672 | - with DynamoDB we usually need to write `UpdateExpression`s by hand, which can be tricky 673 | - Luckily, we don't have to because there's dynoexpr 674 | - `npm i @tuplo/dynoexpr` 675 | - We need to bundle this dependency with our Lambda function, there's a construct that can help us `@aws-cdk/aws-lambda-nodejs` which uses `esbuild` under the hood 676 | - `npm install @aws-cdk/aws-lambda-nodejs` & `npm install --save-dev esbuild` 677 | 678 | ```ts 679 | const updateBookLambda = new nodeJsLambda.NodejsFunction( 680 | this, 681 | "updateBookHandler", 682 | { 683 | ...commonLambdaProps, 684 | entry: path.join(__dirname, "../functions/updateBook.ts"), 685 | }, 686 | ); 687 | 688 | booksTable.grantReadWriteData(updateBookLambda); 689 | 690 | const updateBookDataSource = api.addLambdaDataSource( 691 | "updateBookDataSource", 692 | updateBookLambda, 693 | ); 694 | 695 | updateBookDataSource.createResolver({ 696 | typeName: "Mutation", 697 | fieldName: "updateBook", 698 | }); 699 | ``` 700 | 701 | - Implement the function: 702 | 703 | ```ts 704 | import { AppSyncResolverHandler } from "aws-lambda"; 705 | import { DynamoDB } from "aws-sdk"; 706 | import { Book, MutationUpdateBookArgs } from "../types/books"; 707 | import dynoexpr from "@tuplo/dynoexpr"; 708 | 709 | const documentClient = new DynamoDB.DocumentClient(); 710 | 711 | export const handler: AppSyncResolverHandler< 712 | MutationUpdateBookArgs, 713 | Book | null 714 | > = async (event) => { 715 | try { 716 | const book = event.arguments.book; 717 | if (!process.env.BOOKS_TABLE) { 718 | console.error("Error: BOOKS_TABLE was not specified"); 719 | 720 | return null; 721 | } 722 | 723 | const params = dynoexpr({ 724 | TableName: process.env.BOOKS_TABLE, 725 | Key: { id: book.id }, 726 | ReturnValues: "ALL_NEW", 727 | Update: { 728 | ...(book.title !== undefined ? { title: book.title } : {}), 729 | ...(book.rating !== undefined ? { rating: book.rating } : {}), 730 | ...(book.completed !== undefined ? { completed: book.completed } : {}), 731 | }, 732 | }); 733 | 734 | console.log("params", params); 735 | 736 | const result = await documentClient.update(params).promise(); 737 | 738 | console.log("result", result); 739 | 740 | return result.Attributes as Book; 741 | } catch (error) { 742 | console.error("Whoops", error); 743 | 744 | return null; 745 | } 746 | }; 747 | ``` 748 | 749 | ## Lesson 15 750 | 751 | **Add a GraphQL subscription to an AppSync API** 752 | 753 | Steps: 754 | 755 | - First, edit the schema 756 | 757 | ```graphql 758 | type Subscription { 759 | onCreateBook: Book @aws_subscribe(mutations: ["createBook"]) 760 | } 761 | ``` 762 | 763 | - Then show a subscription being updated side by side with the GraphQL playground 764 | 765 | ## Lesson 16 766 | 767 | **Delete a CDK stack** 768 | -------------------------------------------------------------------------------- /lesson_01/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | 3 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 4 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 5 | super(scope, id, props); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lesson_02/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | 3 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 4 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 5 | super(scope, id, props); 6 | 7 | const hello: string = "world"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lesson_03/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | } 12 | -------------------------------------------------------------------------------- /lesson_03/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | 4 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 5 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 6 | super(scope, id, props); 7 | 8 | const api = new appsync.GraphqlApi(this, "Api", { 9 | name: "my-api", 10 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lesson_04/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | } 12 | -------------------------------------------------------------------------------- /lesson_04/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | 4 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 5 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 6 | super(scope, id, props); 7 | 8 | const api = new appsync.GraphqlApi(this, "Api", { 9 | name: "my-api", 10 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 11 | authorizationConfig: { 12 | defaultAuthorization: { 13 | authorizationType: appsync.AuthorizationType.API_KEY, 14 | apiKeyConfig: { 15 | description: "An API key for my revolutionary bookstore app", 16 | name: "My API Key", 17 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 24 | value: api.graphqlUrl, 25 | }); 26 | 27 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 28 | value: api.apiKey || "", 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lesson_05/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | export const handler = async () => { 2 | return [ 3 | { 4 | id: 123, 5 | title: "My Awesome Book", 6 | completed: true, 7 | rating: 10, 8 | reviews: ["The best book ever written"], 9 | }, 10 | ]; 11 | }; 12 | -------------------------------------------------------------------------------- /lesson_05/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | } 12 | -------------------------------------------------------------------------------- /lesson_05/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | 5 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 6 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 7 | super(scope, id, props); 8 | 9 | const api = new appsync.GraphqlApi(this, "Api", { 10 | name: "my-api", 11 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 12 | authorizationConfig: { 13 | defaultAuthorization: { 14 | authorizationType: appsync.AuthorizationType.API_KEY, 15 | apiKeyConfig: { 16 | description: "An API key for my revolutionary bookstore app", 17 | name: "My API Key", 18 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 19 | }, 20 | }, 21 | }, 22 | }); 23 | 24 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 25 | handler: "listBooks.handler", 26 | runtime: lambda.Runtime.NODEJS_14_X, 27 | code: lambda.Code.fromAsset("functions"), 28 | }); 29 | 30 | const listBookDataSource = api.addLambdaDataSource( 31 | "listBookDataSource", 32 | listBooksLambda, 33 | ); 34 | 35 | listBookDataSource.createResolver({ 36 | typeName: "Query", 37 | fieldName: "listBooks", 38 | }); 39 | 40 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 41 | value: api.graphqlUrl, 42 | }); 43 | 44 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 45 | value: api.apiKey || "", 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lesson_06/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | export const handler = async () => { 2 | return [ 3 | { 4 | id: 123, 5 | title: "My Awesome Book", 6 | completed: true, 7 | rating: 10, 8 | reviews: ["The best book ever written"], 9 | }, 10 | ]; 11 | }; 12 | -------------------------------------------------------------------------------- /lesson_06/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | } 12 | -------------------------------------------------------------------------------- /lesson_06/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const api = new appsync.GraphqlApi(this, "Api", { 19 | name: "my-api", 20 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 21 | authorizationConfig: { 22 | defaultAuthorization: { 23 | authorizationType: appsync.AuthorizationType.API_KEY, 24 | apiKeyConfig: { 25 | description: "An API key for my revolutionary bookstore app", 26 | name: "My API Key", 27 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 34 | handler: "listBooks.handler", 35 | runtime: lambda.Runtime.NODEJS_14_X, 36 | code: lambda.Code.fromAsset("functions"), 37 | environment: { 38 | BOOKS_TABLE: booksTable.tableName, 39 | }, 40 | }); 41 | 42 | booksTable.grantReadData(listBooksLambda); 43 | 44 | const listBookDataSource = api.addLambdaDataSource( 45 | "listBookDataSource", 46 | listBooksLambda, 47 | ); 48 | 49 | listBookDataSource.createResolver({ 50 | typeName: "Query", 51 | fieldName: "listBooks", 52 | }); 53 | 54 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 55 | value: api.graphqlUrl, 56 | }); 57 | 58 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 59 | value: api.apiKey || "", 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lesson_07/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | 4 | type Book = { 5 | id: string; 6 | name: string; 7 | completed?: boolean; 8 | rating?: number; 9 | reviews?: string[]; 10 | }; 11 | 12 | const docClient = new DynamoDB.DocumentClient(); 13 | 14 | export const handler: AppSyncResolverHandler = 15 | async () => { 16 | try { 17 | if (!process.env.BOOKS_TABLE) { 18 | console.log("BOOKS_TABLE was not specified"); 19 | return null; 20 | } 21 | 22 | const data = await docClient 23 | .scan({ TableName: process.env.BOOKS_TABLE }) 24 | .promise(); 25 | 26 | return data.Items as Book[]; 27 | } catch (err) { 28 | console.error("[Error] DynamoDB error: ", err); 29 | return null; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /lesson_07/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | } 12 | -------------------------------------------------------------------------------- /lesson_07/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const api = new appsync.GraphqlApi(this, "Api", { 19 | name: "my-api", 20 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 21 | authorizationConfig: { 22 | defaultAuthorization: { 23 | authorizationType: appsync.AuthorizationType.API_KEY, 24 | apiKeyConfig: { 25 | description: "An API key for my revolutionary bookstore app", 26 | name: "My API Key", 27 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 34 | handler: "listBooks.handler", 35 | runtime: lambda.Runtime.NODEJS_14_X, 36 | code: lambda.Code.fromAsset("functions"), 37 | environment: { 38 | BOOKS_TABLE: booksTable.tableName, 39 | }, 40 | }); 41 | 42 | booksTable.grantReadData(listBooksLambda); 43 | 44 | const listBookDataSource = api.addLambdaDataSource( 45 | "listBookDataSource", 46 | listBooksLambda, 47 | ); 48 | 49 | listBookDataSource.createResolver({ 50 | typeName: "Query", 51 | fieldName: "listBooks", 52 | }); 53 | 54 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 55 | value: api.graphqlUrl, 56 | }); 57 | 58 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 59 | value: api.apiKey || "", 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lesson_08/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 3 | - graphql/schema.graphql 4 | - graphql/appsync.graphql 5 | 6 | config: 7 | scalars: 8 | AWSJSON: string 9 | AWSDate: string 10 | AWSTime: string 11 | AWSDateTime: string 12 | AWSTimestamp: number 13 | AWSEmail: string 14 | AWSURL: string 15 | AWSPhone: string 16 | AWSIPAddress: string 17 | 18 | generates: 19 | types/books.d.ts: 20 | plugins: 21 | - typescript 22 | -------------------------------------------------------------------------------- /lesson_08/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lesson_08/graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /lesson_08/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | input BookInput { 10 | id: ID! 11 | title: String! 12 | completed: Boolean! 13 | rating: Int! 14 | } 15 | 16 | type Query { 17 | listBooks: [Book] 18 | } 19 | 20 | type Mutation { 21 | createBook(book: BookInput!): Book 22 | } 23 | -------------------------------------------------------------------------------- /lesson_08/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const api = new appsync.GraphqlApi(this, "Api", { 19 | name: "my-api", 20 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 21 | authorizationConfig: { 22 | defaultAuthorization: { 23 | authorizationType: appsync.AuthorizationType.API_KEY, 24 | apiKeyConfig: { 25 | description: "An API key for my revolutionary bookstore app", 26 | name: "My API Key", 27 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 34 | handler: "listBooks.handler", 35 | runtime: lambda.Runtime.NODEJS_14_X, 36 | code: lambda.Code.fromAsset("functions"), 37 | environment: { 38 | BOOKS_TABLE: booksTable.tableName, 39 | }, 40 | }); 41 | 42 | booksTable.grantReadData(listBooksLambda); 43 | 44 | const listBookDataSource = api.addLambdaDataSource( 45 | "listBookDataSource", 46 | listBooksLambda, 47 | ); 48 | 49 | listBookDataSource.createResolver({ 50 | typeName: "Query", 51 | fieldName: "listBooks", 52 | }); 53 | 54 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 55 | value: api.graphqlUrl, 56 | }); 57 | 58 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 59 | value: api.apiKey || "", 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lesson_09/functions/createBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, MutationCreateBookArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | MutationCreateBookArgs, 9 | Book | null 10 | > = async (event) => { 11 | const book = event.arguments.book; 12 | 13 | try { 14 | if (!process.env.BOOKS_TABLE) { 15 | console.log("BOOKS_TABLE was not specified"); 16 | return null; 17 | } 18 | 19 | await docClient 20 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 21 | .promise(); 22 | 23 | return book; 24 | } catch (err) { 25 | console.error("[Error] DynamoDB error: ", err); 26 | return null; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lesson_09/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lesson_09/graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /lesson_09/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | input BookInput { 10 | id: ID! 11 | title: String! 12 | } 13 | 14 | type Query { 15 | listBooks: [Book] 16 | } 17 | 18 | type Mutation { 19 | createBook(book: BookInput!): Book 20 | } 21 | -------------------------------------------------------------------------------- /lesson_09/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const api = new appsync.GraphqlApi(this, "Api", { 19 | name: "my-api", 20 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 21 | authorizationConfig: { 22 | defaultAuthorization: { 23 | authorizationType: appsync.AuthorizationType.API_KEY, 24 | apiKeyConfig: { 25 | description: "An API key for my revolutionary bookstore app", 26 | name: "My API Key", 27 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 34 | handler: "listBooks.handler", 35 | runtime: lambda.Runtime.NODEJS_14_X, 36 | code: lambda.Code.fromAsset("functions"), 37 | environment: { 38 | BOOKS_TABLE: booksTable.tableName, 39 | }, 40 | }); 41 | 42 | booksTable.grantReadData(listBooksLambda); 43 | 44 | const listBookDataSource = api.addLambdaDataSource( 45 | "listBookDataSource", 46 | listBooksLambda, 47 | ); 48 | 49 | listBookDataSource.createResolver({ 50 | typeName: "Query", 51 | fieldName: "listBooks", 52 | }); 53 | 54 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 55 | runtime: lambda.Runtime.NODEJS_14_X, 56 | handler: "createBook.handler", 57 | code: lambda.Code.fromAsset("functions"), 58 | memorySize: 1024, 59 | environment: { 60 | BOOKS_TABLE: booksTable.tableName, 61 | }, 62 | }); 63 | 64 | booksTable.grantReadWriteData(createBookLambda); 65 | 66 | const createBookDataSource = api.addLambdaDataSource( 67 | "createBookDataSource", 68 | createBookLambda, 69 | ); 70 | 71 | createBookDataSource.createResolver({ 72 | typeName: "Mutation", 73 | fieldName: "createBook", 74 | }); 75 | 76 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 77 | value: api.graphqlUrl, 78 | }); 79 | 80 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 81 | value: api.apiKey || "", 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lesson_10/functions/createBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, MutationCreateBookArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | MutationCreateBookArgs, 9 | Book | null 10 | > = async (event) => { 11 | const book = event.arguments.book; 12 | 13 | try { 14 | if (!process.env.BOOKS_TABLE) { 15 | console.log("BOOKS_TABLE was not specified"); 16 | return null; 17 | } 18 | 19 | await docClient 20 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 21 | .promise(); 22 | 23 | return book; 24 | } catch (err) { 25 | console.error("[Error] DynamoDB error: ", err); 26 | return null; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lesson_10/functions/getBookById.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, QueryGetBookByIdArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | QueryGetBookByIdArgs, 9 | Book | null 10 | > = async (event) => { 11 | try { 12 | if (!process.env.BOOKS_TABLE) { 13 | console.log("BOOKS_TABLE was not specified"); 14 | return null; 15 | } 16 | 17 | const { Item } = await docClient 18 | .get({ 19 | TableName: process.env.BOOKS_TABLE, 20 | Key: { id: event.arguments.bookId }, 21 | }) 22 | .promise(); 23 | 24 | return Item as Book; 25 | } catch (err) { 26 | console.error("[Error] DynamoDB error: ", err); 27 | return null; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lesson_10/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lesson_10/graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /lesson_10/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | input BookInput { 10 | id: ID! 11 | title: String! 12 | } 13 | 14 | type Query { 15 | listBooks: [Book] 16 | getBookById(bookId: ID!): Book 17 | } 18 | 19 | type Mutation { 20 | createBook(book: BookInput!): Book 21 | } 22 | -------------------------------------------------------------------------------- /lesson_10/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const api = new appsync.GraphqlApi(this, "Api", { 19 | name: "my-api", 20 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 21 | authorizationConfig: { 22 | defaultAuthorization: { 23 | authorizationType: appsync.AuthorizationType.API_KEY, 24 | apiKeyConfig: { 25 | description: "An API key for my revolutionary bookstore app", 26 | name: "My API Key", 27 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 34 | handler: "listBooks.handler", 35 | runtime: lambda.Runtime.NODEJS_14_X, 36 | code: lambda.Code.fromAsset("functions"), 37 | environment: { 38 | BOOKS_TABLE: booksTable.tableName, 39 | }, 40 | }); 41 | 42 | booksTable.grantReadData(listBooksLambda); 43 | 44 | const listBookDataSource = api.addLambdaDataSource( 45 | "listBookDataSource", 46 | listBooksLambda, 47 | ); 48 | 49 | listBookDataSource.createResolver({ 50 | typeName: "Query", 51 | fieldName: "listBooks", 52 | }); 53 | 54 | const getBookByIdLambda = new lambda.Function(this, "getBookById", { 55 | runtime: lambda.Runtime.NODEJS_14_X, 56 | handler: "getBookById.handler", 57 | code: lambda.Code.fromAsset("functions"), 58 | environment: { 59 | BOOKS_TABLE: booksTable.tableName, 60 | }, 61 | }); 62 | 63 | booksTable.grantReadData(getBookByIdLambda); 64 | 65 | const getBookByIdDataSource = api.addLambdaDataSource( 66 | "getBookByIdDataSource", 67 | getBookByIdLambda, 68 | ); 69 | 70 | getBookByIdDataSource.createResolver({ 71 | typeName: "Query", 72 | fieldName: "getBookById", 73 | }); 74 | 75 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 76 | runtime: lambda.Runtime.NODEJS_14_X, 77 | handler: "createBook.handler", 78 | code: lambda.Code.fromAsset("functions"), 79 | memorySize: 1024, 80 | environment: { 81 | BOOKS_TABLE: booksTable.tableName, 82 | }, 83 | }); 84 | 85 | booksTable.grantReadWriteData(createBookLambda); 86 | 87 | const createBookDataSource = api.addLambdaDataSource( 88 | "createBookDataSource", 89 | createBookLambda, 90 | ); 91 | 92 | createBookDataSource.createResolver({ 93 | typeName: "Mutation", 94 | fieldName: "createBook", 95 | }); 96 | 97 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 98 | value: api.graphqlUrl, 99 | }); 100 | 101 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 102 | value: api.apiKey || "", 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lesson_11/functions/createBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, MutationCreateBookArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | MutationCreateBookArgs, 9 | Book | null 10 | > = async (event) => { 11 | const book = event.arguments.book; 12 | 13 | try { 14 | if (!process.env.BOOKS_TABLE) { 15 | console.log("BOOKS_TABLE was not specified"); 16 | return null; 17 | } 18 | 19 | await docClient 20 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 21 | .promise(); 22 | 23 | return book; 24 | } catch (err) { 25 | console.error("[Error] DynamoDB error: ", err); 26 | return null; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lesson_11/functions/getBookById.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, QueryGetBookByIdArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | QueryGetBookByIdArgs, 9 | Book | null 10 | > = async (event) => { 11 | try { 12 | if (!process.env.BOOKS_TABLE) { 13 | console.log("BOOKS_TABLE was not specified"); 14 | return null; 15 | } 16 | 17 | const { Item } = await docClient 18 | .get({ 19 | TableName: process.env.BOOKS_TABLE, 20 | Key: { id: event.arguments.bookId }, 21 | }) 22 | .promise(); 23 | 24 | return Item as Book; 25 | } catch (err) { 26 | console.error("[Error] DynamoDB error: ", err); 27 | return null; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lesson_11/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lesson_11/graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /lesson_11/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | input BookInput { 10 | id: ID! 11 | title: String! 12 | } 13 | 14 | type Query { 15 | listBooks: [Book] 16 | getBookById(bookId: ID!): Book 17 | } 18 | 19 | type Mutation { 20 | createBook(book: BookInput!): Book 21 | } 22 | -------------------------------------------------------------------------------- /lesson_11/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const commonLambdaProps: Omit = { 19 | runtime: lambda.Runtime.NODEJS_14_X, 20 | code: lambda.Code.fromAsset("functions"), 21 | memorySize: 1024, 22 | architectures: [lambda.Architecture.ARM_64], 23 | timeout: cdk.Duration.seconds(10), 24 | environment: { 25 | BOOKS_TABLE: booksTable.tableName, 26 | }, 27 | }; 28 | 29 | const api = new appsync.GraphqlApi(this, "Api", { 30 | name: "my-api", 31 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 32 | authorizationConfig: { 33 | defaultAuthorization: { 34 | authorizationType: appsync.AuthorizationType.API_KEY, 35 | apiKeyConfig: { 36 | description: "An API key for my revolutionary bookstore app", 37 | name: "My API Key", 38 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 39 | }, 40 | }, 41 | }, 42 | }); 43 | 44 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 45 | handler: "listBooks.handler", 46 | ...commonLambdaProps, 47 | }); 48 | 49 | booksTable.grantReadData(listBooksLambda); 50 | 51 | const listBookDataSource = api.addLambdaDataSource( 52 | "listBookDataSource", 53 | listBooksLambda, 54 | ); 55 | 56 | listBookDataSource.createResolver({ 57 | typeName: "Query", 58 | fieldName: "listBooks", 59 | }); 60 | 61 | const getBookByIdLambda = new lambda.Function(this, "getBookById", { 62 | handler: "getBookById.handler", 63 | ...commonLambdaProps, 64 | }); 65 | 66 | booksTable.grantReadData(getBookByIdLambda); 67 | 68 | const getBookByIdDataSource = api.addLambdaDataSource( 69 | "getBookByIdDataSource", 70 | getBookByIdLambda, 71 | ); 72 | 73 | getBookByIdDataSource.createResolver({ 74 | typeName: "Query", 75 | fieldName: "getBookById", 76 | }); 77 | 78 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 79 | handler: "createBook.handler", 80 | ...commonLambdaProps, 81 | }); 82 | 83 | booksTable.grantReadWriteData(createBookLambda); 84 | 85 | const createBookDataSource = api.addLambdaDataSource( 86 | "createBookDataSource", 87 | createBookLambda, 88 | ); 89 | 90 | createBookDataSource.createResolver({ 91 | typeName: "Mutation", 92 | fieldName: "createBook", 93 | }); 94 | 95 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 96 | value: api.graphqlUrl, 97 | }); 98 | 99 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 100 | value: api.apiKey || "", 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lesson_12/functions/createBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, MutationCreateBookArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | MutationCreateBookArgs, 9 | Book | null 10 | > = async (event) => { 11 | const book = event.arguments.book; 12 | 13 | try { 14 | if (!process.env.BOOKS_TABLE) { 15 | console.log("BOOKS_TABLE was not specified"); 16 | return null; 17 | } 18 | 19 | await docClient 20 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 21 | .promise(); 22 | 23 | return book; 24 | } catch (err) { 25 | console.error("[Error] DynamoDB error: ", err); 26 | return null; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lesson_12/functions/getBookById.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, QueryGetBookByIdArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const documentClient = new DynamoDB.DocumentClient(); 6 | 7 | const wait = (timeoutMs: number) => 8 | new Promise((resolve) => setTimeout(resolve, timeoutMs)); 9 | 10 | export const handler: AppSyncResolverHandler< 11 | QueryGetBookByIdArgs, 12 | Book | null 13 | > = async (event) => { 14 | const bookId = event.arguments.bookId; 15 | 16 | try { 17 | if (!process.env.BOOKS_TABLE) { 18 | console.error("Error: BOOKS_TABLE was not specified"); 19 | 20 | return null; 21 | } 22 | 23 | await wait(2000); 24 | 25 | const { Item } = await documentClient 26 | .get({ 27 | TableName: process.env.BOOKS_TABLE, 28 | Key: { id: bookId }, 29 | }) 30 | .promise(); 31 | 32 | return Item as Book; 33 | } catch (error) { 34 | console.error("Whoops", error); 35 | 36 | return null; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /lesson_12/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lesson_12/graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /lesson_12/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | input BookInput { 10 | id: ID! 11 | title: String! 12 | } 13 | 14 | type Query { 15 | listBooks: [Book] 16 | getBookById(bookId: ID!): Book 17 | } 18 | 19 | type Mutation { 20 | createBook(book: BookInput!): Book 21 | } 22 | -------------------------------------------------------------------------------- /lesson_12/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | 6 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const booksTable = new dynamodb.Table(this, "BooksTable", { 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | partitionKey: { 13 | name: "id", 14 | type: dynamodb.AttributeType.STRING, 15 | }, 16 | }); 17 | 18 | const commonLambdaProps: Omit = { 19 | runtime: lambda.Runtime.NODEJS_14_X, 20 | code: lambda.Code.fromAsset("functions"), 21 | memorySize: 1024, 22 | architectures: [lambda.Architecture.ARM_64], 23 | timeout: cdk.Duration.seconds(10), 24 | environment: { 25 | BOOKS_TABLE: booksTable.tableName, 26 | }, 27 | }; 28 | 29 | const api = new appsync.GraphqlApi(this, "Api", { 30 | name: "my-api", 31 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 32 | authorizationConfig: { 33 | defaultAuthorization: { 34 | authorizationType: appsync.AuthorizationType.API_KEY, 35 | apiKeyConfig: { 36 | description: "An API key for my revolutionary bookstore app", 37 | name: "My API Key", 38 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 39 | }, 40 | }, 41 | }, 42 | logConfig: { 43 | fieldLogLevel: appsync.FieldLogLevel.ALL, 44 | }, 45 | xrayEnabled: true, 46 | }); 47 | 48 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 49 | handler: "listBooks.handler", 50 | ...commonLambdaProps, 51 | }); 52 | 53 | booksTable.grantReadData(listBooksLambda); 54 | 55 | const listBookDataSource = api.addLambdaDataSource( 56 | "listBookDataSource", 57 | listBooksLambda, 58 | ); 59 | 60 | listBookDataSource.createResolver({ 61 | typeName: "Query", 62 | fieldName: "listBooks", 63 | }); 64 | 65 | const getBookByIdLambda = new lambda.Function(this, "getBookById", { 66 | handler: "getBookById.handler", 67 | ...commonLambdaProps, 68 | }); 69 | 70 | booksTable.grantReadData(getBookByIdLambda); 71 | 72 | const getBookByIdDataSource = api.addLambdaDataSource( 73 | "getBookByIdDataSource", 74 | getBookByIdLambda, 75 | ); 76 | 77 | getBookByIdDataSource.createResolver({ 78 | typeName: "Query", 79 | fieldName: "getBookById", 80 | }); 81 | 82 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 83 | handler: "createBook.handler", 84 | ...commonLambdaProps, 85 | }); 86 | 87 | booksTable.grantReadWriteData(createBookLambda); 88 | 89 | const createBookDataSource = api.addLambdaDataSource( 90 | "createBookDataSource", 91 | createBookLambda, 92 | ); 93 | 94 | createBookDataSource.createResolver({ 95 | typeName: "Mutation", 96 | fieldName: "createBook", 97 | }); 98 | 99 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 100 | value: api.graphqlUrl, 101 | }); 102 | 103 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 104 | value: api.apiKey || "", 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lesson_13/functions/createBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, MutationCreateBookArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | MutationCreateBookArgs, 9 | Book | null 10 | > = async (event) => { 11 | const book = event.arguments.book; 12 | 13 | try { 14 | if (!process.env.BOOKS_TABLE) { 15 | console.log("BOOKS_TABLE was not specified"); 16 | return null; 17 | } 18 | 19 | await docClient 20 | .put({ TableName: process.env.BOOKS_TABLE, Item: book }) 21 | .promise(); 22 | 23 | return book; 24 | } catch (err) { 25 | console.error("[Error] DynamoDB error: ", err); 26 | return null; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lesson_13/functions/getBookById.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { Book, QueryGetBookByIdArgs } from "../types/books"; 3 | import { DynamoDB } from "aws-sdk"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler< 8 | QueryGetBookByIdArgs, 9 | Book | null 10 | > = async (event) => { 11 | try { 12 | if (!process.env.BOOKS_TABLE) { 13 | console.log("BOOKS_TABLE was not specified"); 14 | return null; 15 | } 16 | 17 | const { Item } = await docClient 18 | .get({ 19 | TableName: process.env.BOOKS_TABLE, 20 | Key: { id: event.arguments.bookId }, 21 | }) 22 | .promise(); 23 | 24 | return Item as Book; 25 | } catch (err) { 26 | console.error("[Error] DynamoDB error: ", err); 27 | return null; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lesson_13/functions/listBooks.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book } from "../types/books"; 4 | 5 | const docClient = new DynamoDB.DocumentClient(); 6 | 7 | export const handler: AppSyncResolverHandler = 8 | async () => { 9 | try { 10 | if (!process.env.BOOKS_TABLE) { 11 | console.log("BOOKS_TABLE was not specified"); 12 | return null; 13 | } 14 | 15 | const data = await docClient 16 | .scan({ TableName: process.env.BOOKS_TABLE }) 17 | .promise(); 18 | 19 | return data.Items as Book[]; 20 | } catch (err) { 21 | console.error("[Error] DynamoDB error: ", err); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lesson_13/functions/updateBook.ts: -------------------------------------------------------------------------------- 1 | import { AppSyncResolverHandler } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { Book, MutationUpdateBookArgs } from "../types/books"; 4 | import dynoexpr from "@tuplo/dynoexpr"; 5 | 6 | const documentClient = new DynamoDB.DocumentClient(); 7 | 8 | export const handler: AppSyncResolverHandler< 9 | MutationUpdateBookArgs, 10 | Book | null 11 | > = async (event) => { 12 | try { 13 | const book = event.arguments.book; 14 | if (!process.env.BOOKS_TABLE) { 15 | console.error("Error: BOOKS_TABLE was not specified"); 16 | 17 | return null; 18 | } 19 | 20 | const params = dynoexpr({ 21 | TableName: process.env.BOOKS_TABLE, 22 | Key: { id: book.id }, 23 | ReturnValues: "ALL_NEW", 24 | Update: { 25 | ...(book.title !== undefined ? { title: book.title } : {}), 26 | ...(book.rating !== undefined ? { rating: book.rating } : {}), 27 | ...(book.completed !== undefined ? { completed: book.completed } : {}), 28 | }, 29 | }); 30 | 31 | console.log("params", params); 32 | 33 | const result = await documentClient.update(params).promise(); 34 | 35 | console.log("result", result); 36 | 37 | return result.Attributes as Book; 38 | } catch (error) { 39 | console.error("Whoops", error); 40 | 41 | return null; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /lesson_13/graphql/appsync.graphql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | -------------------------------------------------------------------------------- /lesson_13/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | completed: Boolean 5 | rating: Int 6 | reviews: [String] 7 | } 8 | 9 | type Query { 10 | listBooks: [Book] 11 | getBookById(bookId: ID!): Book 12 | } 13 | 14 | type Mutation { 15 | createBook(book: BookInput!): Book 16 | updateBook(book: UpdateBookInput!): Book 17 | } 18 | 19 | input BookInput { 20 | id: ID! 21 | title: String! 22 | } 23 | 24 | input UpdateBookInput { 25 | id: ID! 26 | title: String 27 | completed: Boolean 28 | rating: Int 29 | reviews: [String] 30 | } 31 | -------------------------------------------------------------------------------- /lesson_13/lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as nodeJsLambda from "@aws-cdk/aws-lambda-nodejs"; 5 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 6 | import * as path from "path"; 7 | 8 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 9 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const booksTable = new dynamodb.Table(this, "BooksTable", { 13 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 14 | partitionKey: { 15 | name: "id", 16 | type: dynamodb.AttributeType.STRING, 17 | }, 18 | }); 19 | 20 | const commonLambdaProps: Omit = { 21 | runtime: lambda.Runtime.NODEJS_14_X, 22 | code: lambda.Code.fromAsset("functions"), 23 | memorySize: 1024, 24 | architectures: [lambda.Architecture.ARM_64], 25 | timeout: cdk.Duration.seconds(10), 26 | environment: { 27 | BOOKS_TABLE: booksTable.tableName, 28 | }, 29 | }; 30 | 31 | const api = new appsync.GraphqlApi(this, "Api", { 32 | name: "my-api", 33 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 34 | authorizationConfig: { 35 | defaultAuthorization: { 36 | authorizationType: appsync.AuthorizationType.API_KEY, 37 | apiKeyConfig: { 38 | description: "An API key for my revolutionary bookstore app", 39 | name: "My API Key", 40 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 41 | }, 42 | }, 43 | }, 44 | logConfig: { 45 | fieldLogLevel: appsync.FieldLogLevel.ALL, 46 | }, 47 | xrayEnabled: true, 48 | }); 49 | 50 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 51 | handler: "listBooks.handler", 52 | ...commonLambdaProps, 53 | }); 54 | 55 | booksTable.grantReadData(listBooksLambda); 56 | 57 | const listBookDataSource = api.addLambdaDataSource( 58 | "listBookDataSource", 59 | listBooksLambda, 60 | ); 61 | 62 | listBookDataSource.createResolver({ 63 | typeName: "Query", 64 | fieldName: "listBooks", 65 | }); 66 | 67 | const getBookByIdLambda = new lambda.Function(this, "getBookById", { 68 | handler: "getBookById.handler", 69 | ...commonLambdaProps, 70 | }); 71 | 72 | booksTable.grantReadData(getBookByIdLambda); 73 | 74 | const getBookByIdDataSource = api.addLambdaDataSource( 75 | "getBookByIdDataSource", 76 | getBookByIdLambda, 77 | ); 78 | 79 | getBookByIdDataSource.createResolver({ 80 | typeName: "Query", 81 | fieldName: "getBookById", 82 | }); 83 | 84 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 85 | handler: "createBook.handler", 86 | ...commonLambdaProps, 87 | }); 88 | 89 | booksTable.grantReadWriteData(createBookLambda); 90 | 91 | const createBookDataSource = api.addLambdaDataSource( 92 | "createBookDataSource", 93 | createBookLambda, 94 | ); 95 | 96 | createBookDataSource.createResolver({ 97 | typeName: "Mutation", 98 | fieldName: "createBook", 99 | }); 100 | 101 | const updateBookLambda = new nodeJsLambda.NodejsFunction( 102 | this, 103 | "updateBookHandler", 104 | { 105 | ...commonLambdaProps, 106 | entry: path.join(__dirname, "../functions/updateBook.ts"), 107 | }, 108 | ); 109 | 110 | booksTable.grantReadWriteData(updateBookLambda); 111 | 112 | const updateBookDataSource = api.addLambdaDataSource( 113 | "updateBookDataSource", 114 | updateBookLambda, 115 | ); 116 | 117 | updateBookDataSource.createResolver({ 118 | typeName: "Mutation", 119 | fieldName: "updateBook", 120 | }); 121 | 122 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 123 | value: api.graphqlUrl, 124 | }); 125 | 126 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 127 | value: api.apiKey || "", 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/appsync-cdk-course-plan-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import * as appsync from "@aws-cdk/aws-appsync"; 3 | import * as lambda from "@aws-cdk/aws-lambda"; 4 | import * as nodeJsLambda from "@aws-cdk/aws-lambda-nodejs"; 5 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 6 | import * as path from "path"; 7 | 8 | export class AppsyncCdkCoursePlanStack extends cdk.Stack { 9 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const booksTable = new dynamodb.Table(this, "BooksTable", { 13 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 14 | partitionKey: { 15 | name: "id", 16 | type: dynamodb.AttributeType.STRING, 17 | }, 18 | }); 19 | 20 | const commonLambdaProps: Omit = { 21 | runtime: lambda.Runtime.NODEJS_14_X, 22 | code: lambda.Code.fromAsset("functions"), 23 | memorySize: 1024, 24 | architectures: [lambda.Architecture.ARM_64], 25 | timeout: cdk.Duration.seconds(10), 26 | environment: { 27 | BOOKS_TABLE: booksTable.tableName, 28 | }, 29 | }; 30 | 31 | const api = new appsync.GraphqlApi(this, "Api", { 32 | name: "my-api", 33 | schema: appsync.Schema.fromAsset("graphql/schema.graphql"), 34 | authorizationConfig: { 35 | defaultAuthorization: { 36 | authorizationType: appsync.AuthorizationType.API_KEY, 37 | apiKeyConfig: { 38 | description: "An API key for my revolutionary bookstore app", 39 | name: "My API Key", 40 | expires: cdk.Expiration.after(cdk.Duration.days(365)), 41 | }, 42 | }, 43 | }, 44 | logConfig: { 45 | fieldLogLevel: appsync.FieldLogLevel.ALL, 46 | }, 47 | xrayEnabled: true, 48 | }); 49 | 50 | const listBooksLambda = new lambda.Function(this, "listBooksHandler", { 51 | handler: "listBooks.handler", 52 | ...commonLambdaProps, 53 | }); 54 | 55 | booksTable.grantReadData(listBooksLambda); 56 | 57 | const listBookDataSource = api.addLambdaDataSource( 58 | "listBookDataSource", 59 | listBooksLambda, 60 | ); 61 | 62 | listBookDataSource.createResolver({ 63 | typeName: "Query", 64 | fieldName: "listBooks", 65 | }); 66 | 67 | const getBookByIdLambda = new lambda.Function(this, "getBookById", { 68 | handler: "getBookById.handler", 69 | ...commonLambdaProps, 70 | }); 71 | 72 | booksTable.grantReadData(getBookByIdLambda); 73 | 74 | const getBookByIdDataSource = api.addLambdaDataSource( 75 | "getBookByIdDataSource", 76 | getBookByIdLambda, 77 | ); 78 | 79 | getBookByIdDataSource.createResolver({ 80 | typeName: "Query", 81 | fieldName: "getBookById", 82 | }); 83 | 84 | const createBookLambda = new lambda.Function(this, "createBookHandler", { 85 | handler: "createBook.handler", 86 | ...commonLambdaProps, 87 | }); 88 | 89 | booksTable.grantReadWriteData(createBookLambda); 90 | 91 | const createBookDataSource = api.addLambdaDataSource( 92 | "createBookDataSource", 93 | createBookLambda, 94 | ); 95 | 96 | createBookDataSource.createResolver({ 97 | typeName: "Mutation", 98 | fieldName: "createBook", 99 | }); 100 | 101 | const updateBookLambda = new nodeJsLambda.NodejsFunction( 102 | this, 103 | "updateBookHandler", 104 | { 105 | ...commonLambdaProps, 106 | entry: path.join(__dirname, "../functions/updateBook.ts"), 107 | }, 108 | ); 109 | 110 | booksTable.grantReadWriteData(updateBookLambda); 111 | 112 | const updateBookDataSource = api.addLambdaDataSource( 113 | "updateBookDataSource", 114 | updateBookLambda, 115 | ); 116 | 117 | updateBookDataSource.createResolver({ 118 | typeName: "Mutation", 119 | fieldName: "updateBook", 120 | }); 121 | 122 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 123 | value: api.graphqlUrl, 124 | }); 125 | 126 | new cdk.CfnOutput(this, "GraphQLAPIKey", { 127 | value: api.apiKey || "", 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-cdk-course-plan", 3 | "version": "0.1.0", 4 | "bin": { 5 | "appsync-cdk-course-plan": "bin/appsync-cdk-course-plan.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "codegen": "graphql-codegen" 13 | }, 14 | "devDependencies": { 15 | "@aws-cdk/assert": "1.128.0", 16 | "@types/jest": "^26.0.10", 17 | "@types/node": "10.17.27", 18 | "aws-cdk": "1.128.0", 19 | "esbuild": "^0.13.8", 20 | "jest": "^26.4.2", 21 | "ts-jest": "^26.2.0", 22 | "ts-node": "^9.0.0", 23 | "typescript": "~3.9.7" 24 | }, 25 | "dependencies": { 26 | "@aws-cdk/aws-appsync": "^1.128.0", 27 | "@aws-cdk/aws-dynamodb": "^1.128.0", 28 | "@aws-cdk/aws-lambda": "^1.128.0", 29 | "@aws-cdk/core": "1.128.0", 30 | "@graphql-codegen/cli": "^2.2.1", 31 | "@graphql-codegen/typescript": "^2.2.4", 32 | "@tuplo/dynoexpr": "^2.7.2", 33 | "@types/aws-lambda": "^8.10.84", 34 | "aws-sdk": "^2.1008.0", 35 | "source-map-support": "^0.5.16" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/appsync-cdk-course-plan.test.ts: -------------------------------------------------------------------------------- 1 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as AppsyncCdkCoursePlan from '../lib/appsync-cdk-course-plan-stack'; 4 | 5 | test('Empty Stack', () => { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new AppsyncCdkCoursePlan.AppsyncCdkCoursePlanStack(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------