├── .gitignore ├── README.md ├── amplify-next ├── .gitignore ├── .graphqlconfig.yml ├── README.md ├── amplify │ ├── .config │ │ └── project-config.json │ ├── README.md │ ├── backend │ │ ├── api │ │ │ └── NextBlog │ │ │ │ ├── parameters.json │ │ │ │ ├── schema.graphql │ │ │ │ ├── stacks │ │ │ │ └── CustomResources.json │ │ │ │ └── transform.conf.json │ │ ├── auth │ │ │ └── amplifynextff86daff │ │ │ │ ├── amplifynextff86daff-cloudformation-template.yml │ │ │ │ └── parameters.json │ │ ├── backend-config.json │ │ ├── storage │ │ │ └── projectimages │ │ │ │ ├── parameters.json │ │ │ │ ├── s3-cloudformation-template.json │ │ │ │ └── storage-params.json │ │ └── tags.json │ └── cli.json ├── configureAmplify.js ├── graphql │ ├── mutations.js │ ├── queries.js │ ├── schema.json │ └── subscriptions.js ├── package.json ├── pages │ ├── _app.js │ ├── api │ │ └── hello.js │ ├── create-post.js │ ├── edit-post │ │ └── [id].js │ ├── index.js │ ├── my-posts.js │ ├── posts │ │ └── [id].js │ └── profile.js ├── postcss.config.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── serverless.yml ├── styles │ ├── Home.module.css │ └── globals.css ├── tailwind.config.js └── yarn.lock └── images ├── app-2.png ├── app-3.png ├── app.png ├── app4.png ├── banner.jpg └── update-api.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Cloud with Next.js, Tailwind, and AWS 2 | 3 | ![Next.js Amplify Workshop](images/banner.jpg) 4 | 5 | In this workshop we'll learn how to build a full stack cloud application with [Next.js](https://nextjs.org/), [Tailwind](https://tailwindcss.com/), & [AWS Amplify](https://docs.amplify.aws/). 6 | 7 |
8 | What you'll be building. 9 | 10 | ![App preview](images/app.png) 11 | 12 | ![App preview](images/app-2.png) 13 | 14 | ![App preview](images/app-3.png) 15 | 16 | ![App preview](images/app4.png) 17 |
18 | 19 | ### Overview 20 | 21 | We'll start from scratch, creating a new Next.js app. We'll then, step by step, use the [Amplify CLI](https://github.com/aws-amplify/amplify-cli) to build out and configure our cloud infrastructure and then use the [Amplify JS Libraries](https://github.com/aws-amplify/amplify-js) to connect the Next.js app to the APIs we create using the CLI. 22 | 23 | The app will be a multi-user blogging platform with a markdown editor. When you think of many types of applications like Instagram, Twitter, or Facebook, they consist of a list of items and often the ability to drill down into a single item view. The app we will be building will be very similar to this, displaying a list of posts with data like the title, content, and author of the post. 24 | 25 | This workshop should take you anywhere between 1 to 4 hours to complete. 26 | 27 | ### TOC 28 | 29 | - [Getting Started](#getting-started---creating-the-nextjs-application) 30 | - [Adding an API](#adding-an-aws-appsync-graphql-api) 31 | - [Adding authentication](#adding-authentication) 32 | - [Enabling post creation](#adding-the-create-post-form-and-page) 33 | - [Adding a my-posts view](#adding-a-filtered-view-for-signed-in-users-posts) 34 | - [Updating and deleting posts](#updating-and-deleting-posts) 35 | - [Adding a cover image](#adding-a-cover-image-with-amazon-s3) 36 | - [Deploying to AWS](#deployment-with-amplify) 37 | - [Removing services](#removing-services) 38 | 39 | ### Environment & prerequisites 40 | 41 | Before we begin, make sure you have the following: 42 | 43 | - Node.js v12.x or later installed 44 | - A valid and confirmed AWS account 45 | 46 | We will be working from a terminal using a [Bash shell](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) to run Amplify CLI commands to provision infrastructure and also to run a local version of the Next.js app and test it in a web browser. 47 | 48 | ### Background needed / level 49 | 50 | This workshop is intended for intermediate to advanced front end & back end developers wanting to learn more about full stack serverless development. 51 | 52 | While some level of React and GraphQL is helpful, this workshop requires zero previous knowledge about React or GraphQL. 53 | 54 | ### Topics we'll be covering: 55 | 56 | - GraphQL API with AWS AppSync 57 | - Authentication 58 | - Authorization 59 | - Hosting 60 | - Deleting the resources 61 | 62 | ## Getting Started - Creating the Next.js Application 63 | 64 | To get started, we first need to create a new Next.js project. 65 | 66 | ```bash 67 | $ npx create-next-app amplify-next 68 | ``` 69 | 70 | Now change into the new app directory & install AWS Amplify, AWS Amplify UI React and a few other libraries we'll be using: 71 | 72 | ```bash 73 | $ cd amplify-next 74 | $ npm install aws-amplify @aws-amplify/ui-react react-simplemde-editor@4.1.5 react-markdown uuid 75 | ``` 76 | 77 | Since we will be using Tailwind, let's also install the tailwind dependencies: 78 | 79 | ```sh 80 | npm install tailwindcss@latest postcss@latest autoprefixer@latest @tailwindcss/typography 81 | ``` 82 | 83 | Next, create the necessary Tailwind configuration files: 84 | 85 | ```sh 86 | npx tailwindcss init -p 87 | ``` 88 | 89 | Now update __tailwind.config.js__ to add the Tailwind `typography` plugin to the array of plugins: 90 | 91 | ```js 92 | plugins: [ 93 | require('@tailwindcss/typography') 94 | ], 95 | ``` 96 | 97 | Finally, replace the styles in __styles/globals.css__ with the following: 98 | 99 | ```css 100 | @tailwind base; 101 | @tailwind components; 102 | @tailwind utilities; 103 | ``` 104 | 105 | ## Installing the CLI & Initializing a new AWS Amplify Project 106 | 107 | ### Installing the CLI 108 | 109 | Next, we'll install the AWS Amplify CLI: 110 | 111 | ```bash 112 | # NPM 113 | $ npm install -g @aws-amplify/cli 114 | 115 | # cURL (Mac & Linux) 116 | curl -sL https://aws-amplify.github.io/amplify-cli/install | bash && $SHELL 117 | 118 | # cURL (Windows) 119 | curl -sL https://aws-amplify.github.io/amplify-cli/install-win -o install.cmd && install.cmd 120 | ``` 121 | 122 | Now we need to configure the CLI with our credentials. 123 | 124 | > If you'd like to see a video walkthrough of this configuration process, click [here](https://www.youtube.com/watch?v=fWbM5DLh25U). 125 | 126 | ```sh 127 | $ amplify configure 128 | 129 | - Specify the AWS Region: us-east-1 || us-west-2 || eu-central-1 130 | - Specify the username of the new IAM user: amplify-cli-user 131 | > In the AWS Console, click Next: Permissions, Next: Tags, Next: Review, & Create User to create the new IAM user. Then return to the command line & press Enter. 132 | - Enter the access key of the newly created user: 133 | ? accessKeyId: () 134 | ? secretAccessKey: () 135 | - Profile Name: amplify-cli-user 136 | ``` 137 | 138 | ### Initializing A New Project 139 | 140 | ```bashin 141 | $ amplify init 142 | 143 | - Enter a name for the project: amplifynext 144 | - Initialize the project with the above configuration? No 145 | - Enter a name for the environment: dev 146 | - Choose your default editor: Visual Studio Code (or your default editor) 147 | - Please choose the type of app that youre building: javascript 148 | - What javascript framework are you using: react 149 | - Source Directory Path: . (this sets the base directory to the root directory) 150 | - Distribution Directory Path: .next 151 | - Build Command: npm run-script build 152 | - Start Command: npm run-script start 153 | - Select the authentication method you want to use: AWS profile 154 | - Please choose the profile you want to use: amplify-cli-user (or your preferred profile) 155 | ``` 156 | 157 | The Amplify CLI has initialized a new project & you will see a new folder: __amplify__ & a new file called `aws-exports.js` in the root directory. These files hold your project configuration. 158 | 159 | To view the status of the amplify project at any time, you can run the Amplify `status` command: 160 | 161 | ```sh 162 | $ amplify status 163 | ``` 164 | 165 | To view the amplify project in the Amplify console at any time, run the `console` command: 166 | 167 | ```sh 168 | $ amplify console 169 | ``` 170 | 171 | ## Adding an AWS AppSync GraphQL API 172 | 173 | To add a GraphQL API, we can use the following command: 174 | 175 | ```sh 176 | $ amplify add api 177 | 178 | ? Please select from one of the above mentioned services: GraphQL 179 | ? Provide API name: NextBlog 180 | ? Choose the default authorization type for the API: API key 181 | ? Enter a description for the API key: public 182 | ? After how many days from now the API key should expire (1-365): 365 (or your preferred expiration) 183 | ? Do you want to configure advanced settings for the GraphQL API: No 184 | ? Do you have an annotated GraphQL schema? N 185 | ? Choose a schema template: Single object with fields 186 | ? Do you want to edit the schema now? (Y/n) Y 187 | ``` 188 | 189 | The CLI should open this GraphQL schema in your text editor. 190 | 191 | __amplify/backend/api/NextBlog/schema.graphql__ 192 | 193 | Update the schema to the following: 194 | 195 | ```graphql 196 | type Post @model { 197 | id: ID! 198 | title: String! 199 | content: String! 200 | } 201 | ``` 202 | 203 | After saving the schema, go back to the CLI and press enter. 204 | 205 | ### Deploying the API 206 | 207 | To deploy the API, run the push command: 208 | 209 | ``` 210 | $ amplify push 211 | 212 | ? Are you sure you want to continue? Y 213 | 214 | # You will be walked through the following questions for GraphQL code generation 215 | ? Do you want to generate code for your newly created GraphQL API? Y 216 | ? Choose the code generation language target: javascript 217 | ? Enter the file name pattern of graphql queries, mutations and subscriptions: ./graphql/**/*.js 218 | ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions? Yes 219 | ? Enter maximum statement depth [increase from default if your schema is deeply nested]: 2 220 | ``` 221 | 222 | Now the API is live and you can start interacting with it! 223 | 224 | ### Testing the API 225 | 226 | To test it out we can use the GraphiQL editor in the AppSync dashboard. To open the AppSync dashboard, run the following command: 227 | 228 | ```sh 229 | $ amplify console api 230 | 231 | > Choose GraphQL 232 | ``` 233 | 234 | In the AppSync dashboard, click on __Queries__ to open the GraphiQL editor. In the editor, create a new post with the following mutation: 235 | 236 | ```graphql 237 | mutation createPost { 238 | createPost(input: { 239 | title: "My first post" 240 | content: "Hello world!" 241 | }) { 242 | id 243 | title 244 | content 245 | } 246 | } 247 | ``` 248 | 249 | Then, query for the posts: 250 | 251 | ```graphql 252 | query listPosts { 253 | listPosts { 254 | items { 255 | id 256 | title 257 | content 258 | } 259 | } 260 | } 261 | ``` 262 | 263 | ### Configuring the Next app 264 | 265 | Now, our API is created & we can test it out in our app! 266 | 267 | The first thing we need to do is to configure our Next.js app to be aware of our Amplify project. We can do this by referencing the auto-generated `aws-exports.js` file that was created by the CLI. 268 | 269 | Create a new file called __configureAmplify.js__ in the root of the project and add the following code: 270 | 271 | 272 | ```js 273 | import Amplify from 'aws-amplify' 274 | import config from './aws-exports' 275 | Amplify.configure(config) 276 | ``` 277 | 278 | Next, open __pages/\_app.js__ and import the Amplify configuration below the last import: 279 | 280 | ```js 281 | import '../configureAmplify' 282 | ``` 283 | 284 | Now, our app is ready to start using our AWS services. 285 | 286 | ### Interacting with the GraphQL API from the Next.js application - Querying for data 287 | 288 | Now that the GraphQL API is running we can begin interacting with it. The first thing we'll do is perform a query to fetch data from our API. 289 | 290 | To do so, we need to define the query, execute the query, store the data in our state, then list the items in our UI. 291 | 292 | The main thing to notice in this component is the API call. Take a look at this piece of code: 293 | 294 | ```js 295 | /* Call API.graphql, passing in the query that we'd like to execute. */ 296 | const postData = await API.graphql({ query: listPosts }) 297 | ``` 298 | 299 | Open __pages/index.js__ and add the following code: 300 | 301 | ```js 302 | import { useState, useEffect } from 'react' 303 | import Link from 'next/link' 304 | import { API } from 'aws-amplify' 305 | import { listPosts } from '../graphql/queries' 306 | 307 | export default function Home() { 308 | const [posts, setPosts] = useState([]) 309 | useEffect(() => { 310 | fetchPosts() 311 | }, []) 312 | async function fetchPosts() { 313 | const postData = await API.graphql({ 314 | query: listPosts 315 | }) 316 | setPosts(postData.data.listPosts.items) 317 | } 318 | return ( 319 |
320 |

Posts

321 | { 322 | posts.map((post, index) => ( 323 | 324 |
325 |

{post.title}

326 |
327 | ) 328 | ) 329 | } 330 |
331 | ) 332 | } 333 | ``` 334 | 335 | Next, start the app: 336 | 337 | ```sh 338 | $ npm run dev 339 | ``` 340 | 341 | You should be able to view the list of posts. You will not yet be able to click on a post to navigate to the detail view, that is coming up later. 342 | 343 | ## Adding authentication 344 | 345 | Next, let's add some authentication. 346 | 347 | To add the authentication service, run the following command using the Amplify CLI: 348 | 349 | ```sh 350 | $ amplify add auth 351 | 352 | ? Do you want to use default authentication and security configuration? Default configuration 353 | ? How do you want users to be able to sign in when using your Cognito User Pool? Username 354 | ? Do you want to configure advanced settings? No, I am done. 355 | ``` 356 | 357 | To deploy the authentication service, you can run the push command: 358 | 359 | ```sh 360 | $ amplify push 361 | 362 | ? Are you sure you want to continue? Yes 363 | ``` 364 | 365 | Next, let's add a profile screen and login flow to the app. 366 | 367 | To do so, create a new file called __profile.js__ in the __pages__ directory. Here, add the following code: 368 | 369 | ```js 370 | import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react' 371 | import { Auth } from 'aws-amplify' 372 | import { useState, useEffect } from 'react' 373 | 374 | function Profile() { 375 | const [user, setUser] = useState(null) 376 | useEffect(() => { 377 | checkUser() 378 | }, []) 379 | async function checkUser() { 380 | const user = await Auth.currentAuthenticatedUser() 381 | setUser(user) 382 | } 383 | if (!user) return null 384 | return ( 385 |
386 |

Profile

387 |

Username: {user.username}

388 |

Email: {user.attributes.email}

389 | 390 |
391 | ) 392 | } 393 | 394 | export default withAuthenticator(Profile) 395 | ``` 396 | 397 | The `withAuthenticator` Amplify UI component will scaffold out an entire authentication flow to allow users to sign up and sign in. 398 | 399 | The `AmplifySignOut` button adds a pre-style sign out button. 400 | 401 | Next, add some styling to the UI component by opening __styles/globals.css__ and adding the following code: 402 | 403 | ```css 404 | :root { 405 | --amplify-primary-color: #2563EB; 406 | --amplify-primary-tint: #2563EB; 407 | --amplify-primary-shade: #2563EB; 408 | } 409 | ``` 410 | 411 | Next, open __pages/\_app.js__ to add some navigation and styling to be able to navigate to the new Profile page: 412 | 413 | ```js 414 | import '../styles/globals.css' 415 | import '../configureAmplify' 416 | import Link from 'next/link' 417 | 418 | function MyApp({ Component, pageProps }) { 419 | return ( 420 |
421 | 432 |
433 | 434 |
435 |
436 | ) 437 | } 438 | 439 | export default MyApp 440 | ``` 441 | 442 | Next, run the app: 443 | 444 | ```sh 445 | $ npm run dev 446 | ``` 447 | 448 | You should now be able to sign up and view your profile. 449 | 450 | > The link to __/create-post__ will not yet work as we have not yet created this page. 451 | 452 | ## Adding authorization 453 | 454 | Next, update the API to enable another authorization type to enable both public and private API access. 455 | 456 | ```sh 457 | $ amplify update api 458 | 459 | ? Please select from one of the below mentioned services: GraphQL 460 | ? Select from the options below: Update auth settings 461 | ? Choose the default authorization type for the API: API key 462 | ? Enter a description for the API key: public 463 | ? After how many days from now the API key should expire (1-365): 365 464 | ? Configure additional auth types? Y 465 | ? Choose the additional authorization types you want to configure for the API: Amazon Cognito User Pool 466 | ``` 467 | 468 | ![Updating the API](images/update-api.png) 469 | 470 | Next, let's update the GraphQL schema with the following changes: 471 | 472 | 1. A new field (`username`) to identify the author of a post. 473 | 2. An `@key` directive for enabling a new data access pattern to query posts by username 474 | 475 | Open __amplify/backend/api/NextBlog/schema.graphql__ and update it with the following: 476 | 477 | ```graphql 478 | type Post @model 479 | @key(name: "postsByUsername", fields: ["username"], queryField: "postsByUsername") 480 | @auth(rules: [ 481 | { allow: owner, ownerField: "username" }, 482 | { allow: public, operations: [read] } 483 | ]) { 484 | id: ID! 485 | title: String! 486 | content: String! 487 | username: String 488 | } 489 | ``` 490 | 491 | Next, deploy the updates: 492 | 493 | ```sh 494 | $ amplify push --y 495 | ``` 496 | 497 | Now, you will have two types of API access: 498 | 499 | 1. Private (Cognito) - to create a post, a user must be signed in. Once they have created a post, they can update and delete their own post. They can also read all posts. 500 | 2. Public (API key) - Any user, regardless if they are signed in, can query for posts or a single post. 501 | Using this combination, you can easily query for just a single user's posts or for all posts. 502 | 503 | To make this secondary private API call from the client, the authorization type needs to be specified in the query or mutation: 504 | 505 | ```js 506 | const postData = await API.graphql({ 507 | mutation: createPost, 508 | authMode: 'AMAZON_COGNITO_USER_POOLS', 509 | variables: { 510 | input: postInfo 511 | } 512 | }) 513 | ``` 514 | 515 | ## Adding the Create Post form and page 516 | 517 | Next, create a new page at __pages/create-post.js__ and add the following code: 518 | 519 | ```js 520 | import { withAuthenticator } from '@aws-amplify/ui-react' 521 | import { useState } from 'react' 522 | import { API } from 'aws-amplify' 523 | import { v4 as uuid } from 'uuid' 524 | import { useRouter } from 'next/router' 525 | import SimpleMDE from "react-simplemde-editor" 526 | import "easymde/dist/easymde.min.css" 527 | import { createPost } from '../graphql/mutations' 528 | 529 | const initialState = { title: '', content: '' } 530 | 531 | function CreatePost() { 532 | const [post, setPost] = useState(initialState) 533 | const { title, content } = post 534 | const router = useRouter() 535 | function onChange(e) { 536 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 537 | } 538 | async function createNewPost() { 539 | if (!title || !content) return 540 | const id = uuid() 541 | post.id = id 542 | 543 | await API.graphql({ 544 | query: createPost, 545 | variables: { input: post }, 546 | authMode: "AMAZON_COGNITO_USER_POOLS" 547 | }) 548 | router.push(`/posts/${id}`) 549 | } 550 | return ( 551 |
552 |

Create new post

553 | 560 | setPost({ ...post, content: value })} /> 561 | 566 |
567 | ) 568 | } 569 | 570 | export default withAuthenticator(CreatePost) 571 | ``` 572 | 573 | This will render a form and a markdown editor, allowing users to create new posts. 574 | 575 | Next, create a new folder in the __pages__ directory called __posts__ and a file called __[id].js__ within that folder. In __pages/posts/[id].js__, add the following code: 576 | 577 | ```js 578 | import { API } from 'aws-amplify' 579 | import { useRouter } from 'next/router' 580 | import ReactMarkdown from 'react-markdown' 581 | import '../../configureAmplify' 582 | import { listPosts, getPost } from '../../graphql/queries' 583 | 584 | export default function Post({ post }) { 585 | const router = useRouter() 586 | if (router.isFallback) { 587 | return
Loading...
588 | } 589 | return ( 590 |
591 |

{post.title}

592 |

by {post.username}

593 |
594 | 595 |
596 |
597 | ) 598 | } 599 | 600 | export async function getStaticPaths() { 601 | const postData = await API.graphql({ 602 | query: listPosts 603 | }) 604 | const paths = postData.data.listPosts.items.map(post => ({ params: { id: post.id }})) 605 | return { 606 | paths, 607 | fallback: true 608 | } 609 | } 610 | 611 | export async function getStaticProps ({ params }) { 612 | const { id } = params 613 | const postData = await API.graphql({ 614 | query: getPost, variables: { id } 615 | }) 616 | return { 617 | props: { 618 | post: postData.data.getPost 619 | } 620 | } 621 | } 622 | ``` 623 | 624 | This page uses `getStaticPaths` to dynamically create pages at build time based on the posts coming back from the API. 625 | 626 | We also use the `fallback` flag to enable fallback routes for dynamic SSG page generation. 627 | 628 | `getStaticProps` is used to enable the Post data to be passed into the page as props at build time. 629 | 630 | Finally, update __pages/index.js__ to add the author field and author styles: 631 | 632 | ```js 633 | import { useState, useEffect } from 'react' 634 | import Link from 'next/link' 635 | import { API } from 'aws-amplify' 636 | import { listPosts } from '../graphql/queries' 637 | 638 | export default function Home() { 639 | const [posts, setPosts] = useState([]) 640 | useEffect(() => { 641 | fetchPosts() 642 | }, []) 643 | async function fetchPosts() { 644 | const postData = await API.graphql({ 645 | query: listPosts 646 | }) 647 | setPosts(postData.data.listPosts.items) 648 | } 649 | return ( 650 |
651 |

Posts

652 | { 653 | posts.map((post, index) => ( 654 | 655 |
656 |

{post.title}

657 |

Author: {post.username}

658 |
659 | ) 660 | ) 661 | } 662 |
663 | ) 664 | } 665 | ``` 666 | 667 | ### Deleting existing data 668 | 669 | Now the app is ready to test out, but before we do let's delete the existing data in the database that does not contain an author field. To do so, follow these steps: 670 | 671 | 1. Open the Amplify Console 672 | 673 | ```sh 674 | $ amplify console api 675 | 676 | > Choose GraphQL 677 | ``` 678 | 679 | 2. Click on __Data sources__ 680 | 3. Click on the link to the database 681 | 4. Click on the __Items__ tab. 682 | 5. Select the items in the database and delete them by choosing __Delete__ from the __Actions__ button. 683 | 684 | Next, run the app: 685 | 686 | ```sh 687 | $ npm run dev 688 | ``` 689 | 690 | You should be able to create new posts and view them dynamically. 691 | 692 | ### Running a build 693 | 694 | To run a build and test it out, run the following: 695 | 696 | ```sh 697 | $ npm run build 698 | 699 | $ npm start 700 | ``` 701 | 702 | ## Adding a filtered view for signed in user's posts 703 | 704 | In a future step, we will be enabling the ability to edit or delete the posts that were created by the signed in user. Before we enable that functionality, let's first create a page for only viewing the posts created by the signed in user. 705 | 706 | To do so, create a new file called __my-posts.js__ in the pages directory. This page will be using the `postsByUsername` query, passing in the username of the signed in user to query for only posts created by that user. 707 | 708 | ```js 709 | // pages/my-posts.js 710 | import { useState, useEffect } from 'react' 711 | import Link from 'next/link' 712 | import { API, Auth } from 'aws-amplify' 713 | import { postsByUsername } from '../graphql/queries' 714 | 715 | export default function MyPosts() { 716 | const [posts, setPosts] = useState([]) 717 | useEffect(() => { 718 | fetchPosts() 719 | }, []) 720 | async function fetchPosts() { 721 | const { username } = await Auth.currentAuthenticatedUser() 722 | const postData = await API.graphql({ 723 | query: postsByUsername, variables: { username } 724 | }) 725 | setPosts(postData.data.postsByUsername.items) 726 | } 727 | return ( 728 |
729 |

My Posts

730 | { 731 | posts.map((post, index) => ( 732 | 733 |
734 |

{post.title}

735 |

Author: {post.username}

736 |
737 | ) 738 | ) 739 | } 740 |
741 | ) 742 | } 743 | ``` 744 | 745 | ### Updating the nav 746 | 747 | Next, we need to update the nav to show the link to the new __my-posts__ page, but only show the link if there is a signed in user. 748 | 749 | To do so, we'll be using a combination of the `Auth` class as well as `Hub` which allows us to listen to authentication events. 750 | 751 | Open __pages/\_app.js__ and make the following updates: 752 | 753 | 1. Import the `useState` and `useEffect` hooks from React as well as the `Auth` and `Hub` classes from AWS Amplify: 754 | 755 | ```js 756 | import { useState, useEffect } from 'react' 757 | import { Auth, Hub } from 'aws-amplify' 758 | ``` 759 | 760 | 2. In the `MyApp` function, create some state to hold the signed in user state: 761 | 762 | ```js 763 | const [signedInUser, setSignedInUser] = useState(false) 764 | ``` 765 | 766 | 3. In the `MyApp` function, create a function to detect and maintain user state and invoke it in a `useEffect` hook: 767 | 768 | ```js 769 | useEffect(() => { 770 | authListener() 771 | }) 772 | async function authListener() { 773 | Hub.listen('auth', (data) => { 774 | switch (data.payload.event) { 775 | case 'signIn': 776 | return setSignedInUser(true) 777 | case 'signOut': 778 | return setSignedInUser(false) 779 | } 780 | }) 781 | try { 782 | await Auth.currentAuthenticatedUser() 783 | setSignedInUser(true) 784 | } catch (err) {} 785 | } 786 | ``` 787 | 788 | 4. In the navigation, add a link to the new route to show only if a user is currently signed in: 789 | 790 | ```js 791 | { 792 | signedInUser && ( 793 | 794 | My Posts 795 | 796 | ) 797 | } 798 | ``` 799 | 800 | Next, test it out by restarting the dev server: 801 | 802 | ```sh 803 | npm run dev 804 | ``` 805 | 806 | ## Updating and deleting posts 807 | 808 | Next, let's add a way for a signed in user to edit and delete their posts. 809 | 810 | First, create a new folder named __edit-post__ in the __pages__ directory. Then, create a file named __[id].js__ in this folder. 811 | 812 | In this file, we'll be accessing the `id` of the post from a route parameter. When the component loads, we will then use the post id from the route to fetch the post data and make it available for editing. 813 | 814 | In this file, add the following code: 815 | 816 | ```js 817 | // pages/edit-post/[id].js 818 | import { useEffect, useState } from 'react' 819 | import { API } from 'aws-amplify' 820 | import { useRouter } from 'next/router' 821 | import SimpleMDE from "react-simplemde-editor" 822 | import "easymde/dist/easymde.min.css" 823 | import { updatePost } from '../../graphql/mutations' 824 | import { getPost } from '../../graphql/queries' 825 | 826 | function EditPost() { 827 | const [post, setPost] = useState(null) 828 | const router = useRouter() 829 | const { id } = router.query 830 | 831 | useEffect(() => { 832 | fetchPost() 833 | async function fetchPost() { 834 | if (!id) return 835 | const postData = await API.graphql({ query: getPost, variables: { id }}) 836 | setPost(postData.data.getPost) 837 | } 838 | }, [id]) 839 | if (!post) return null 840 | function onChange(e) { 841 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 842 | } 843 | const { title, content } = post 844 | async function updateCurrentPost() { 845 | if (!title || !content) return 846 | await API.graphql({ 847 | query: updatePost, 848 | variables: { input: { title, content, id } }, 849 | authMode: "AMAZON_COGNITO_USER_POOLS" 850 | }) 851 | console.log('post successfully updated!') 852 | router.push('/my-posts') 853 | } 854 | return ( 855 |
856 |

Edit post

857 | 864 | setPost({ ...post, content: value })} /> 865 | 868 |
869 | ) 870 | } 871 | 872 | export default EditPost 873 | ``` 874 | 875 | Next, open __pages/my-posts.js__. We'll make a few updates to this page: 876 | 877 | 1. Create a function for deleting a post 878 | 2. Add a link to edit the post by navigating to `/edit-post/:postID` 879 | 3. Add a link to view the post 880 | 4. Create a button for deleting posts 881 | 882 | Update this file with the following code: 883 | 884 | ```js 885 | // pages/my-posts.js 886 | import { useState, useEffect } from 'react' 887 | import Link from 'next/link' 888 | import { API, Auth } from 'aws-amplify' 889 | import { postsByUsername } from '../graphql/queries' 890 | import { deletePost as deletePostMutation } from '../graphql/mutations' 891 | 892 | export default function MyPosts() { 893 | const [posts, setPosts] = useState([]) 894 | useEffect(() => { 895 | fetchPosts() 896 | }, []) 897 | async function fetchPosts() { 898 | const { username } = await Auth.currentAuthenticatedUser() 899 | const postData = await API.graphql({ 900 | query: postsByUsername, variables: { username } 901 | }) 902 | setPosts(postData.data.postsByUsername.items) 903 | } 904 | async function deletePost(id) { 905 | await API.graphql({ 906 | query: deletePostMutation, 907 | variables: { input: { id } }, 908 | authMode: "AMAZON_COGNITO_USER_POOLS" 909 | }) 910 | fetchPosts() 911 | } 912 | return ( 913 |
914 |

My Posts

915 | { 916 | posts.map((post, index) => ( 917 |
918 |

{post.title}

919 |

Author: {post.username}

920 | Edit Post 921 | View Post 922 | 926 |
927 | )) 928 | } 929 |
930 | ) 931 | } 932 | ``` 933 | 934 | ### Enabling Incremental Static Generation 935 | 936 | The last thing we need to do is implement [Incremental Static Generation](https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration). Since we are allowing users to update posts, we need to have a way for our site to render the newly updated posts. 937 | 938 | Incremental Static Regeneration allows you to update existing pages by re-rendering them in the background as traffic comes in. 939 | 940 | To enable this, open __pages/posts/[id].js__ and update the `getStaticProps` method with the following: 941 | 942 | ```js 943 | 944 | export async function getStaticProps ({ params }) { 945 | const { id } = params 946 | const postData = await API.graphql({ 947 | query: getPost, variables: { id } 948 | }) 949 | return { 950 | props: { 951 | post: postData.data.getPost 952 | }, 953 | // Next.js will attempt to re-generate the page: 954 | // - When a request comes in 955 | // - At most once every second 956 | revalidate: 1 // adds Incremental Static Generation, sets time in seconds 957 | } 958 | } 959 | ``` 960 | 961 | To test it out, restart the server or run a new build: 962 | 963 | ```sh 964 | npm run dev 965 | 966 | # or 967 | 968 | npm run build && npm start 969 | ``` 970 | 971 | ## Adding a cover image with Amazon S3 972 | 973 | Next, let's give users the ability to add a cover image to their post. 974 | 975 | To do so, we need to do the following things: 976 | 977 | 1. Add the `storage` category to the Amplify project. 978 | 2. Update the GraphQL schema to add a `coverImage` field to the `Post` type 979 | 3. Update the UI to enable users to upload images 980 | 4. Update the UI to render the cover image (if it exists) 981 | 982 | To get started, let's first open the GraphQL schema located at __amplify/backend/api/NextBlog/schema.graphql__ and add a `coverImage` field: 983 | 984 | ```graphql 985 | type Post @model 986 | @key(name: "postsByUsername", fields: ["username"], queryField: "postsByUsername") 987 | @auth(rules: [ 988 | { allow: owner, ownerField: "username" }, 989 | { allow: public, operations: [read] } 990 | ]) { 991 | id: ID! 992 | title: String! 993 | content: String! 994 | username: String 995 | coverImage: String 996 | } 997 | ``` 998 | 999 | Next, enable file storage by running the Amplify `add` command: 1000 | 1001 | ```sh 1002 | amplify add storage 1003 | 1004 | ? Please select from one of the below mentioned services: Content (Images, audio, video, etc.) 1005 | ? Please provide a friendly name for your resource that will be used to label this category in the project: projectimages 1006 | ? Please provide bucket name: 1007 | ? Who should have access: Auth and guest users 1008 | ? What kind of access do you want for Authenticated users? create/update, read, delete 1009 | ? What kind of access do you want for Guest users? read 1010 | ? Do you want to add a Lambda Trigger for your S3 Bucket? No 1011 | ``` 1012 | 1013 | Next, deploy the back end: 1014 | 1015 | ```sh 1016 | amplify push --y 1017 | ``` 1018 | 1019 | ### Allowing users to upload a cover image 1020 | 1021 | Next, let's enable the ability to upload a cover image when creating a post. 1022 | 1023 | To do so, open __pages/create-post.js__. 1024 | 1025 | We will be making the following updates. 1026 | 1027 | 1. Adding a button to enable users to upload a file and save it in the local state 1028 | 2. Import the Amplify `Storage` category and `useRef` from React. 1029 | 3. When creating a new post, we will check to see if there is an image in the local state, and if there is then upload the image to S3 and store the image key along with the other post data. 1030 | 4. When a user uploads an image, show a preview of the image in the UI 1031 | 1032 | ```js 1033 | // pages/create-post.js. 1034 | import { withAuthenticator } from '@aws-amplify/ui-react' 1035 | import { useState, useRef } from 'react' // new 1036 | import { API, Storage } from 'aws-amplify' 1037 | import { v4 as uuid } from 'uuid' 1038 | import { useRouter } from 'next/router' 1039 | import SimpleMDE from "react-simplemde-editor" 1040 | import "easymde/dist/easymde.min.css" 1041 | import { createPost } from '../graphql/mutations' 1042 | 1043 | const initialState = { title: '', content: '' } 1044 | 1045 | function CreatePost() { 1046 | const [post, setPost] = useState(initialState) 1047 | const [image, setImage] = useState(null) 1048 | const hiddenFileInput = useRef(null); 1049 | const { title, content } = post 1050 | const router = useRouter() 1051 | function onChange(e) { 1052 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 1053 | } 1054 | async function createNewPost() { 1055 | if (!title || !content) return 1056 | const id = uuid() 1057 | post.id = id 1058 | // If there is an image uploaded, store it in S3 and add it to the post metadata 1059 | if (image) { 1060 | const fileName = `${image.name}_${uuid()}` 1061 | post.coverImage = fileName 1062 | await Storage.put(fileName, image) 1063 | } 1064 | 1065 | await API.graphql({ 1066 | query: createPost, 1067 | variables: { input: post }, 1068 | authMode: "AMAZON_COGNITO_USER_POOLS" 1069 | }) 1070 | router.push(`/posts/${id}`) 1071 | } 1072 | async function uploadImage() { 1073 | hiddenFileInput.current.click(); 1074 | } 1075 | function handleChange (e) { 1076 | const fileUploaded = e.target.files[0]; 1077 | if (!fileUploaded) return 1078 | setImage(fileUploaded) 1079 | } 1080 | return ( 1081 |
1082 |

Create new post

1083 | 1090 | { 1091 | image && ( 1092 | 1093 | ) 1094 | } 1095 | setPost({ ...post, content: value })} /> 1096 | 1102 | 1108 | 1113 |
1114 | ) 1115 | } 1116 | 1117 | export default withAuthenticator(CreatePost) 1118 | ``` 1119 | 1120 | Now, users should be able to upload a cover image along with their post. If there is a cover image present, it will show them a preview. 1121 | 1122 | ### Rendering the cover image in the detail view 1123 | 1124 | Next, let's look at how to render the cover image. To do so, we need to check to see if the cover image key exists as part of the post. If it does, we will fetch the image from S3 and render it in the view. 1125 | 1126 | Update __pages/posts/[id].js__ with the following: 1127 | 1128 | ```js 1129 | // pages/posts/[id].js 1130 | import { API, Storage } from 'aws-amplify' 1131 | import { useState, useEffect } from 'react' 1132 | import { useRouter } from 'next/router' 1133 | import ReactMarkdown from 'react-markdown' 1134 | import { listPosts, getPost } from '../../graphql/queries' 1135 | 1136 | export default function Post({ post }) { 1137 | const [coverImage, setCoverImage] = useState(null) 1138 | useEffect(() => { 1139 | updateCoverImage() 1140 | }, []) 1141 | async function updateCoverImage() { 1142 | if (post.coverImage) { 1143 | const imageKey = await Storage.get(post.coverImage) 1144 | setCoverImage(imageKey) 1145 | } 1146 | } 1147 | console.log('post: ', post) 1148 | const router = useRouter() 1149 | if (router.isFallback) { 1150 | return
Loading...
1151 | } 1152 | return ( 1153 |
1154 |

{post.title}

1155 | { 1156 | coverImage && 1157 | } 1158 |

by {post.username}

1159 |
1160 | 1161 |
1162 |
1163 | ) 1164 | } 1165 | 1166 | export async function getStaticPaths() { 1167 | const postData = await API.graphql({ 1168 | query: listPosts 1169 | }) 1170 | const paths = postData.data.listPosts.items.map(post => ({ params: { id: post.id }})) 1171 | return { 1172 | paths, 1173 | fallback: true 1174 | } 1175 | } 1176 | 1177 | export async function getStaticProps ({ params }) { 1178 | const { id } = params 1179 | const postData = await API.graphql({ 1180 | query: getPost, variables: { id } 1181 | }) 1182 | return { 1183 | props: { 1184 | post: postData.data.getPost 1185 | } 1186 | } 1187 | } 1188 | ``` 1189 | 1190 | ### Allowing users the ability to update a cover image 1191 | 1192 | Next, let's enable users to edit a post that contains a cover image. To do so, we'll need to enable similar functionality as we did when allowing users to create a post with a cover image. 1193 | 1194 | We'll need to detect whether a post has a cover image, but also whether they have uploaded a new cover image and save the update if they have done so. 1195 | 1196 | To implement this, update __pages/edit-post/[id].js__ with the following code: 1197 | 1198 | ```js 1199 | // pages/edit-post/[id].js 1200 | import { useEffect, useState, useRef } from 'react' 1201 | import { API, Storage } from 'aws-amplify' 1202 | import { useRouter } from 'next/router' 1203 | import SimpleMDE from "react-simplemde-editor" 1204 | import "easymde/dist/easymde.min.css" 1205 | import { v4 as uuid } from 'uuid' 1206 | import { updatePost } from '../../graphql/mutations' 1207 | import { getPost } from '../../graphql/queries' 1208 | 1209 | function EditPost() { 1210 | const [post, setPost] = useState(null) 1211 | const router = useRouter() 1212 | const { id } = router.query 1213 | const [coverImage, setCoverImage] = useState(null) 1214 | const [localImage, setLocalImage] = useState(null) 1215 | const fileInput = useRef(null) 1216 | 1217 | useEffect(() => { 1218 | fetchPost() 1219 | async function fetchPost() { 1220 | if (!id) return 1221 | const postData = await API.graphql({ query: getPost, variables: { id }}) 1222 | console.log('postData: ', postData) 1223 | setPost(postData.data.getPost) 1224 | if (postData.data.getPost.coverImage) { 1225 | updateCoverImage(postData.data.getPost.coverImage) 1226 | } 1227 | } 1228 | }, [id]) 1229 | if (!post) return null 1230 | async function updateCoverImage(coverImage) { 1231 | const imageKey = await Storage.get(coverImage) 1232 | setCoverImage(imageKey) 1233 | } 1234 | async function uploadImage() { 1235 | fileInput.current.click(); 1236 | } 1237 | function handleChange (e) { 1238 | const fileUploaded = e.target.files[0]; 1239 | if (!fileUploaded) return 1240 | setCoverImage(fileUploaded) 1241 | setLocalImage(URL.createObjectURL(fileUploaded)) 1242 | } 1243 | function onChange(e) { 1244 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 1245 | } 1246 | const { title, content } = post 1247 | async function updateCurrentPost() { 1248 | if (!title || !content) return 1249 | const postUpdated = { 1250 | id, content, title 1251 | } 1252 | // check to see if there is a cover image and that it has been updated 1253 | if (coverImage && localImage) { 1254 | const fileName = `${coverImage.name}_${uuid()}` 1255 | postUpdated.coverImage = fileName 1256 | await Storage.put(fileName, coverImage) 1257 | } 1258 | await API.graphql({ 1259 | query: updatePost, 1260 | variables: { input: postUpdated }, 1261 | authMode: "AMAZON_COGNITO_USER_POOLS" 1262 | }) 1263 | console.log('post successfully updated!') 1264 | router.push('/my-posts') 1265 | } 1266 | return ( 1267 |
1268 |

Edit post

1269 | { 1270 | coverImage && 1271 | } 1272 | 1279 | setPost({ ...post, content: value })} /> 1280 | 1286 | 1292 | 1295 |
1296 | ) 1297 | } 1298 | 1299 | export default EditPost 1300 | ``` 1301 | 1302 | Now, users should be able to edit the cover image if it exists, or add a cover image for posts that do not contain one. 1303 | 1304 | ### Rendering a cover image thumbnail preview 1305 | 1306 | The last thing we may want to do is give a preview of the cover image in the list of posts on the main index page. 1307 | 1308 | To do so, let's update our code to see if there is a cover image associated with each post. If there is, we'll fetch the image from S3 and then render the post image if it exists. 1309 | 1310 | To implement this, open __pages/index.js__ and update it with the following code: 1311 | 1312 | ```js 1313 | // pages/index.js 1314 | import { useState, useEffect } from 'react' 1315 | import Link from 'next/link' 1316 | import { API, Storage } from 'aws-amplify' 1317 | import { listPosts } from '../graphql/queries' 1318 | 1319 | export default function Home() { 1320 | const [posts, setPosts] = useState([]) 1321 | useEffect(() => { 1322 | fetchPosts() 1323 | }, []) 1324 | async function fetchPosts() { 1325 | const postData = await API.graphql({ 1326 | query: listPosts 1327 | }) 1328 | const { items } = postData.data.listPosts 1329 | // Fetch images from S3 for posts that contain a cover image 1330 | const postsWithImages = await Promise.all(items.map(async post => { 1331 | if (post.coverImage) { 1332 | post.coverImage = await Storage.get(post.coverImage) 1333 | } 1334 | return post 1335 | })) 1336 | setPosts(postsWithImages) 1337 | } 1338 | return ( 1339 |
1340 |

Posts

1341 | { 1342 | posts.map((post, index) => ( 1343 | 1344 |
1345 | { 1346 | post.coverImage && 1347 | } 1348 |
1349 |

{post.title}

1350 |

Author: {post.username}

1351 |
1352 |
1353 | ) 1354 | ) 1355 | } 1356 |
1357 | ) 1358 | } 1359 | ``` 1360 | 1361 | If you'd like to also have the same functionality to preview cover images in the __my-posts.js__ view, try adding the same updates there. 1362 | 1363 | ## Deployment with amplify 1364 | 1365 | To deploy to AWS hosting, follow the guide laid out [here](https://docs.amplify.aws/guides/hosting/nextjs/q/platform/js) 1366 | 1367 | ## Removing Services 1368 | 1369 | If at any time, or at the end of this workshop, you would like to delete a service from your project & your account, you can do this by running the `amplify remove` command: 1370 | 1371 | ```sh 1372 | $ amplify remove auth 1373 | 1374 | $ amplify push 1375 | ``` 1376 | 1377 | If you are unsure of what services you have enabled at any time, you can run the `amplify status` command: 1378 | 1379 | ```sh 1380 | $ amplify status 1381 | ``` 1382 | 1383 | `amplify status` will give you the list of resources that are currently enabled in your app. 1384 | 1385 | ### Deleting the Amplify project and all services 1386 | 1387 | If you'd like to delete the entire project, you can run the `delete` command: 1388 | 1389 | ```sh 1390 | $ amplify delete 1391 | ``` 1392 | -------------------------------------------------------------------------------- /amplify-next/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | .serverless 37 | .serverless_nextjs 38 | .vscode 39 | 40 | #amplify 41 | amplify/\#current-cloud-backend 42 | amplify/.config/local-* 43 | amplify/logs 44 | amplify/mock-data 45 | amplify/backend/amplify-meta.json 46 | amplify/backend/awscloudformation 47 | amplify/backend/.temp 48 | build/ 49 | dist/ 50 | node_modules/ 51 | aws-exports.js 52 | awsconfiguration.json 53 | amplifyconfiguration.json 54 | amplify-build-config.json 55 | amplify-gradle-config.json 56 | amplifytools.xcconfig 57 | .secret-* -------------------------------------------------------------------------------- /amplify-next/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | NextBlog: 3 | schemaPath: graphql/schema.json 4 | includes: 5 | - ./graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /amplify-next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /amplify-next/amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "amplifynext", 3 | "version": "3.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": ".", 9 | "DistributionDir": ".next", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /amplify-next/amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /amplify-next/amplify/backend/api/NextBlog/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "NextBlog", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authamplifynextff86daff", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /amplify-next/amplify/backend/api/NextBlog/schema.graphql: -------------------------------------------------------------------------------- 1 | type Post @model 2 | @key(name: "postsByUsername", fields: ["username"], queryField: "postsByUsername") 3 | @auth(rules: [ 4 | { allow: owner, ownerField: "username" }, 5 | { allow: public, operations: [read] } 6 | ]) { 7 | id: ID! 8 | title: String! 9 | content: String! 10 | username: String 11 | coverImage: String 12 | } -------------------------------------------------------------------------------- /amplify-next/amplify/backend/api/NextBlog/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": ["true", "false"] 50 | } 51 | }, 52 | "Outputs": { 53 | "EmptyOutput": { 54 | "Description": "An empty output. You may delete this if you have at least one resource above.", 55 | "Value": "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /amplify-next/amplify/backend/api/NextBlog/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /amplify-next/amplify/backend/auth/amplifynextff86daff/amplifynextff86daff-cloudformation-template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | env: 5 | Type: String 6 | authRoleArn: 7 | Type: String 8 | unauthRoleArn: 9 | Type: String 10 | 11 | 12 | 13 | 14 | identityPoolName: 15 | Type: String 16 | 17 | 18 | 19 | allowUnauthenticatedIdentities: 20 | Type: String 21 | 22 | resourceNameTruncated: 23 | Type: String 24 | 25 | 26 | userPoolName: 27 | Type: String 28 | 29 | 30 | 31 | autoVerifiedAttributes: 32 | Type: CommaDelimitedList 33 | 34 | mfaConfiguration: 35 | Type: String 36 | 37 | 38 | 39 | mfaTypes: 40 | Type: CommaDelimitedList 41 | 42 | smsAuthenticationMessage: 43 | Type: String 44 | 45 | 46 | smsVerificationMessage: 47 | Type: String 48 | 49 | 50 | emailVerificationSubject: 51 | Type: String 52 | 53 | 54 | emailVerificationMessage: 55 | Type: String 56 | 57 | 58 | 59 | defaultPasswordPolicy: 60 | Type: String 61 | 62 | 63 | passwordPolicyMinLength: 64 | Type: Number 65 | 66 | 67 | passwordPolicyCharacters: 68 | Type: CommaDelimitedList 69 | 70 | 71 | requiredAttributes: 72 | Type: CommaDelimitedList 73 | 74 | 75 | userpoolClientGenerateSecret: 76 | Type: String 77 | 78 | 79 | userpoolClientRefreshTokenValidity: 80 | Type: Number 81 | 82 | 83 | userpoolClientWriteAttributes: 84 | Type: CommaDelimitedList 85 | 86 | 87 | userpoolClientReadAttributes: 88 | Type: CommaDelimitedList 89 | 90 | userpoolClientLambdaRole: 91 | Type: String 92 | 93 | 94 | 95 | userpoolClientSetAttributes: 96 | Type: String 97 | 98 | sharedId: 99 | Type: String 100 | 101 | 102 | resourceName: 103 | Type: String 104 | 105 | 106 | authSelections: 107 | Type: String 108 | 109 | 110 | 111 | 112 | useDefault: 113 | Type: String 114 | 115 | 116 | 117 | userPoolGroupList: 118 | Type: CommaDelimitedList 119 | 120 | serviceName: 121 | Type: String 122 | 123 | 124 | 125 | usernameCaseSensitive: 126 | Type: String 127 | 128 | 129 | dependsOn: 130 | Type: CommaDelimitedList 131 | 132 | Conditions: 133 | ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ] 134 | 135 | Resources: 136 | 137 | 138 | # BEGIN SNS ROLE RESOURCE 139 | SNSRole: 140 | # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process 141 | Type: AWS::IAM::Role 142 | Properties: 143 | RoleName: !If [ShouldNotCreateEnvResources, 'amplifff86daff_sns-role', !Join ['',[ 'sns', 'ff86daff', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 144 | AssumeRolePolicyDocument: 145 | Version: "2012-10-17" 146 | Statement: 147 | - Sid: "" 148 | Effect: "Allow" 149 | Principal: 150 | Service: "cognito-idp.amazonaws.com" 151 | Action: 152 | - "sts:AssumeRole" 153 | Condition: 154 | StringEquals: 155 | sts:ExternalId: amplifff86daff_role_external_id 156 | Policies: 157 | - 158 | PolicyName: amplifff86daff-sns-policy 159 | PolicyDocument: 160 | Version: "2012-10-17" 161 | Statement: 162 | - 163 | Effect: "Allow" 164 | Action: 165 | - "sns:Publish" 166 | Resource: "*" 167 | # BEGIN USER POOL RESOURCES 168 | UserPool: 169 | # Created upon user selection 170 | # Depends on SNS Role for Arn if MFA is enabled 171 | Type: AWS::Cognito::UserPool 172 | UpdateReplacePolicy: Retain 173 | Properties: 174 | UserPoolName: !If [ShouldNotCreateEnvResources, !Ref userPoolName, !Join ['',[!Ref userPoolName, '-', !Ref env]]] 175 | 176 | 177 | UsernameConfiguration: 178 | CaseSensitive: false 179 | 180 | Schema: 181 | 182 | - 183 | Name: email 184 | Required: true 185 | Mutable: true 186 | 187 | 188 | 189 | 190 | AutoVerifiedAttributes: !Ref autoVerifiedAttributes 191 | 192 | 193 | EmailVerificationMessage: !Ref emailVerificationMessage 194 | EmailVerificationSubject: !Ref emailVerificationSubject 195 | 196 | Policies: 197 | PasswordPolicy: 198 | MinimumLength: !Ref passwordPolicyMinLength 199 | RequireLowercase: false 200 | RequireNumbers: false 201 | RequireSymbols: false 202 | RequireUppercase: false 203 | 204 | MfaConfiguration: !Ref mfaConfiguration 205 | SmsVerificationMessage: !Ref smsVerificationMessage 206 | SmsConfiguration: 207 | SnsCallerArn: !GetAtt SNSRole.Arn 208 | ExternalId: amplifff86daff_role_external_id 209 | 210 | 211 | UserPoolClientWeb: 212 | # Created provide application access to user pool 213 | # Depends on UserPool for ID reference 214 | Type: "AWS::Cognito::UserPoolClient" 215 | Properties: 216 | ClientName: amplifff86daff_app_clientWeb 217 | 218 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 219 | UserPoolId: !Ref UserPool 220 | DependsOn: UserPool 221 | UserPoolClient: 222 | # Created provide application access to user pool 223 | # Depends on UserPool for ID reference 224 | Type: "AWS::Cognito::UserPoolClient" 225 | Properties: 226 | ClientName: amplifff86daff_app_client 227 | 228 | GenerateSecret: !Ref userpoolClientGenerateSecret 229 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 230 | UserPoolId: !Ref UserPool 231 | DependsOn: UserPool 232 | # BEGIN USER POOL LAMBDA RESOURCES 233 | UserPoolClientRole: 234 | # Created to execute Lambda which gets userpool app client config values 235 | Type: 'AWS::IAM::Role' 236 | Properties: 237 | RoleName: !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',['upClientLambdaRole', 'ff86daff', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 238 | AssumeRolePolicyDocument: 239 | Version: '2012-10-17' 240 | Statement: 241 | - Effect: Allow 242 | Principal: 243 | Service: 244 | - lambda.amazonaws.com 245 | Action: 246 | - 'sts:AssumeRole' 247 | DependsOn: UserPoolClient 248 | UserPoolClientLambda: 249 | # Lambda which gets userpool app client config values 250 | # Depends on UserPool for id 251 | # Depends on UserPoolClientRole for role ARN 252 | Type: 'AWS::Lambda::Function' 253 | Properties: 254 | Code: 255 | ZipFile: !Join 256 | - |+ 257 | - - 'const response = require(''cfn-response'');' 258 | - 'const aws = require(''aws-sdk'');' 259 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 260 | - 'exports.handler = (event, context, callback) => {' 261 | - ' if (event.RequestType == ''Delete'') { ' 262 | - ' response.send(event, context, response.SUCCESS, {})' 263 | - ' }' 264 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 265 | - ' const params = {' 266 | - ' ClientId: event.ResourceProperties.clientId,' 267 | - ' UserPoolId: event.ResourceProperties.userpoolId' 268 | - ' };' 269 | - ' identity.describeUserPoolClient(params).promise()' 270 | - ' .then((res) => {' 271 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});' 272 | - ' })' 273 | - ' .catch((err) => {' 274 | - ' response.send(event, context, response.FAILED, {err});' 275 | - ' });' 276 | - ' }' 277 | - '};' 278 | Handler: index.handler 279 | Runtime: nodejs10.x 280 | Timeout: '300' 281 | Role: !GetAtt 282 | - UserPoolClientRole 283 | - Arn 284 | DependsOn: UserPoolClientRole 285 | UserPoolClientLambdaPolicy: 286 | # Sets userpool policy for the role that executes the Userpool Client Lambda 287 | # Depends on UserPool for Arn 288 | # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing 289 | Type: 'AWS::IAM::Policy' 290 | Properties: 291 | PolicyName: amplifff86daff_userpoolclient_lambda_iam_policy 292 | Roles: 293 | - !Ref UserPoolClientRole 294 | PolicyDocument: 295 | Version: '2012-10-17' 296 | Statement: 297 | - Effect: Allow 298 | Action: 299 | - 'cognito-idp:DescribeUserPoolClient' 300 | Resource: !GetAtt UserPool.Arn 301 | DependsOn: UserPoolClientLambda 302 | UserPoolClientLogPolicy: 303 | # Sets log policy for the role that executes the Userpool Client Lambda 304 | # Depends on UserPool for Arn 305 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 306 | Type: 'AWS::IAM::Policy' 307 | Properties: 308 | PolicyName: amplifff86daff_userpoolclient_lambda_log_policy 309 | Roles: 310 | - !Ref UserPoolClientRole 311 | PolicyDocument: 312 | Version: 2012-10-17 313 | Statement: 314 | - Effect: Allow 315 | Action: 316 | - 'logs:CreateLogGroup' 317 | - 'logs:CreateLogStream' 318 | - 'logs:PutLogEvents' 319 | Resource: !Sub 320 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 321 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref UserPoolClientLambda} 322 | DependsOn: UserPoolClientLambdaPolicy 323 | UserPoolClientInputs: 324 | # Values passed to Userpool client Lambda 325 | # Depends on UserPool for Id 326 | # Depends on UserPoolClient for Id 327 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 328 | Type: 'Custom::LambdaCallout' 329 | Properties: 330 | ServiceToken: !GetAtt UserPoolClientLambda.Arn 331 | clientId: !Ref UserPoolClient 332 | userpoolId: !Ref UserPool 333 | DependsOn: UserPoolClientLogPolicy 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | # BEGIN IDENTITY POOL RESOURCES 342 | 343 | 344 | IdentityPool: 345 | # Always created 346 | Type: AWS::Cognito::IdentityPool 347 | Properties: 348 | IdentityPoolName: !If [ShouldNotCreateEnvResources, 'amplifynextff86daff_identitypool_ff86daff', !Join ['',['amplifynextff86daff_identitypool_ff86daff', '__', !Ref env]]] 349 | 350 | CognitoIdentityProviders: 351 | - ClientId: !Ref UserPoolClient 352 | ProviderName: !Sub 353 | - cognito-idp.${region}.amazonaws.com/${client} 354 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 355 | - ClientId: !Ref UserPoolClientWeb 356 | ProviderName: !Sub 357 | - cognito-idp.${region}.amazonaws.com/${client} 358 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 359 | 360 | AllowUnauthenticatedIdentities: !Ref allowUnauthenticatedIdentities 361 | 362 | 363 | DependsOn: UserPoolClientInputs 364 | 365 | 366 | IdentityPoolRoleMap: 367 | # Created to map Auth and Unauth roles to the identity pool 368 | # Depends on Identity Pool for ID ref 369 | Type: AWS::Cognito::IdentityPoolRoleAttachment 370 | Properties: 371 | IdentityPoolId: !Ref IdentityPool 372 | Roles: 373 | unauthenticated: !Ref unauthRoleArn 374 | authenticated: !Ref authRoleArn 375 | DependsOn: IdentityPool 376 | 377 | 378 | Outputs : 379 | 380 | IdentityPoolId: 381 | Value: !Ref 'IdentityPool' 382 | Description: Id for the identity pool 383 | IdentityPoolName: 384 | Value: !GetAtt IdentityPool.Name 385 | 386 | 387 | 388 | 389 | UserPoolId: 390 | Value: !Ref 'UserPool' 391 | Description: Id for the user pool 392 | UserPoolName: 393 | Value: !Ref userPoolName 394 | AppClientIDWeb: 395 | Value: !Ref 'UserPoolClientWeb' 396 | Description: The user pool app client id for web 397 | AppClientID: 398 | Value: !Ref 'UserPoolClient' 399 | Description: The user pool app client id 400 | AppClientSecret: 401 | Value: !GetAtt UserPoolClientInputs.appSecret 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /amplify-next/amplify/backend/auth/amplifynextff86daff/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "amplifynextff86daff_identitypool_ff86daff", 3 | "allowUnauthenticatedIdentities": true, 4 | "resourceNameTruncated": "amplifff86daff", 5 | "userPoolName": "amplifynextff86daff_userpool_ff86daff", 6 | "autoVerifiedAttributes": [ 7 | "email" 8 | ], 9 | "mfaConfiguration": "OFF", 10 | "mfaTypes": [ 11 | "SMS Text Message" 12 | ], 13 | "smsAuthenticationMessage": "Your authentication code is {####}", 14 | "smsVerificationMessage": "Your verification code is {####}", 15 | "emailVerificationSubject": "Your verification code", 16 | "emailVerificationMessage": "Your verification code is {####}", 17 | "defaultPasswordPolicy": false, 18 | "passwordPolicyMinLength": 8, 19 | "passwordPolicyCharacters": [], 20 | "requiredAttributes": [ 21 | "email" 22 | ], 23 | "userpoolClientGenerateSecret": true, 24 | "userpoolClientRefreshTokenValidity": 30, 25 | "userpoolClientWriteAttributes": [ 26 | "email" 27 | ], 28 | "userpoolClientReadAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientLambdaRole": "amplifff86daff_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "sharedId": "ff86daff", 34 | "resourceName": "amplifynextff86daff", 35 | "authSelections": "identityPoolAndUserPool", 36 | "authRoleArn": { 37 | "Fn::GetAtt": [ 38 | "AuthRole", 39 | "Arn" 40 | ] 41 | }, 42 | "unauthRoleArn": { 43 | "Fn::GetAtt": [ 44 | "UnauthRole", 45 | "Arn" 46 | ] 47 | }, 48 | "useDefault": "default", 49 | "userPoolGroupList": [], 50 | "serviceName": "Cognito", 51 | "usernameCaseSensitive": false, 52 | "dependsOn": [] 53 | } -------------------------------------------------------------------------------- /amplify-next/amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "NextBlog": { 4 | "service": "AppSync", 5 | "providerPlugin": "awscloudformation", 6 | "output": { 7 | "authConfig": { 8 | "defaultAuthentication": { 9 | "authenticationType": "API_KEY", 10 | "apiKeyConfig": { 11 | "apiKeyExpirationDays": 365, 12 | "description": "public" 13 | } 14 | }, 15 | "additionalAuthenticationProviders": [ 16 | { 17 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 18 | "userPoolConfig": { 19 | "userPoolId": "authamplifynextff86daff" 20 | } 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | }, 27 | "auth": { 28 | "amplifynextff86daff": { 29 | "service": "Cognito", 30 | "providerPlugin": "awscloudformation", 31 | "dependsOn": [], 32 | "customAuth": false 33 | } 34 | }, 35 | "storage": { 36 | "projectimages": { 37 | "service": "S3", 38 | "providerPlugin": "awscloudformation" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /amplify-next/amplify/backend/storage/projectimages/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucketName": "amplifynext1d6d0204f91f4304b58a3b41df8e3f57", 3 | "authPolicyName": "s3_amplify_47f8d3d0", 4 | "unauthPolicyName": "s3_amplify_47f8d3d0", 5 | "authRoleName": { 6 | "Ref": "AuthRoleName" 7 | }, 8 | "unauthRoleName": { 9 | "Ref": "UnauthRoleName" 10 | }, 11 | "selectedGuestPermissions": [ 12 | "s3:GetObject", 13 | "s3:ListBucket" 14 | ], 15 | "selectedAuthenticatedPermissions": [ 16 | "s3:PutObject", 17 | "s3:GetObject", 18 | "s3:ListBucket", 19 | "s3:DeleteObject" 20 | ], 21 | "s3PermissionsAuthenticatedPublic": "s3:PutObject,s3:GetObject,s3:DeleteObject", 22 | "s3PublicPolicy": "Public_policy_9fdef19d", 23 | "s3PermissionsAuthenticatedUploads": "s3:PutObject", 24 | "s3UploadsPolicy": "Uploads_policy_9fdef19d", 25 | "s3PermissionsAuthenticatedProtected": "s3:PutObject,s3:GetObject,s3:DeleteObject", 26 | "s3ProtectedPolicy": "Protected_policy_1a137b68", 27 | "s3PermissionsAuthenticatedPrivate": "s3:PutObject,s3:GetObject,s3:DeleteObject", 28 | "s3PrivatePolicy": "Private_policy_1a137b68", 29 | "AuthenticatedAllowList": "ALLOW", 30 | "s3ReadPolicy": "read_policy_9fdef19d", 31 | "s3PermissionsGuestPublic": "s3:GetObject", 32 | "s3PermissionsGuestUploads": "DISALLOW", 33 | "GuestAllowList": "ALLOW", 34 | "triggerFunction": "NONE" 35 | } -------------------------------------------------------------------------------- /amplify-next/amplify/backend/storage/projectimages/s3-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "S3 resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "bucketName": { 6 | "Type": "String" 7 | }, 8 | "authPolicyName": { 9 | "Type": "String" 10 | }, 11 | "unauthPolicyName": { 12 | "Type": "String" 13 | }, 14 | "authRoleName": { 15 | "Type": "String" 16 | }, 17 | "unauthRoleName": { 18 | "Type": "String" 19 | }, 20 | "s3PublicPolicy": { 21 | "Type": "String", 22 | "Default" : "NONE" 23 | }, 24 | "s3PrivatePolicy": { 25 | "Type": "String", 26 | "Default" : "NONE" 27 | }, 28 | "s3ProtectedPolicy": { 29 | "Type": "String", 30 | "Default" : "NONE" 31 | }, 32 | "s3UploadsPolicy": { 33 | "Type": "String", 34 | "Default" : "NONE" 35 | }, 36 | "s3ReadPolicy": { 37 | "Type": "String", 38 | "Default" : "NONE" 39 | }, 40 | "s3PermissionsAuthenticatedPublic": { 41 | "Type": "String", 42 | "Default" : "DISALLOW" 43 | }, 44 | "s3PermissionsAuthenticatedProtected": { 45 | "Type": "String", 46 | "Default" : "DISALLOW" 47 | }, 48 | "s3PermissionsAuthenticatedPrivate": { 49 | "Type": "String", 50 | "Default" : "DISALLOW" 51 | }, 52 | "s3PermissionsAuthenticatedUploads": { 53 | "Type": "String", 54 | "Default" : "DISALLOW" 55 | }, 56 | "s3PermissionsGuestPublic": { 57 | "Type": "String", 58 | "Default" : "DISALLOW" 59 | }, 60 | "s3PermissionsGuestUploads": { 61 | "Type": "String", 62 | "Default" : "DISALLOW" }, 63 | "AuthenticatedAllowList": { 64 | "Type": "String", 65 | "Default" : "DISALLOW" 66 | }, 67 | "GuestAllowList": { 68 | "Type": "String", 69 | "Default" : "DISALLOW" 70 | }, 71 | "selectedGuestPermissions": { 72 | "Type": "CommaDelimitedList", 73 | "Default" : "NONE" 74 | }, 75 | "selectedAuthenticatedPermissions": { 76 | "Type": "CommaDelimitedList", 77 | "Default" : "NONE" 78 | }, 79 | "env": { 80 | "Type": "String" 81 | }, 82 | "triggerFunction": { 83 | "Type": "String" 84 | } 85 | 86 | 87 | }, 88 | "Conditions": { 89 | "ShouldNotCreateEnvResources": { 90 | "Fn::Equals": [ 91 | { 92 | "Ref": "env" 93 | }, 94 | "NONE" 95 | ] 96 | }, 97 | "CreateAuthPublic": { 98 | "Fn::Not" : [{ 99 | "Fn::Equals" : [ 100 | {"Ref" : "s3PermissionsAuthenticatedPublic"}, 101 | "DISALLOW" 102 | ] 103 | }] 104 | }, 105 | "CreateAuthProtected": { 106 | "Fn::Not" : [{ 107 | "Fn::Equals" : [ 108 | {"Ref" : "s3PermissionsAuthenticatedProtected"}, 109 | "DISALLOW" 110 | ] 111 | }] 112 | }, 113 | "CreateAuthPrivate": { 114 | "Fn::Not" : [{ 115 | "Fn::Equals" : [ 116 | {"Ref" : "s3PermissionsAuthenticatedPrivate"}, 117 | "DISALLOW" 118 | ] 119 | }] 120 | }, 121 | "CreateAuthUploads": { 122 | "Fn::Not" : [{ 123 | "Fn::Equals" : [ 124 | {"Ref" : "s3PermissionsAuthenticatedUploads"}, 125 | "DISALLOW" 126 | ] 127 | }] 128 | }, 129 | "CreateGuestPublic": { 130 | "Fn::Not" : [{ 131 | "Fn::Equals" : [ 132 | {"Ref" : "s3PermissionsGuestPublic"}, 133 | "DISALLOW" 134 | ] 135 | }] 136 | }, 137 | "CreateGuestUploads": { 138 | "Fn::Not" : [{ 139 | "Fn::Equals" : [ 140 | {"Ref" : "s3PermissionsGuestUploads"}, 141 | "DISALLOW" 142 | ] 143 | }] 144 | }, 145 | "AuthReadAndList": { 146 | "Fn::Not" : [{ 147 | "Fn::Equals" : [ 148 | {"Ref" : "AuthenticatedAllowList"}, 149 | "DISALLOW" 150 | ] 151 | }] 152 | }, 153 | "GuestReadAndList": { 154 | "Fn::Not" : [{ 155 | "Fn::Equals" : [ 156 | {"Ref" : "GuestAllowList"}, 157 | "DISALLOW" 158 | ] 159 | }] 160 | } 161 | }, 162 | "Resources": { 163 | "S3Bucket": { 164 | "Type": "AWS::S3::Bucket", 165 | 166 | "DeletionPolicy" : "Retain", 167 | "Properties": { 168 | "BucketName": { 169 | "Fn::If": [ 170 | "ShouldNotCreateEnvResources", 171 | { 172 | "Ref": "bucketName" 173 | }, 174 | { 175 | "Fn::Join": [ 176 | "", 177 | [ 178 | { 179 | "Ref": "bucketName" 180 | }, 181 | { 182 | "Fn::Select": [ 183 | 3, 184 | { 185 | "Fn::Split": [ 186 | "-", 187 | { 188 | "Ref": "AWS::StackName" 189 | } 190 | ] 191 | } 192 | ] 193 | }, 194 | "-", 195 | { 196 | "Ref": "env" 197 | } 198 | ] 199 | ] 200 | } 201 | ] 202 | }, 203 | 204 | "CorsConfiguration": { 205 | "CorsRules": [ 206 | { 207 | "AllowedHeaders": [ 208 | "*" 209 | ], 210 | "AllowedMethods": [ 211 | "GET", 212 | "HEAD", 213 | "PUT", 214 | "POST", 215 | "DELETE" 216 | ], 217 | "AllowedOrigins": [ 218 | "*" 219 | ], 220 | "ExposedHeaders": [ 221 | "x-amz-server-side-encryption", 222 | "x-amz-request-id", 223 | "x-amz-id-2", 224 | "ETag" 225 | ], 226 | "Id": "S3CORSRuleId1", 227 | "MaxAge": "3000" 228 | } 229 | ] 230 | } 231 | } 232 | }, 233 | 234 | 235 | "S3AuthPublicPolicy": { 236 | "DependsOn": [ 237 | "S3Bucket" 238 | ], 239 | "Condition": "CreateAuthPublic", 240 | "Type": "AWS::IAM::Policy", 241 | "Properties": { 242 | "PolicyName": { 243 | "Ref": "s3PublicPolicy" 244 | }, 245 | "Roles": [ 246 | { 247 | "Ref": "authRoleName" 248 | } 249 | ], 250 | "PolicyDocument": { 251 | "Version": "2012-10-17", 252 | "Statement": [ 253 | { 254 | "Effect": "Allow", 255 | "Action": { 256 | "Fn::Split" : [ "," , { 257 | "Ref": "s3PermissionsAuthenticatedPublic" 258 | } ] 259 | }, 260 | "Resource": [ 261 | { 262 | "Fn::Join": [ 263 | "", 264 | [ 265 | "arn:aws:s3:::", 266 | { 267 | "Ref": "S3Bucket" 268 | }, 269 | "/public/*" 270 | ] 271 | ] 272 | } 273 | ] 274 | } 275 | ] 276 | } 277 | } 278 | }, 279 | "S3AuthProtectedPolicy": { 280 | "DependsOn": [ 281 | "S3Bucket" 282 | ], 283 | "Condition": "CreateAuthProtected", 284 | "Type": "AWS::IAM::Policy", 285 | "Properties": { 286 | "PolicyName": { 287 | "Ref": "s3ProtectedPolicy" 288 | }, 289 | "Roles": [ 290 | { 291 | "Ref": "authRoleName" 292 | } 293 | ], 294 | "PolicyDocument": { 295 | "Version": "2012-10-17", 296 | "Statement": [ 297 | { 298 | "Effect": "Allow", 299 | "Action": { 300 | "Fn::Split" : [ "," , { 301 | "Ref": "s3PermissionsAuthenticatedProtected" 302 | } ] 303 | }, 304 | "Resource": [ 305 | { 306 | "Fn::Join": [ 307 | "", 308 | [ 309 | "arn:aws:s3:::", 310 | { 311 | "Ref": "S3Bucket" 312 | }, 313 | "/protected/${cognito-identity.amazonaws.com:sub}/*" 314 | ] 315 | ] 316 | } 317 | ] 318 | } 319 | ] 320 | } 321 | } 322 | }, 323 | "S3AuthPrivatePolicy": { 324 | "DependsOn": [ 325 | "S3Bucket" 326 | ], 327 | "Condition": "CreateAuthPrivate", 328 | "Type": "AWS::IAM::Policy", 329 | "Properties": { 330 | "PolicyName": { 331 | "Ref": "s3PrivatePolicy" 332 | }, 333 | "Roles": [ 334 | { 335 | "Ref": "authRoleName" 336 | } 337 | ], 338 | "PolicyDocument": { 339 | "Version": "2012-10-17", 340 | "Statement": [ 341 | { 342 | "Effect": "Allow", 343 | "Action": { 344 | "Fn::Split" : [ "," , { 345 | "Ref": "s3PermissionsAuthenticatedPrivate" 346 | } ] 347 | }, 348 | "Resource": [ 349 | { 350 | "Fn::Join": [ 351 | "", 352 | [ 353 | "arn:aws:s3:::", 354 | { 355 | "Ref": "S3Bucket" 356 | }, 357 | "/private/${cognito-identity.amazonaws.com:sub}/*" 358 | ] 359 | ] 360 | } 361 | ] 362 | } 363 | ] 364 | } 365 | } 366 | }, 367 | "S3AuthUploadPolicy": { 368 | "DependsOn": [ 369 | "S3Bucket" 370 | ], 371 | "Condition": "CreateAuthUploads", 372 | "Type": "AWS::IAM::Policy", 373 | "Properties": { 374 | "PolicyName": { 375 | "Ref": "s3UploadsPolicy" 376 | }, 377 | "Roles": [ 378 | { 379 | "Ref": "authRoleName" 380 | } 381 | ], 382 | "PolicyDocument": { 383 | "Version": "2012-10-17", 384 | "Statement": [ 385 | { 386 | "Effect": "Allow", 387 | "Action": { 388 | "Fn::Split" : [ "," , { 389 | "Ref": "s3PermissionsAuthenticatedUploads" 390 | } ] 391 | }, 392 | "Resource": [ 393 | { 394 | "Fn::Join": [ 395 | "", 396 | [ 397 | "arn:aws:s3:::", 398 | { 399 | "Ref": "S3Bucket" 400 | }, 401 | "/uploads/*" 402 | ] 403 | ] 404 | } 405 | ] 406 | } 407 | ] 408 | } 409 | } 410 | }, 411 | "S3AuthReadPolicy": { 412 | "DependsOn": [ 413 | "S3Bucket" 414 | ], 415 | "Condition": "AuthReadAndList", 416 | "Type": "AWS::IAM::Policy", 417 | "Properties": { 418 | "PolicyName": { 419 | "Ref": "s3ReadPolicy" 420 | }, 421 | "Roles": [ 422 | { 423 | "Ref": "authRoleName" 424 | } 425 | ], 426 | "PolicyDocument": { 427 | "Version": "2012-10-17", 428 | "Statement": [ 429 | { 430 | "Effect": "Allow", 431 | "Action": [ 432 | "s3:GetObject" 433 | ], 434 | "Resource": [ 435 | { 436 | "Fn::Join": [ 437 | "", 438 | [ 439 | "arn:aws:s3:::", 440 | { 441 | "Ref": "S3Bucket" 442 | }, 443 | "/protected/*" 444 | ] 445 | ] 446 | } 447 | ] 448 | }, 449 | { 450 | "Effect": "Allow", 451 | "Action": [ 452 | "s3:ListBucket" 453 | ], 454 | "Resource": [ 455 | { 456 | "Fn::Join": [ 457 | "", 458 | [ 459 | "arn:aws:s3:::", 460 | { 461 | "Ref": "S3Bucket" 462 | } 463 | ] 464 | ] 465 | } 466 | ], 467 | "Condition": { 468 | "StringLike": { 469 | "s3:prefix": [ 470 | "public/", 471 | "public/*", 472 | "protected/", 473 | "protected/*", 474 | "private/${cognito-identity.amazonaws.com:sub}/", 475 | "private/${cognito-identity.amazonaws.com:sub}/*" 476 | ] 477 | } 478 | } 479 | } 480 | ] 481 | } 482 | } 483 | }, 484 | "S3GuestPublicPolicy": { 485 | "DependsOn": [ 486 | "S3Bucket" 487 | ], 488 | "Condition": "CreateGuestPublic", 489 | "Type": "AWS::IAM::Policy", 490 | "Properties": { 491 | "PolicyName": { 492 | "Ref": "s3PublicPolicy" 493 | }, 494 | "Roles": [ 495 | { 496 | "Ref": "unauthRoleName" 497 | } 498 | ], 499 | "PolicyDocument": { 500 | "Version": "2012-10-17", 501 | "Statement": [ 502 | { 503 | "Effect": "Allow", 504 | "Action": { 505 | "Fn::Split" : [ "," , { 506 | "Ref": "s3PermissionsGuestPublic" 507 | } ] 508 | }, 509 | "Resource": [ 510 | { 511 | "Fn::Join": [ 512 | "", 513 | [ 514 | "arn:aws:s3:::", 515 | { 516 | "Ref": "S3Bucket" 517 | }, 518 | "/public/*" 519 | ] 520 | ] 521 | } 522 | ] 523 | } 524 | ] 525 | } 526 | } 527 | }, 528 | "S3GuestUploadPolicy": { 529 | "DependsOn": [ 530 | "S3Bucket" 531 | ], 532 | "Condition": "CreateGuestUploads", 533 | "Type": "AWS::IAM::Policy", 534 | "Properties": { 535 | "PolicyName": { 536 | "Ref": "s3UploadsPolicy" 537 | }, 538 | "Roles": [ 539 | { 540 | "Ref": "unauthRoleName" 541 | } 542 | ], 543 | "PolicyDocument": { 544 | "Version": "2012-10-17", 545 | "Statement": [ 546 | { 547 | "Effect": "Allow", 548 | "Action": { 549 | "Fn::Split" : [ "," , { 550 | "Ref": "s3PermissionsGuestUploads" 551 | } ] 552 | }, 553 | "Resource": [ 554 | { 555 | "Fn::Join": [ 556 | "", 557 | [ 558 | "arn:aws:s3:::", 559 | { 560 | "Ref": "S3Bucket" 561 | }, 562 | "/uploads/*" 563 | ] 564 | ] 565 | } 566 | ] 567 | } 568 | ] 569 | } 570 | } 571 | }, 572 | "S3GuestReadPolicy": { 573 | "DependsOn": [ 574 | "S3Bucket" 575 | ], 576 | "Condition": "GuestReadAndList", 577 | "Type": "AWS::IAM::Policy", 578 | "Properties": { 579 | "PolicyName": { 580 | "Ref": "s3ReadPolicy" 581 | }, 582 | "Roles": [ 583 | { 584 | "Ref": "unauthRoleName" 585 | } 586 | ], 587 | "PolicyDocument": { 588 | "Version": "2012-10-17", 589 | "Statement": [ 590 | { 591 | "Effect": "Allow", 592 | "Action": [ 593 | "s3:GetObject" 594 | ], 595 | "Resource": [ 596 | { 597 | "Fn::Join": [ 598 | "", 599 | [ 600 | "arn:aws:s3:::", 601 | { 602 | "Ref": "S3Bucket" 603 | }, 604 | "/protected/*" 605 | ] 606 | ] 607 | } 608 | ] 609 | }, 610 | { 611 | "Effect": "Allow", 612 | "Action": [ 613 | "s3:ListBucket" 614 | ], 615 | "Resource": [ 616 | { 617 | "Fn::Join": [ 618 | "", 619 | [ 620 | "arn:aws:s3:::", 621 | { 622 | "Ref": "S3Bucket" 623 | } 624 | ] 625 | ] 626 | } 627 | ], 628 | "Condition": { 629 | "StringLike": { 630 | "s3:prefix": [ 631 | "public/", 632 | "public/*", 633 | "protected/", 634 | "protected/*" 635 | ] 636 | } 637 | } 638 | } 639 | ] 640 | } 641 | } 642 | } 643 | }, 644 | "Outputs": { 645 | "BucketName": { 646 | "Value": { 647 | "Ref": "S3Bucket" 648 | }, 649 | "Description": "Bucket name for the S3 bucket" 650 | }, 651 | "Region": { 652 | "Value": { 653 | "Ref": "AWS::Region" 654 | } 655 | } 656 | } 657 | } 658 | -------------------------------------------------------------------------------- /amplify-next/amplify/backend/storage/projectimages/storage-params.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /amplify-next/amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /amplify-next/amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "validatetypenamereservedwords": true, 6 | "useexperimentalpipelinedtransformer": false, 7 | "enableiterativegsiupdates": false 8 | }, 9 | "frontend-ios": { 10 | "enablexcodeintegration": true 11 | }, 12 | "auth": { 13 | "enablecaseinsensitivity": true 14 | }, 15 | "codegen": { 16 | "useappsyncmodelgenplugin": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /amplify-next/configureAmplify.js: -------------------------------------------------------------------------------- 1 | import Amplify from 'aws-amplify' 2 | import config from './aws-exports' 3 | Amplify.configure(config) -------------------------------------------------------------------------------- /amplify-next/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const createPost = /* GraphQL */ ` 5 | mutation CreatePost( 6 | $input: CreatePostInput! 7 | $condition: ModelPostConditionInput 8 | ) { 9 | createPost(input: $input, condition: $condition) { 10 | id 11 | title 12 | content 13 | username 14 | coverImage 15 | createdAt 16 | updatedAt 17 | } 18 | } 19 | `; 20 | export const updatePost = /* GraphQL */ ` 21 | mutation UpdatePost( 22 | $input: UpdatePostInput! 23 | $condition: ModelPostConditionInput 24 | ) { 25 | updatePost(input: $input, condition: $condition) { 26 | id 27 | title 28 | content 29 | username 30 | coverImage 31 | createdAt 32 | updatedAt 33 | } 34 | } 35 | `; 36 | export const deletePost = /* GraphQL */ ` 37 | mutation DeletePost( 38 | $input: DeletePostInput! 39 | $condition: ModelPostConditionInput 40 | ) { 41 | deletePost(input: $input, condition: $condition) { 42 | id 43 | title 44 | content 45 | username 46 | coverImage 47 | createdAt 48 | updatedAt 49 | } 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /amplify-next/graphql/queries.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const getPost = /* GraphQL */ ` 5 | query GetPost($id: ID!) { 6 | getPost(id: $id) { 7 | id 8 | title 9 | content 10 | username 11 | coverImage 12 | createdAt 13 | updatedAt 14 | } 15 | } 16 | `; 17 | export const listPosts = /* GraphQL */ ` 18 | query ListPosts( 19 | $filter: ModelPostFilterInput 20 | $limit: Int 21 | $nextToken: String 22 | ) { 23 | listPosts(filter: $filter, limit: $limit, nextToken: $nextToken) { 24 | items { 25 | id 26 | title 27 | content 28 | username 29 | coverImage 30 | createdAt 31 | updatedAt 32 | } 33 | nextToken 34 | } 35 | } 36 | `; 37 | export const postsByUsername = /* GraphQL */ ` 38 | query PostsByUsername( 39 | $username: String 40 | $sortDirection: ModelSortDirection 41 | $filter: ModelPostFilterInput 42 | $limit: Int 43 | $nextToken: String 44 | ) { 45 | postsByUsername( 46 | username: $username 47 | sortDirection: $sortDirection 48 | filter: $filter 49 | limit: $limit 50 | nextToken: $nextToken 51 | ) { 52 | items { 53 | id 54 | title 55 | content 56 | username 57 | coverImage 58 | createdAt 59 | updatedAt 60 | } 61 | nextToken 62 | } 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /amplify-next/graphql/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : { 3 | "__schema" : { 4 | "queryType" : { 5 | "name" : "Query" 6 | }, 7 | "mutationType" : { 8 | "name" : "Mutation" 9 | }, 10 | "subscriptionType" : { 11 | "name" : "Subscription" 12 | }, 13 | "types" : [ { 14 | "kind" : "OBJECT", 15 | "name" : "Query", 16 | "description" : null, 17 | "fields" : [ { 18 | "name" : "getPost", 19 | "description" : null, 20 | "args" : [ { 21 | "name" : "id", 22 | "description" : null, 23 | "type" : { 24 | "kind" : "NON_NULL", 25 | "name" : null, 26 | "ofType" : { 27 | "kind" : "SCALAR", 28 | "name" : "ID", 29 | "ofType" : null 30 | } 31 | }, 32 | "defaultValue" : null 33 | } ], 34 | "type" : { 35 | "kind" : "OBJECT", 36 | "name" : "Post", 37 | "ofType" : null 38 | }, 39 | "isDeprecated" : false, 40 | "deprecationReason" : null 41 | }, { 42 | "name" : "listPosts", 43 | "description" : null, 44 | "args" : [ { 45 | "name" : "filter", 46 | "description" : null, 47 | "type" : { 48 | "kind" : "INPUT_OBJECT", 49 | "name" : "ModelPostFilterInput", 50 | "ofType" : null 51 | }, 52 | "defaultValue" : null 53 | }, { 54 | "name" : "limit", 55 | "description" : null, 56 | "type" : { 57 | "kind" : "SCALAR", 58 | "name" : "Int", 59 | "ofType" : null 60 | }, 61 | "defaultValue" : null 62 | }, { 63 | "name" : "nextToken", 64 | "description" : null, 65 | "type" : { 66 | "kind" : "SCALAR", 67 | "name" : "String", 68 | "ofType" : null 69 | }, 70 | "defaultValue" : null 71 | } ], 72 | "type" : { 73 | "kind" : "OBJECT", 74 | "name" : "ModelPostConnection", 75 | "ofType" : null 76 | }, 77 | "isDeprecated" : false, 78 | "deprecationReason" : null 79 | }, { 80 | "name" : "postsByUsername", 81 | "description" : null, 82 | "args" : [ { 83 | "name" : "username", 84 | "description" : null, 85 | "type" : { 86 | "kind" : "SCALAR", 87 | "name" : "String", 88 | "ofType" : null 89 | }, 90 | "defaultValue" : null 91 | }, { 92 | "name" : "sortDirection", 93 | "description" : null, 94 | "type" : { 95 | "kind" : "ENUM", 96 | "name" : "ModelSortDirection", 97 | "ofType" : null 98 | }, 99 | "defaultValue" : null 100 | }, { 101 | "name" : "filter", 102 | "description" : null, 103 | "type" : { 104 | "kind" : "INPUT_OBJECT", 105 | "name" : "ModelPostFilterInput", 106 | "ofType" : null 107 | }, 108 | "defaultValue" : null 109 | }, { 110 | "name" : "limit", 111 | "description" : null, 112 | "type" : { 113 | "kind" : "SCALAR", 114 | "name" : "Int", 115 | "ofType" : null 116 | }, 117 | "defaultValue" : null 118 | }, { 119 | "name" : "nextToken", 120 | "description" : null, 121 | "type" : { 122 | "kind" : "SCALAR", 123 | "name" : "String", 124 | "ofType" : null 125 | }, 126 | "defaultValue" : null 127 | } ], 128 | "type" : { 129 | "kind" : "OBJECT", 130 | "name" : "ModelPostConnection", 131 | "ofType" : null 132 | }, 133 | "isDeprecated" : false, 134 | "deprecationReason" : null 135 | } ], 136 | "inputFields" : null, 137 | "interfaces" : [ ], 138 | "enumValues" : null, 139 | "possibleTypes" : null 140 | }, { 141 | "kind" : "OBJECT", 142 | "name" : "Post", 143 | "description" : null, 144 | "fields" : [ { 145 | "name" : "id", 146 | "description" : null, 147 | "args" : [ ], 148 | "type" : { 149 | "kind" : "NON_NULL", 150 | "name" : null, 151 | "ofType" : { 152 | "kind" : "SCALAR", 153 | "name" : "ID", 154 | "ofType" : null 155 | } 156 | }, 157 | "isDeprecated" : false, 158 | "deprecationReason" : null 159 | }, { 160 | "name" : "title", 161 | "description" : null, 162 | "args" : [ ], 163 | "type" : { 164 | "kind" : "NON_NULL", 165 | "name" : null, 166 | "ofType" : { 167 | "kind" : "SCALAR", 168 | "name" : "String", 169 | "ofType" : null 170 | } 171 | }, 172 | "isDeprecated" : false, 173 | "deprecationReason" : null 174 | }, { 175 | "name" : "content", 176 | "description" : null, 177 | "args" : [ ], 178 | "type" : { 179 | "kind" : "NON_NULL", 180 | "name" : null, 181 | "ofType" : { 182 | "kind" : "SCALAR", 183 | "name" : "String", 184 | "ofType" : null 185 | } 186 | }, 187 | "isDeprecated" : false, 188 | "deprecationReason" : null 189 | }, { 190 | "name" : "username", 191 | "description" : null, 192 | "args" : [ ], 193 | "type" : { 194 | "kind" : "SCALAR", 195 | "name" : "String", 196 | "ofType" : null 197 | }, 198 | "isDeprecated" : false, 199 | "deprecationReason" : null 200 | }, { 201 | "name" : "coverImage", 202 | "description" : null, 203 | "args" : [ ], 204 | "type" : { 205 | "kind" : "SCALAR", 206 | "name" : "String", 207 | "ofType" : null 208 | }, 209 | "isDeprecated" : false, 210 | "deprecationReason" : null 211 | }, { 212 | "name" : "createdAt", 213 | "description" : null, 214 | "args" : [ ], 215 | "type" : { 216 | "kind" : "NON_NULL", 217 | "name" : null, 218 | "ofType" : { 219 | "kind" : "SCALAR", 220 | "name" : "AWSDateTime", 221 | "ofType" : null 222 | } 223 | }, 224 | "isDeprecated" : false, 225 | "deprecationReason" : null 226 | }, { 227 | "name" : "updatedAt", 228 | "description" : null, 229 | "args" : [ ], 230 | "type" : { 231 | "kind" : "NON_NULL", 232 | "name" : null, 233 | "ofType" : { 234 | "kind" : "SCALAR", 235 | "name" : "AWSDateTime", 236 | "ofType" : null 237 | } 238 | }, 239 | "isDeprecated" : false, 240 | "deprecationReason" : null 241 | } ], 242 | "inputFields" : null, 243 | "interfaces" : [ ], 244 | "enumValues" : null, 245 | "possibleTypes" : null 246 | }, { 247 | "kind" : "SCALAR", 248 | "name" : "ID", 249 | "description" : "Built-in ID", 250 | "fields" : null, 251 | "inputFields" : null, 252 | "interfaces" : null, 253 | "enumValues" : null, 254 | "possibleTypes" : null 255 | }, { 256 | "kind" : "SCALAR", 257 | "name" : "String", 258 | "description" : "Built-in String", 259 | "fields" : null, 260 | "inputFields" : null, 261 | "interfaces" : null, 262 | "enumValues" : null, 263 | "possibleTypes" : null 264 | }, { 265 | "kind" : "SCALAR", 266 | "name" : "AWSDateTime", 267 | "description" : "The `AWSDateTime` scalar type provided by AWS AppSync, represents a valid ***extended*** [ISO 8601 DateTime](https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations) string. In other words, this scalar type accepts datetime strings of the form `YYYY-MM-DDThh:mm:ss.SSSZ`. The scalar can also accept \"negative years\" of the form `-YYYY` which correspond to years before `0000`. For example, \"**-2017-01-01T00:00Z**\" and \"**-9999-01-01T00:00Z**\" are both valid datetime strings. The field after the two digit seconds field is a nanoseconds field. It can accept between 1 and 9 digits. So, for example, \"**1970-01-01T12:00:00.2Z**\", \"**1970-01-01T12:00:00.277Z**\" and \"**1970-01-01T12:00:00.123456789Z**\" are all valid datetime strings. The seconds and nanoseconds fields are optional (the seconds field must be specified if the nanoseconds field is to be used). The [time zone offset](https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators) is compulsory for this scalar. The time zone offset must either be `Z` (representing the UTC time zone) or be in the format `±hh:mm:ss`. The seconds field in the timezone offset will be considered valid even though it is not part of the ISO 8601 standard.", 268 | "fields" : null, 269 | "inputFields" : null, 270 | "interfaces" : null, 271 | "enumValues" : null, 272 | "possibleTypes" : null 273 | }, { 274 | "kind" : "OBJECT", 275 | "name" : "ModelPostConnection", 276 | "description" : null, 277 | "fields" : [ { 278 | "name" : "items", 279 | "description" : null, 280 | "args" : [ ], 281 | "type" : { 282 | "kind" : "LIST", 283 | "name" : null, 284 | "ofType" : { 285 | "kind" : "OBJECT", 286 | "name" : "Post", 287 | "ofType" : null 288 | } 289 | }, 290 | "isDeprecated" : false, 291 | "deprecationReason" : null 292 | }, { 293 | "name" : "nextToken", 294 | "description" : null, 295 | "args" : [ ], 296 | "type" : { 297 | "kind" : "SCALAR", 298 | "name" : "String", 299 | "ofType" : null 300 | }, 301 | "isDeprecated" : false, 302 | "deprecationReason" : null 303 | } ], 304 | "inputFields" : null, 305 | "interfaces" : [ ], 306 | "enumValues" : null, 307 | "possibleTypes" : null 308 | }, { 309 | "kind" : "INPUT_OBJECT", 310 | "name" : "ModelPostFilterInput", 311 | "description" : null, 312 | "fields" : null, 313 | "inputFields" : [ { 314 | "name" : "id", 315 | "description" : null, 316 | "type" : { 317 | "kind" : "INPUT_OBJECT", 318 | "name" : "ModelIDInput", 319 | "ofType" : null 320 | }, 321 | "defaultValue" : null 322 | }, { 323 | "name" : "title", 324 | "description" : null, 325 | "type" : { 326 | "kind" : "INPUT_OBJECT", 327 | "name" : "ModelStringInput", 328 | "ofType" : null 329 | }, 330 | "defaultValue" : null 331 | }, { 332 | "name" : "content", 333 | "description" : null, 334 | "type" : { 335 | "kind" : "INPUT_OBJECT", 336 | "name" : "ModelStringInput", 337 | "ofType" : null 338 | }, 339 | "defaultValue" : null 340 | }, { 341 | "name" : "username", 342 | "description" : null, 343 | "type" : { 344 | "kind" : "INPUT_OBJECT", 345 | "name" : "ModelStringInput", 346 | "ofType" : null 347 | }, 348 | "defaultValue" : null 349 | }, { 350 | "name" : "coverImage", 351 | "description" : null, 352 | "type" : { 353 | "kind" : "INPUT_OBJECT", 354 | "name" : "ModelStringInput", 355 | "ofType" : null 356 | }, 357 | "defaultValue" : null 358 | }, { 359 | "name" : "and", 360 | "description" : null, 361 | "type" : { 362 | "kind" : "LIST", 363 | "name" : null, 364 | "ofType" : { 365 | "kind" : "INPUT_OBJECT", 366 | "name" : "ModelPostFilterInput", 367 | "ofType" : null 368 | } 369 | }, 370 | "defaultValue" : null 371 | }, { 372 | "name" : "or", 373 | "description" : null, 374 | "type" : { 375 | "kind" : "LIST", 376 | "name" : null, 377 | "ofType" : { 378 | "kind" : "INPUT_OBJECT", 379 | "name" : "ModelPostFilterInput", 380 | "ofType" : null 381 | } 382 | }, 383 | "defaultValue" : null 384 | }, { 385 | "name" : "not", 386 | "description" : null, 387 | "type" : { 388 | "kind" : "INPUT_OBJECT", 389 | "name" : "ModelPostFilterInput", 390 | "ofType" : null 391 | }, 392 | "defaultValue" : null 393 | } ], 394 | "interfaces" : null, 395 | "enumValues" : null, 396 | "possibleTypes" : null 397 | }, { 398 | "kind" : "INPUT_OBJECT", 399 | "name" : "ModelIDInput", 400 | "description" : null, 401 | "fields" : null, 402 | "inputFields" : [ { 403 | "name" : "ne", 404 | "description" : null, 405 | "type" : { 406 | "kind" : "SCALAR", 407 | "name" : "ID", 408 | "ofType" : null 409 | }, 410 | "defaultValue" : null 411 | }, { 412 | "name" : "eq", 413 | "description" : null, 414 | "type" : { 415 | "kind" : "SCALAR", 416 | "name" : "ID", 417 | "ofType" : null 418 | }, 419 | "defaultValue" : null 420 | }, { 421 | "name" : "le", 422 | "description" : null, 423 | "type" : { 424 | "kind" : "SCALAR", 425 | "name" : "ID", 426 | "ofType" : null 427 | }, 428 | "defaultValue" : null 429 | }, { 430 | "name" : "lt", 431 | "description" : null, 432 | "type" : { 433 | "kind" : "SCALAR", 434 | "name" : "ID", 435 | "ofType" : null 436 | }, 437 | "defaultValue" : null 438 | }, { 439 | "name" : "ge", 440 | "description" : null, 441 | "type" : { 442 | "kind" : "SCALAR", 443 | "name" : "ID", 444 | "ofType" : null 445 | }, 446 | "defaultValue" : null 447 | }, { 448 | "name" : "gt", 449 | "description" : null, 450 | "type" : { 451 | "kind" : "SCALAR", 452 | "name" : "ID", 453 | "ofType" : null 454 | }, 455 | "defaultValue" : null 456 | }, { 457 | "name" : "contains", 458 | "description" : null, 459 | "type" : { 460 | "kind" : "SCALAR", 461 | "name" : "ID", 462 | "ofType" : null 463 | }, 464 | "defaultValue" : null 465 | }, { 466 | "name" : "notContains", 467 | "description" : null, 468 | "type" : { 469 | "kind" : "SCALAR", 470 | "name" : "ID", 471 | "ofType" : null 472 | }, 473 | "defaultValue" : null 474 | }, { 475 | "name" : "between", 476 | "description" : null, 477 | "type" : { 478 | "kind" : "LIST", 479 | "name" : null, 480 | "ofType" : { 481 | "kind" : "SCALAR", 482 | "name" : "ID", 483 | "ofType" : null 484 | } 485 | }, 486 | "defaultValue" : null 487 | }, { 488 | "name" : "beginsWith", 489 | "description" : null, 490 | "type" : { 491 | "kind" : "SCALAR", 492 | "name" : "ID", 493 | "ofType" : null 494 | }, 495 | "defaultValue" : null 496 | }, { 497 | "name" : "attributeExists", 498 | "description" : null, 499 | "type" : { 500 | "kind" : "SCALAR", 501 | "name" : "Boolean", 502 | "ofType" : null 503 | }, 504 | "defaultValue" : null 505 | }, { 506 | "name" : "attributeType", 507 | "description" : null, 508 | "type" : { 509 | "kind" : "ENUM", 510 | "name" : "ModelAttributeTypes", 511 | "ofType" : null 512 | }, 513 | "defaultValue" : null 514 | }, { 515 | "name" : "size", 516 | "description" : null, 517 | "type" : { 518 | "kind" : "INPUT_OBJECT", 519 | "name" : "ModelSizeInput", 520 | "ofType" : null 521 | }, 522 | "defaultValue" : null 523 | } ], 524 | "interfaces" : null, 525 | "enumValues" : null, 526 | "possibleTypes" : null 527 | }, { 528 | "kind" : "SCALAR", 529 | "name" : "Boolean", 530 | "description" : "Built-in Boolean", 531 | "fields" : null, 532 | "inputFields" : null, 533 | "interfaces" : null, 534 | "enumValues" : null, 535 | "possibleTypes" : null 536 | }, { 537 | "kind" : "ENUM", 538 | "name" : "ModelAttributeTypes", 539 | "description" : null, 540 | "fields" : null, 541 | "inputFields" : null, 542 | "interfaces" : null, 543 | "enumValues" : [ { 544 | "name" : "binary", 545 | "description" : null, 546 | "isDeprecated" : false, 547 | "deprecationReason" : null 548 | }, { 549 | "name" : "binarySet", 550 | "description" : null, 551 | "isDeprecated" : false, 552 | "deprecationReason" : null 553 | }, { 554 | "name" : "bool", 555 | "description" : null, 556 | "isDeprecated" : false, 557 | "deprecationReason" : null 558 | }, { 559 | "name" : "list", 560 | "description" : null, 561 | "isDeprecated" : false, 562 | "deprecationReason" : null 563 | }, { 564 | "name" : "map", 565 | "description" : null, 566 | "isDeprecated" : false, 567 | "deprecationReason" : null 568 | }, { 569 | "name" : "number", 570 | "description" : null, 571 | "isDeprecated" : false, 572 | "deprecationReason" : null 573 | }, { 574 | "name" : "numberSet", 575 | "description" : null, 576 | "isDeprecated" : false, 577 | "deprecationReason" : null 578 | }, { 579 | "name" : "string", 580 | "description" : null, 581 | "isDeprecated" : false, 582 | "deprecationReason" : null 583 | }, { 584 | "name" : "stringSet", 585 | "description" : null, 586 | "isDeprecated" : false, 587 | "deprecationReason" : null 588 | }, { 589 | "name" : "_null", 590 | "description" : null, 591 | "isDeprecated" : false, 592 | "deprecationReason" : null 593 | } ], 594 | "possibleTypes" : null 595 | }, { 596 | "kind" : "INPUT_OBJECT", 597 | "name" : "ModelSizeInput", 598 | "description" : null, 599 | "fields" : null, 600 | "inputFields" : [ { 601 | "name" : "ne", 602 | "description" : null, 603 | "type" : { 604 | "kind" : "SCALAR", 605 | "name" : "Int", 606 | "ofType" : null 607 | }, 608 | "defaultValue" : null 609 | }, { 610 | "name" : "eq", 611 | "description" : null, 612 | "type" : { 613 | "kind" : "SCALAR", 614 | "name" : "Int", 615 | "ofType" : null 616 | }, 617 | "defaultValue" : null 618 | }, { 619 | "name" : "le", 620 | "description" : null, 621 | "type" : { 622 | "kind" : "SCALAR", 623 | "name" : "Int", 624 | "ofType" : null 625 | }, 626 | "defaultValue" : null 627 | }, { 628 | "name" : "lt", 629 | "description" : null, 630 | "type" : { 631 | "kind" : "SCALAR", 632 | "name" : "Int", 633 | "ofType" : null 634 | }, 635 | "defaultValue" : null 636 | }, { 637 | "name" : "ge", 638 | "description" : null, 639 | "type" : { 640 | "kind" : "SCALAR", 641 | "name" : "Int", 642 | "ofType" : null 643 | }, 644 | "defaultValue" : null 645 | }, { 646 | "name" : "gt", 647 | "description" : null, 648 | "type" : { 649 | "kind" : "SCALAR", 650 | "name" : "Int", 651 | "ofType" : null 652 | }, 653 | "defaultValue" : null 654 | }, { 655 | "name" : "between", 656 | "description" : null, 657 | "type" : { 658 | "kind" : "LIST", 659 | "name" : null, 660 | "ofType" : { 661 | "kind" : "SCALAR", 662 | "name" : "Int", 663 | "ofType" : null 664 | } 665 | }, 666 | "defaultValue" : null 667 | } ], 668 | "interfaces" : null, 669 | "enumValues" : null, 670 | "possibleTypes" : null 671 | }, { 672 | "kind" : "SCALAR", 673 | "name" : "Int", 674 | "description" : "Built-in Int", 675 | "fields" : null, 676 | "inputFields" : null, 677 | "interfaces" : null, 678 | "enumValues" : null, 679 | "possibleTypes" : null 680 | }, { 681 | "kind" : "INPUT_OBJECT", 682 | "name" : "ModelStringInput", 683 | "description" : null, 684 | "fields" : null, 685 | "inputFields" : [ { 686 | "name" : "ne", 687 | "description" : null, 688 | "type" : { 689 | "kind" : "SCALAR", 690 | "name" : "String", 691 | "ofType" : null 692 | }, 693 | "defaultValue" : null 694 | }, { 695 | "name" : "eq", 696 | "description" : null, 697 | "type" : { 698 | "kind" : "SCALAR", 699 | "name" : "String", 700 | "ofType" : null 701 | }, 702 | "defaultValue" : null 703 | }, { 704 | "name" : "le", 705 | "description" : null, 706 | "type" : { 707 | "kind" : "SCALAR", 708 | "name" : "String", 709 | "ofType" : null 710 | }, 711 | "defaultValue" : null 712 | }, { 713 | "name" : "lt", 714 | "description" : null, 715 | "type" : { 716 | "kind" : "SCALAR", 717 | "name" : "String", 718 | "ofType" : null 719 | }, 720 | "defaultValue" : null 721 | }, { 722 | "name" : "ge", 723 | "description" : null, 724 | "type" : { 725 | "kind" : "SCALAR", 726 | "name" : "String", 727 | "ofType" : null 728 | }, 729 | "defaultValue" : null 730 | }, { 731 | "name" : "gt", 732 | "description" : null, 733 | "type" : { 734 | "kind" : "SCALAR", 735 | "name" : "String", 736 | "ofType" : null 737 | }, 738 | "defaultValue" : null 739 | }, { 740 | "name" : "contains", 741 | "description" : null, 742 | "type" : { 743 | "kind" : "SCALAR", 744 | "name" : "String", 745 | "ofType" : null 746 | }, 747 | "defaultValue" : null 748 | }, { 749 | "name" : "notContains", 750 | "description" : null, 751 | "type" : { 752 | "kind" : "SCALAR", 753 | "name" : "String", 754 | "ofType" : null 755 | }, 756 | "defaultValue" : null 757 | }, { 758 | "name" : "between", 759 | "description" : null, 760 | "type" : { 761 | "kind" : "LIST", 762 | "name" : null, 763 | "ofType" : { 764 | "kind" : "SCALAR", 765 | "name" : "String", 766 | "ofType" : null 767 | } 768 | }, 769 | "defaultValue" : null 770 | }, { 771 | "name" : "beginsWith", 772 | "description" : null, 773 | "type" : { 774 | "kind" : "SCALAR", 775 | "name" : "String", 776 | "ofType" : null 777 | }, 778 | "defaultValue" : null 779 | }, { 780 | "name" : "attributeExists", 781 | "description" : null, 782 | "type" : { 783 | "kind" : "SCALAR", 784 | "name" : "Boolean", 785 | "ofType" : null 786 | }, 787 | "defaultValue" : null 788 | }, { 789 | "name" : "attributeType", 790 | "description" : null, 791 | "type" : { 792 | "kind" : "ENUM", 793 | "name" : "ModelAttributeTypes", 794 | "ofType" : null 795 | }, 796 | "defaultValue" : null 797 | }, { 798 | "name" : "size", 799 | "description" : null, 800 | "type" : { 801 | "kind" : "INPUT_OBJECT", 802 | "name" : "ModelSizeInput", 803 | "ofType" : null 804 | }, 805 | "defaultValue" : null 806 | } ], 807 | "interfaces" : null, 808 | "enumValues" : null, 809 | "possibleTypes" : null 810 | }, { 811 | "kind" : "ENUM", 812 | "name" : "ModelSortDirection", 813 | "description" : null, 814 | "fields" : null, 815 | "inputFields" : null, 816 | "interfaces" : null, 817 | "enumValues" : [ { 818 | "name" : "ASC", 819 | "description" : null, 820 | "isDeprecated" : false, 821 | "deprecationReason" : null 822 | }, { 823 | "name" : "DESC", 824 | "description" : null, 825 | "isDeprecated" : false, 826 | "deprecationReason" : null 827 | } ], 828 | "possibleTypes" : null 829 | }, { 830 | "kind" : "OBJECT", 831 | "name" : "Mutation", 832 | "description" : null, 833 | "fields" : [ { 834 | "name" : "createPost", 835 | "description" : null, 836 | "args" : [ { 837 | "name" : "input", 838 | "description" : null, 839 | "type" : { 840 | "kind" : "NON_NULL", 841 | "name" : null, 842 | "ofType" : { 843 | "kind" : "INPUT_OBJECT", 844 | "name" : "CreatePostInput", 845 | "ofType" : null 846 | } 847 | }, 848 | "defaultValue" : null 849 | }, { 850 | "name" : "condition", 851 | "description" : null, 852 | "type" : { 853 | "kind" : "INPUT_OBJECT", 854 | "name" : "ModelPostConditionInput", 855 | "ofType" : null 856 | }, 857 | "defaultValue" : null 858 | } ], 859 | "type" : { 860 | "kind" : "OBJECT", 861 | "name" : "Post", 862 | "ofType" : null 863 | }, 864 | "isDeprecated" : false, 865 | "deprecationReason" : null 866 | }, { 867 | "name" : "updatePost", 868 | "description" : null, 869 | "args" : [ { 870 | "name" : "input", 871 | "description" : null, 872 | "type" : { 873 | "kind" : "NON_NULL", 874 | "name" : null, 875 | "ofType" : { 876 | "kind" : "INPUT_OBJECT", 877 | "name" : "UpdatePostInput", 878 | "ofType" : null 879 | } 880 | }, 881 | "defaultValue" : null 882 | }, { 883 | "name" : "condition", 884 | "description" : null, 885 | "type" : { 886 | "kind" : "INPUT_OBJECT", 887 | "name" : "ModelPostConditionInput", 888 | "ofType" : null 889 | }, 890 | "defaultValue" : null 891 | } ], 892 | "type" : { 893 | "kind" : "OBJECT", 894 | "name" : "Post", 895 | "ofType" : null 896 | }, 897 | "isDeprecated" : false, 898 | "deprecationReason" : null 899 | }, { 900 | "name" : "deletePost", 901 | "description" : null, 902 | "args" : [ { 903 | "name" : "input", 904 | "description" : null, 905 | "type" : { 906 | "kind" : "NON_NULL", 907 | "name" : null, 908 | "ofType" : { 909 | "kind" : "INPUT_OBJECT", 910 | "name" : "DeletePostInput", 911 | "ofType" : null 912 | } 913 | }, 914 | "defaultValue" : null 915 | }, { 916 | "name" : "condition", 917 | "description" : null, 918 | "type" : { 919 | "kind" : "INPUT_OBJECT", 920 | "name" : "ModelPostConditionInput", 921 | "ofType" : null 922 | }, 923 | "defaultValue" : null 924 | } ], 925 | "type" : { 926 | "kind" : "OBJECT", 927 | "name" : "Post", 928 | "ofType" : null 929 | }, 930 | "isDeprecated" : false, 931 | "deprecationReason" : null 932 | } ], 933 | "inputFields" : null, 934 | "interfaces" : [ ], 935 | "enumValues" : null, 936 | "possibleTypes" : null 937 | }, { 938 | "kind" : "INPUT_OBJECT", 939 | "name" : "CreatePostInput", 940 | "description" : null, 941 | "fields" : null, 942 | "inputFields" : [ { 943 | "name" : "id", 944 | "description" : null, 945 | "type" : { 946 | "kind" : "SCALAR", 947 | "name" : "ID", 948 | "ofType" : null 949 | }, 950 | "defaultValue" : null 951 | }, { 952 | "name" : "title", 953 | "description" : null, 954 | "type" : { 955 | "kind" : "NON_NULL", 956 | "name" : null, 957 | "ofType" : { 958 | "kind" : "SCALAR", 959 | "name" : "String", 960 | "ofType" : null 961 | } 962 | }, 963 | "defaultValue" : null 964 | }, { 965 | "name" : "content", 966 | "description" : null, 967 | "type" : { 968 | "kind" : "NON_NULL", 969 | "name" : null, 970 | "ofType" : { 971 | "kind" : "SCALAR", 972 | "name" : "String", 973 | "ofType" : null 974 | } 975 | }, 976 | "defaultValue" : null 977 | }, { 978 | "name" : "username", 979 | "description" : null, 980 | "type" : { 981 | "kind" : "SCALAR", 982 | "name" : "String", 983 | "ofType" : null 984 | }, 985 | "defaultValue" : null 986 | }, { 987 | "name" : "coverImage", 988 | "description" : null, 989 | "type" : { 990 | "kind" : "SCALAR", 991 | "name" : "String", 992 | "ofType" : null 993 | }, 994 | "defaultValue" : null 995 | } ], 996 | "interfaces" : null, 997 | "enumValues" : null, 998 | "possibleTypes" : null 999 | }, { 1000 | "kind" : "INPUT_OBJECT", 1001 | "name" : "ModelPostConditionInput", 1002 | "description" : null, 1003 | "fields" : null, 1004 | "inputFields" : [ { 1005 | "name" : "title", 1006 | "description" : null, 1007 | "type" : { 1008 | "kind" : "INPUT_OBJECT", 1009 | "name" : "ModelStringInput", 1010 | "ofType" : null 1011 | }, 1012 | "defaultValue" : null 1013 | }, { 1014 | "name" : "content", 1015 | "description" : null, 1016 | "type" : { 1017 | "kind" : "INPUT_OBJECT", 1018 | "name" : "ModelStringInput", 1019 | "ofType" : null 1020 | }, 1021 | "defaultValue" : null 1022 | }, { 1023 | "name" : "coverImage", 1024 | "description" : null, 1025 | "type" : { 1026 | "kind" : "INPUT_OBJECT", 1027 | "name" : "ModelStringInput", 1028 | "ofType" : null 1029 | }, 1030 | "defaultValue" : null 1031 | }, { 1032 | "name" : "and", 1033 | "description" : null, 1034 | "type" : { 1035 | "kind" : "LIST", 1036 | "name" : null, 1037 | "ofType" : { 1038 | "kind" : "INPUT_OBJECT", 1039 | "name" : "ModelPostConditionInput", 1040 | "ofType" : null 1041 | } 1042 | }, 1043 | "defaultValue" : null 1044 | }, { 1045 | "name" : "or", 1046 | "description" : null, 1047 | "type" : { 1048 | "kind" : "LIST", 1049 | "name" : null, 1050 | "ofType" : { 1051 | "kind" : "INPUT_OBJECT", 1052 | "name" : "ModelPostConditionInput", 1053 | "ofType" : null 1054 | } 1055 | }, 1056 | "defaultValue" : null 1057 | }, { 1058 | "name" : "not", 1059 | "description" : null, 1060 | "type" : { 1061 | "kind" : "INPUT_OBJECT", 1062 | "name" : "ModelPostConditionInput", 1063 | "ofType" : null 1064 | }, 1065 | "defaultValue" : null 1066 | } ], 1067 | "interfaces" : null, 1068 | "enumValues" : null, 1069 | "possibleTypes" : null 1070 | }, { 1071 | "kind" : "INPUT_OBJECT", 1072 | "name" : "UpdatePostInput", 1073 | "description" : null, 1074 | "fields" : null, 1075 | "inputFields" : [ { 1076 | "name" : "id", 1077 | "description" : null, 1078 | "type" : { 1079 | "kind" : "NON_NULL", 1080 | "name" : null, 1081 | "ofType" : { 1082 | "kind" : "SCALAR", 1083 | "name" : "ID", 1084 | "ofType" : null 1085 | } 1086 | }, 1087 | "defaultValue" : null 1088 | }, { 1089 | "name" : "title", 1090 | "description" : null, 1091 | "type" : { 1092 | "kind" : "SCALAR", 1093 | "name" : "String", 1094 | "ofType" : null 1095 | }, 1096 | "defaultValue" : null 1097 | }, { 1098 | "name" : "content", 1099 | "description" : null, 1100 | "type" : { 1101 | "kind" : "SCALAR", 1102 | "name" : "String", 1103 | "ofType" : null 1104 | }, 1105 | "defaultValue" : null 1106 | }, { 1107 | "name" : "username", 1108 | "description" : null, 1109 | "type" : { 1110 | "kind" : "SCALAR", 1111 | "name" : "String", 1112 | "ofType" : null 1113 | }, 1114 | "defaultValue" : null 1115 | }, { 1116 | "name" : "coverImage", 1117 | "description" : null, 1118 | "type" : { 1119 | "kind" : "SCALAR", 1120 | "name" : "String", 1121 | "ofType" : null 1122 | }, 1123 | "defaultValue" : null 1124 | } ], 1125 | "interfaces" : null, 1126 | "enumValues" : null, 1127 | "possibleTypes" : null 1128 | }, { 1129 | "kind" : "INPUT_OBJECT", 1130 | "name" : "DeletePostInput", 1131 | "description" : null, 1132 | "fields" : null, 1133 | "inputFields" : [ { 1134 | "name" : "id", 1135 | "description" : null, 1136 | "type" : { 1137 | "kind" : "SCALAR", 1138 | "name" : "ID", 1139 | "ofType" : null 1140 | }, 1141 | "defaultValue" : null 1142 | } ], 1143 | "interfaces" : null, 1144 | "enumValues" : null, 1145 | "possibleTypes" : null 1146 | }, { 1147 | "kind" : "OBJECT", 1148 | "name" : "Subscription", 1149 | "description" : null, 1150 | "fields" : [ { 1151 | "name" : "onCreatePost", 1152 | "description" : null, 1153 | "args" : [ { 1154 | "name" : "username", 1155 | "description" : null, 1156 | "type" : { 1157 | "kind" : "SCALAR", 1158 | "name" : "String", 1159 | "ofType" : null 1160 | }, 1161 | "defaultValue" : null 1162 | } ], 1163 | "type" : { 1164 | "kind" : "OBJECT", 1165 | "name" : "Post", 1166 | "ofType" : null 1167 | }, 1168 | "isDeprecated" : false, 1169 | "deprecationReason" : null 1170 | }, { 1171 | "name" : "onUpdatePost", 1172 | "description" : null, 1173 | "args" : [ { 1174 | "name" : "username", 1175 | "description" : null, 1176 | "type" : { 1177 | "kind" : "SCALAR", 1178 | "name" : "String", 1179 | "ofType" : null 1180 | }, 1181 | "defaultValue" : null 1182 | } ], 1183 | "type" : { 1184 | "kind" : "OBJECT", 1185 | "name" : "Post", 1186 | "ofType" : null 1187 | }, 1188 | "isDeprecated" : false, 1189 | "deprecationReason" : null 1190 | }, { 1191 | "name" : "onDeletePost", 1192 | "description" : null, 1193 | "args" : [ { 1194 | "name" : "username", 1195 | "description" : null, 1196 | "type" : { 1197 | "kind" : "SCALAR", 1198 | "name" : "String", 1199 | "ofType" : null 1200 | }, 1201 | "defaultValue" : null 1202 | } ], 1203 | "type" : { 1204 | "kind" : "OBJECT", 1205 | "name" : "Post", 1206 | "ofType" : null 1207 | }, 1208 | "isDeprecated" : false, 1209 | "deprecationReason" : null 1210 | } ], 1211 | "inputFields" : null, 1212 | "interfaces" : [ ], 1213 | "enumValues" : null, 1214 | "possibleTypes" : null 1215 | }, { 1216 | "kind" : "INPUT_OBJECT", 1217 | "name" : "ModelIntInput", 1218 | "description" : null, 1219 | "fields" : null, 1220 | "inputFields" : [ { 1221 | "name" : "ne", 1222 | "description" : null, 1223 | "type" : { 1224 | "kind" : "SCALAR", 1225 | "name" : "Int", 1226 | "ofType" : null 1227 | }, 1228 | "defaultValue" : null 1229 | }, { 1230 | "name" : "eq", 1231 | "description" : null, 1232 | "type" : { 1233 | "kind" : "SCALAR", 1234 | "name" : "Int", 1235 | "ofType" : null 1236 | }, 1237 | "defaultValue" : null 1238 | }, { 1239 | "name" : "le", 1240 | "description" : null, 1241 | "type" : { 1242 | "kind" : "SCALAR", 1243 | "name" : "Int", 1244 | "ofType" : null 1245 | }, 1246 | "defaultValue" : null 1247 | }, { 1248 | "name" : "lt", 1249 | "description" : null, 1250 | "type" : { 1251 | "kind" : "SCALAR", 1252 | "name" : "Int", 1253 | "ofType" : null 1254 | }, 1255 | "defaultValue" : null 1256 | }, { 1257 | "name" : "ge", 1258 | "description" : null, 1259 | "type" : { 1260 | "kind" : "SCALAR", 1261 | "name" : "Int", 1262 | "ofType" : null 1263 | }, 1264 | "defaultValue" : null 1265 | }, { 1266 | "name" : "gt", 1267 | "description" : null, 1268 | "type" : { 1269 | "kind" : "SCALAR", 1270 | "name" : "Int", 1271 | "ofType" : null 1272 | }, 1273 | "defaultValue" : null 1274 | }, { 1275 | "name" : "between", 1276 | "description" : null, 1277 | "type" : { 1278 | "kind" : "LIST", 1279 | "name" : null, 1280 | "ofType" : { 1281 | "kind" : "SCALAR", 1282 | "name" : "Int", 1283 | "ofType" : null 1284 | } 1285 | }, 1286 | "defaultValue" : null 1287 | }, { 1288 | "name" : "attributeExists", 1289 | "description" : null, 1290 | "type" : { 1291 | "kind" : "SCALAR", 1292 | "name" : "Boolean", 1293 | "ofType" : null 1294 | }, 1295 | "defaultValue" : null 1296 | }, { 1297 | "name" : "attributeType", 1298 | "description" : null, 1299 | "type" : { 1300 | "kind" : "ENUM", 1301 | "name" : "ModelAttributeTypes", 1302 | "ofType" : null 1303 | }, 1304 | "defaultValue" : null 1305 | } ], 1306 | "interfaces" : null, 1307 | "enumValues" : null, 1308 | "possibleTypes" : null 1309 | }, { 1310 | "kind" : "INPUT_OBJECT", 1311 | "name" : "ModelFloatInput", 1312 | "description" : null, 1313 | "fields" : null, 1314 | "inputFields" : [ { 1315 | "name" : "ne", 1316 | "description" : null, 1317 | "type" : { 1318 | "kind" : "SCALAR", 1319 | "name" : "Float", 1320 | "ofType" : null 1321 | }, 1322 | "defaultValue" : null 1323 | }, { 1324 | "name" : "eq", 1325 | "description" : null, 1326 | "type" : { 1327 | "kind" : "SCALAR", 1328 | "name" : "Float", 1329 | "ofType" : null 1330 | }, 1331 | "defaultValue" : null 1332 | }, { 1333 | "name" : "le", 1334 | "description" : null, 1335 | "type" : { 1336 | "kind" : "SCALAR", 1337 | "name" : "Float", 1338 | "ofType" : null 1339 | }, 1340 | "defaultValue" : null 1341 | }, { 1342 | "name" : "lt", 1343 | "description" : null, 1344 | "type" : { 1345 | "kind" : "SCALAR", 1346 | "name" : "Float", 1347 | "ofType" : null 1348 | }, 1349 | "defaultValue" : null 1350 | }, { 1351 | "name" : "ge", 1352 | "description" : null, 1353 | "type" : { 1354 | "kind" : "SCALAR", 1355 | "name" : "Float", 1356 | "ofType" : null 1357 | }, 1358 | "defaultValue" : null 1359 | }, { 1360 | "name" : "gt", 1361 | "description" : null, 1362 | "type" : { 1363 | "kind" : "SCALAR", 1364 | "name" : "Float", 1365 | "ofType" : null 1366 | }, 1367 | "defaultValue" : null 1368 | }, { 1369 | "name" : "between", 1370 | "description" : null, 1371 | "type" : { 1372 | "kind" : "LIST", 1373 | "name" : null, 1374 | "ofType" : { 1375 | "kind" : "SCALAR", 1376 | "name" : "Float", 1377 | "ofType" : null 1378 | } 1379 | }, 1380 | "defaultValue" : null 1381 | }, { 1382 | "name" : "attributeExists", 1383 | "description" : null, 1384 | "type" : { 1385 | "kind" : "SCALAR", 1386 | "name" : "Boolean", 1387 | "ofType" : null 1388 | }, 1389 | "defaultValue" : null 1390 | }, { 1391 | "name" : "attributeType", 1392 | "description" : null, 1393 | "type" : { 1394 | "kind" : "ENUM", 1395 | "name" : "ModelAttributeTypes", 1396 | "ofType" : null 1397 | }, 1398 | "defaultValue" : null 1399 | } ], 1400 | "interfaces" : null, 1401 | "enumValues" : null, 1402 | "possibleTypes" : null 1403 | }, { 1404 | "kind" : "SCALAR", 1405 | "name" : "Float", 1406 | "description" : "Built-in Float", 1407 | "fields" : null, 1408 | "inputFields" : null, 1409 | "interfaces" : null, 1410 | "enumValues" : null, 1411 | "possibleTypes" : null 1412 | }, { 1413 | "kind" : "INPUT_OBJECT", 1414 | "name" : "ModelBooleanInput", 1415 | "description" : null, 1416 | "fields" : null, 1417 | "inputFields" : [ { 1418 | "name" : "ne", 1419 | "description" : null, 1420 | "type" : { 1421 | "kind" : "SCALAR", 1422 | "name" : "Boolean", 1423 | "ofType" : null 1424 | }, 1425 | "defaultValue" : null 1426 | }, { 1427 | "name" : "eq", 1428 | "description" : null, 1429 | "type" : { 1430 | "kind" : "SCALAR", 1431 | "name" : "Boolean", 1432 | "ofType" : null 1433 | }, 1434 | "defaultValue" : null 1435 | }, { 1436 | "name" : "attributeExists", 1437 | "description" : null, 1438 | "type" : { 1439 | "kind" : "SCALAR", 1440 | "name" : "Boolean", 1441 | "ofType" : null 1442 | }, 1443 | "defaultValue" : null 1444 | }, { 1445 | "name" : "attributeType", 1446 | "description" : null, 1447 | "type" : { 1448 | "kind" : "ENUM", 1449 | "name" : "ModelAttributeTypes", 1450 | "ofType" : null 1451 | }, 1452 | "defaultValue" : null 1453 | } ], 1454 | "interfaces" : null, 1455 | "enumValues" : null, 1456 | "possibleTypes" : null 1457 | }, { 1458 | "kind" : "OBJECT", 1459 | "name" : "__Schema", 1460 | "description" : "A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations.", 1461 | "fields" : [ { 1462 | "name" : "types", 1463 | "description" : "A list of all types supported by this server.", 1464 | "args" : [ ], 1465 | "type" : { 1466 | "kind" : "NON_NULL", 1467 | "name" : null, 1468 | "ofType" : { 1469 | "kind" : "LIST", 1470 | "name" : null, 1471 | "ofType" : { 1472 | "kind" : "NON_NULL", 1473 | "name" : null, 1474 | "ofType" : { 1475 | "kind" : "OBJECT", 1476 | "name" : "__Type", 1477 | "ofType" : null 1478 | } 1479 | } 1480 | } 1481 | }, 1482 | "isDeprecated" : false, 1483 | "deprecationReason" : null 1484 | }, { 1485 | "name" : "queryType", 1486 | "description" : "The type that query operations will be rooted at.", 1487 | "args" : [ ], 1488 | "type" : { 1489 | "kind" : "NON_NULL", 1490 | "name" : null, 1491 | "ofType" : { 1492 | "kind" : "OBJECT", 1493 | "name" : "__Type", 1494 | "ofType" : null 1495 | } 1496 | }, 1497 | "isDeprecated" : false, 1498 | "deprecationReason" : null 1499 | }, { 1500 | "name" : "mutationType", 1501 | "description" : "If this server supports mutation, the type that mutation operations will be rooted at.", 1502 | "args" : [ ], 1503 | "type" : { 1504 | "kind" : "OBJECT", 1505 | "name" : "__Type", 1506 | "ofType" : null 1507 | }, 1508 | "isDeprecated" : false, 1509 | "deprecationReason" : null 1510 | }, { 1511 | "name" : "directives", 1512 | "description" : "'A list of all directives supported by this server.", 1513 | "args" : [ ], 1514 | "type" : { 1515 | "kind" : "NON_NULL", 1516 | "name" : null, 1517 | "ofType" : { 1518 | "kind" : "LIST", 1519 | "name" : null, 1520 | "ofType" : { 1521 | "kind" : "NON_NULL", 1522 | "name" : null, 1523 | "ofType" : { 1524 | "kind" : "OBJECT", 1525 | "name" : "__Directive", 1526 | "ofType" : null 1527 | } 1528 | } 1529 | } 1530 | }, 1531 | "isDeprecated" : false, 1532 | "deprecationReason" : null 1533 | }, { 1534 | "name" : "subscriptionType", 1535 | "description" : "'If this server support subscription, the type that subscription operations will be rooted at.", 1536 | "args" : [ ], 1537 | "type" : { 1538 | "kind" : "OBJECT", 1539 | "name" : "__Type", 1540 | "ofType" : null 1541 | }, 1542 | "isDeprecated" : false, 1543 | "deprecationReason" : null 1544 | } ], 1545 | "inputFields" : null, 1546 | "interfaces" : [ ], 1547 | "enumValues" : null, 1548 | "possibleTypes" : null 1549 | }, { 1550 | "kind" : "OBJECT", 1551 | "name" : "__Type", 1552 | "description" : null, 1553 | "fields" : [ { 1554 | "name" : "kind", 1555 | "description" : null, 1556 | "args" : [ ], 1557 | "type" : { 1558 | "kind" : "NON_NULL", 1559 | "name" : null, 1560 | "ofType" : { 1561 | "kind" : "ENUM", 1562 | "name" : "__TypeKind", 1563 | "ofType" : null 1564 | } 1565 | }, 1566 | "isDeprecated" : false, 1567 | "deprecationReason" : null 1568 | }, { 1569 | "name" : "name", 1570 | "description" : null, 1571 | "args" : [ ], 1572 | "type" : { 1573 | "kind" : "SCALAR", 1574 | "name" : "String", 1575 | "ofType" : null 1576 | }, 1577 | "isDeprecated" : false, 1578 | "deprecationReason" : null 1579 | }, { 1580 | "name" : "description", 1581 | "description" : null, 1582 | "args" : [ ], 1583 | "type" : { 1584 | "kind" : "SCALAR", 1585 | "name" : "String", 1586 | "ofType" : null 1587 | }, 1588 | "isDeprecated" : false, 1589 | "deprecationReason" : null 1590 | }, { 1591 | "name" : "fields", 1592 | "description" : null, 1593 | "args" : [ { 1594 | "name" : "includeDeprecated", 1595 | "description" : null, 1596 | "type" : { 1597 | "kind" : "SCALAR", 1598 | "name" : "Boolean", 1599 | "ofType" : null 1600 | }, 1601 | "defaultValue" : "false" 1602 | } ], 1603 | "type" : { 1604 | "kind" : "LIST", 1605 | "name" : null, 1606 | "ofType" : { 1607 | "kind" : "NON_NULL", 1608 | "name" : null, 1609 | "ofType" : { 1610 | "kind" : "OBJECT", 1611 | "name" : "__Field", 1612 | "ofType" : null 1613 | } 1614 | } 1615 | }, 1616 | "isDeprecated" : false, 1617 | "deprecationReason" : null 1618 | }, { 1619 | "name" : "interfaces", 1620 | "description" : null, 1621 | "args" : [ ], 1622 | "type" : { 1623 | "kind" : "LIST", 1624 | "name" : null, 1625 | "ofType" : { 1626 | "kind" : "NON_NULL", 1627 | "name" : null, 1628 | "ofType" : { 1629 | "kind" : "OBJECT", 1630 | "name" : "__Type", 1631 | "ofType" : null 1632 | } 1633 | } 1634 | }, 1635 | "isDeprecated" : false, 1636 | "deprecationReason" : null 1637 | }, { 1638 | "name" : "possibleTypes", 1639 | "description" : null, 1640 | "args" : [ ], 1641 | "type" : { 1642 | "kind" : "LIST", 1643 | "name" : null, 1644 | "ofType" : { 1645 | "kind" : "NON_NULL", 1646 | "name" : null, 1647 | "ofType" : { 1648 | "kind" : "OBJECT", 1649 | "name" : "__Type", 1650 | "ofType" : null 1651 | } 1652 | } 1653 | }, 1654 | "isDeprecated" : false, 1655 | "deprecationReason" : null 1656 | }, { 1657 | "name" : "enumValues", 1658 | "description" : null, 1659 | "args" : [ { 1660 | "name" : "includeDeprecated", 1661 | "description" : null, 1662 | "type" : { 1663 | "kind" : "SCALAR", 1664 | "name" : "Boolean", 1665 | "ofType" : null 1666 | }, 1667 | "defaultValue" : "false" 1668 | } ], 1669 | "type" : { 1670 | "kind" : "LIST", 1671 | "name" : null, 1672 | "ofType" : { 1673 | "kind" : "NON_NULL", 1674 | "name" : null, 1675 | "ofType" : { 1676 | "kind" : "OBJECT", 1677 | "name" : "__EnumValue", 1678 | "ofType" : null 1679 | } 1680 | } 1681 | }, 1682 | "isDeprecated" : false, 1683 | "deprecationReason" : null 1684 | }, { 1685 | "name" : "inputFields", 1686 | "description" : null, 1687 | "args" : [ ], 1688 | "type" : { 1689 | "kind" : "LIST", 1690 | "name" : null, 1691 | "ofType" : { 1692 | "kind" : "NON_NULL", 1693 | "name" : null, 1694 | "ofType" : { 1695 | "kind" : "OBJECT", 1696 | "name" : "__InputValue", 1697 | "ofType" : null 1698 | } 1699 | } 1700 | }, 1701 | "isDeprecated" : false, 1702 | "deprecationReason" : null 1703 | }, { 1704 | "name" : "ofType", 1705 | "description" : null, 1706 | "args" : [ ], 1707 | "type" : { 1708 | "kind" : "OBJECT", 1709 | "name" : "__Type", 1710 | "ofType" : null 1711 | }, 1712 | "isDeprecated" : false, 1713 | "deprecationReason" : null 1714 | } ], 1715 | "inputFields" : null, 1716 | "interfaces" : [ ], 1717 | "enumValues" : null, 1718 | "possibleTypes" : null 1719 | }, { 1720 | "kind" : "ENUM", 1721 | "name" : "__TypeKind", 1722 | "description" : "An enum describing what kind of type a given __Type is", 1723 | "fields" : null, 1724 | "inputFields" : null, 1725 | "interfaces" : null, 1726 | "enumValues" : [ { 1727 | "name" : "SCALAR", 1728 | "description" : "Indicates this type is a scalar.", 1729 | "isDeprecated" : false, 1730 | "deprecationReason" : null 1731 | }, { 1732 | "name" : "OBJECT", 1733 | "description" : "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 1734 | "isDeprecated" : false, 1735 | "deprecationReason" : null 1736 | }, { 1737 | "name" : "INTERFACE", 1738 | "description" : "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 1739 | "isDeprecated" : false, 1740 | "deprecationReason" : null 1741 | }, { 1742 | "name" : "UNION", 1743 | "description" : "Indicates this type is a union. `possibleTypes` is a valid field.", 1744 | "isDeprecated" : false, 1745 | "deprecationReason" : null 1746 | }, { 1747 | "name" : "ENUM", 1748 | "description" : "Indicates this type is an enum. `enumValues` is a valid field.", 1749 | "isDeprecated" : false, 1750 | "deprecationReason" : null 1751 | }, { 1752 | "name" : "INPUT_OBJECT", 1753 | "description" : "Indicates this type is an input object. `inputFields` is a valid field.", 1754 | "isDeprecated" : false, 1755 | "deprecationReason" : null 1756 | }, { 1757 | "name" : "LIST", 1758 | "description" : "Indicates this type is a list. `ofType` is a valid field.", 1759 | "isDeprecated" : false, 1760 | "deprecationReason" : null 1761 | }, { 1762 | "name" : "NON_NULL", 1763 | "description" : "Indicates this type is a non-null. `ofType` is a valid field.", 1764 | "isDeprecated" : false, 1765 | "deprecationReason" : null 1766 | } ], 1767 | "possibleTypes" : null 1768 | }, { 1769 | "kind" : "OBJECT", 1770 | "name" : "__Field", 1771 | "description" : null, 1772 | "fields" : [ { 1773 | "name" : "name", 1774 | "description" : null, 1775 | "args" : [ ], 1776 | "type" : { 1777 | "kind" : "NON_NULL", 1778 | "name" : null, 1779 | "ofType" : { 1780 | "kind" : "SCALAR", 1781 | "name" : "String", 1782 | "ofType" : null 1783 | } 1784 | }, 1785 | "isDeprecated" : false, 1786 | "deprecationReason" : null 1787 | }, { 1788 | "name" : "description", 1789 | "description" : null, 1790 | "args" : [ ], 1791 | "type" : { 1792 | "kind" : "SCALAR", 1793 | "name" : "String", 1794 | "ofType" : null 1795 | }, 1796 | "isDeprecated" : false, 1797 | "deprecationReason" : null 1798 | }, { 1799 | "name" : "args", 1800 | "description" : null, 1801 | "args" : [ ], 1802 | "type" : { 1803 | "kind" : "NON_NULL", 1804 | "name" : null, 1805 | "ofType" : { 1806 | "kind" : "LIST", 1807 | "name" : null, 1808 | "ofType" : { 1809 | "kind" : "NON_NULL", 1810 | "name" : null, 1811 | "ofType" : { 1812 | "kind" : "OBJECT", 1813 | "name" : "__InputValue", 1814 | "ofType" : null 1815 | } 1816 | } 1817 | } 1818 | }, 1819 | "isDeprecated" : false, 1820 | "deprecationReason" : null 1821 | }, { 1822 | "name" : "type", 1823 | "description" : null, 1824 | "args" : [ ], 1825 | "type" : { 1826 | "kind" : "NON_NULL", 1827 | "name" : null, 1828 | "ofType" : { 1829 | "kind" : "OBJECT", 1830 | "name" : "__Type", 1831 | "ofType" : null 1832 | } 1833 | }, 1834 | "isDeprecated" : false, 1835 | "deprecationReason" : null 1836 | }, { 1837 | "name" : "isDeprecated", 1838 | "description" : null, 1839 | "args" : [ ], 1840 | "type" : { 1841 | "kind" : "NON_NULL", 1842 | "name" : null, 1843 | "ofType" : { 1844 | "kind" : "SCALAR", 1845 | "name" : "Boolean", 1846 | "ofType" : null 1847 | } 1848 | }, 1849 | "isDeprecated" : false, 1850 | "deprecationReason" : null 1851 | }, { 1852 | "name" : "deprecationReason", 1853 | "description" : null, 1854 | "args" : [ ], 1855 | "type" : { 1856 | "kind" : "SCALAR", 1857 | "name" : "String", 1858 | "ofType" : null 1859 | }, 1860 | "isDeprecated" : false, 1861 | "deprecationReason" : null 1862 | } ], 1863 | "inputFields" : null, 1864 | "interfaces" : [ ], 1865 | "enumValues" : null, 1866 | "possibleTypes" : null 1867 | }, { 1868 | "kind" : "OBJECT", 1869 | "name" : "__InputValue", 1870 | "description" : null, 1871 | "fields" : [ { 1872 | "name" : "name", 1873 | "description" : null, 1874 | "args" : [ ], 1875 | "type" : { 1876 | "kind" : "NON_NULL", 1877 | "name" : null, 1878 | "ofType" : { 1879 | "kind" : "SCALAR", 1880 | "name" : "String", 1881 | "ofType" : null 1882 | } 1883 | }, 1884 | "isDeprecated" : false, 1885 | "deprecationReason" : null 1886 | }, { 1887 | "name" : "description", 1888 | "description" : null, 1889 | "args" : [ ], 1890 | "type" : { 1891 | "kind" : "SCALAR", 1892 | "name" : "String", 1893 | "ofType" : null 1894 | }, 1895 | "isDeprecated" : false, 1896 | "deprecationReason" : null 1897 | }, { 1898 | "name" : "type", 1899 | "description" : null, 1900 | "args" : [ ], 1901 | "type" : { 1902 | "kind" : "NON_NULL", 1903 | "name" : null, 1904 | "ofType" : { 1905 | "kind" : "OBJECT", 1906 | "name" : "__Type", 1907 | "ofType" : null 1908 | } 1909 | }, 1910 | "isDeprecated" : false, 1911 | "deprecationReason" : null 1912 | }, { 1913 | "name" : "defaultValue", 1914 | "description" : null, 1915 | "args" : [ ], 1916 | "type" : { 1917 | "kind" : "SCALAR", 1918 | "name" : "String", 1919 | "ofType" : null 1920 | }, 1921 | "isDeprecated" : false, 1922 | "deprecationReason" : null 1923 | } ], 1924 | "inputFields" : null, 1925 | "interfaces" : [ ], 1926 | "enumValues" : null, 1927 | "possibleTypes" : null 1928 | }, { 1929 | "kind" : "OBJECT", 1930 | "name" : "__EnumValue", 1931 | "description" : null, 1932 | "fields" : [ { 1933 | "name" : "name", 1934 | "description" : null, 1935 | "args" : [ ], 1936 | "type" : { 1937 | "kind" : "NON_NULL", 1938 | "name" : null, 1939 | "ofType" : { 1940 | "kind" : "SCALAR", 1941 | "name" : "String", 1942 | "ofType" : null 1943 | } 1944 | }, 1945 | "isDeprecated" : false, 1946 | "deprecationReason" : null 1947 | }, { 1948 | "name" : "description", 1949 | "description" : null, 1950 | "args" : [ ], 1951 | "type" : { 1952 | "kind" : "SCALAR", 1953 | "name" : "String", 1954 | "ofType" : null 1955 | }, 1956 | "isDeprecated" : false, 1957 | "deprecationReason" : null 1958 | }, { 1959 | "name" : "isDeprecated", 1960 | "description" : null, 1961 | "args" : [ ], 1962 | "type" : { 1963 | "kind" : "NON_NULL", 1964 | "name" : null, 1965 | "ofType" : { 1966 | "kind" : "SCALAR", 1967 | "name" : "Boolean", 1968 | "ofType" : null 1969 | } 1970 | }, 1971 | "isDeprecated" : false, 1972 | "deprecationReason" : null 1973 | }, { 1974 | "name" : "deprecationReason", 1975 | "description" : null, 1976 | "args" : [ ], 1977 | "type" : { 1978 | "kind" : "SCALAR", 1979 | "name" : "String", 1980 | "ofType" : null 1981 | }, 1982 | "isDeprecated" : false, 1983 | "deprecationReason" : null 1984 | } ], 1985 | "inputFields" : null, 1986 | "interfaces" : [ ], 1987 | "enumValues" : null, 1988 | "possibleTypes" : null 1989 | }, { 1990 | "kind" : "OBJECT", 1991 | "name" : "__Directive", 1992 | "description" : null, 1993 | "fields" : [ { 1994 | "name" : "name", 1995 | "description" : null, 1996 | "args" : [ ], 1997 | "type" : { 1998 | "kind" : "SCALAR", 1999 | "name" : "String", 2000 | "ofType" : null 2001 | }, 2002 | "isDeprecated" : false, 2003 | "deprecationReason" : null 2004 | }, { 2005 | "name" : "description", 2006 | "description" : null, 2007 | "args" : [ ], 2008 | "type" : { 2009 | "kind" : "SCALAR", 2010 | "name" : "String", 2011 | "ofType" : null 2012 | }, 2013 | "isDeprecated" : false, 2014 | "deprecationReason" : null 2015 | }, { 2016 | "name" : "locations", 2017 | "description" : null, 2018 | "args" : [ ], 2019 | "type" : { 2020 | "kind" : "LIST", 2021 | "name" : null, 2022 | "ofType" : { 2023 | "kind" : "NON_NULL", 2024 | "name" : null, 2025 | "ofType" : { 2026 | "kind" : "ENUM", 2027 | "name" : "__DirectiveLocation", 2028 | "ofType" : null 2029 | } 2030 | } 2031 | }, 2032 | "isDeprecated" : false, 2033 | "deprecationReason" : null 2034 | }, { 2035 | "name" : "args", 2036 | "description" : null, 2037 | "args" : [ ], 2038 | "type" : { 2039 | "kind" : "NON_NULL", 2040 | "name" : null, 2041 | "ofType" : { 2042 | "kind" : "LIST", 2043 | "name" : null, 2044 | "ofType" : { 2045 | "kind" : "NON_NULL", 2046 | "name" : null, 2047 | "ofType" : { 2048 | "kind" : "OBJECT", 2049 | "name" : "__InputValue", 2050 | "ofType" : null 2051 | } 2052 | } 2053 | } 2054 | }, 2055 | "isDeprecated" : false, 2056 | "deprecationReason" : null 2057 | }, { 2058 | "name" : "onOperation", 2059 | "description" : null, 2060 | "args" : [ ], 2061 | "type" : { 2062 | "kind" : "SCALAR", 2063 | "name" : "Boolean", 2064 | "ofType" : null 2065 | }, 2066 | "isDeprecated" : true, 2067 | "deprecationReason" : "Use `locations`." 2068 | }, { 2069 | "name" : "onFragment", 2070 | "description" : null, 2071 | "args" : [ ], 2072 | "type" : { 2073 | "kind" : "SCALAR", 2074 | "name" : "Boolean", 2075 | "ofType" : null 2076 | }, 2077 | "isDeprecated" : true, 2078 | "deprecationReason" : "Use `locations`." 2079 | }, { 2080 | "name" : "onField", 2081 | "description" : null, 2082 | "args" : [ ], 2083 | "type" : { 2084 | "kind" : "SCALAR", 2085 | "name" : "Boolean", 2086 | "ofType" : null 2087 | }, 2088 | "isDeprecated" : true, 2089 | "deprecationReason" : "Use `locations`." 2090 | } ], 2091 | "inputFields" : null, 2092 | "interfaces" : [ ], 2093 | "enumValues" : null, 2094 | "possibleTypes" : null 2095 | }, { 2096 | "kind" : "ENUM", 2097 | "name" : "__DirectiveLocation", 2098 | "description" : "An enum describing valid locations where a directive can be placed", 2099 | "fields" : null, 2100 | "inputFields" : null, 2101 | "interfaces" : null, 2102 | "enumValues" : [ { 2103 | "name" : "QUERY", 2104 | "description" : "Indicates the directive is valid on queries.", 2105 | "isDeprecated" : false, 2106 | "deprecationReason" : null 2107 | }, { 2108 | "name" : "MUTATION", 2109 | "description" : "Indicates the directive is valid on mutations.", 2110 | "isDeprecated" : false, 2111 | "deprecationReason" : null 2112 | }, { 2113 | "name" : "FIELD", 2114 | "description" : "Indicates the directive is valid on fields.", 2115 | "isDeprecated" : false, 2116 | "deprecationReason" : null 2117 | }, { 2118 | "name" : "FRAGMENT_DEFINITION", 2119 | "description" : "Indicates the directive is valid on fragment definitions.", 2120 | "isDeprecated" : false, 2121 | "deprecationReason" : null 2122 | }, { 2123 | "name" : "FRAGMENT_SPREAD", 2124 | "description" : "Indicates the directive is valid on fragment spreads.", 2125 | "isDeprecated" : false, 2126 | "deprecationReason" : null 2127 | }, { 2128 | "name" : "INLINE_FRAGMENT", 2129 | "description" : "Indicates the directive is valid on inline fragments.", 2130 | "isDeprecated" : false, 2131 | "deprecationReason" : null 2132 | }, { 2133 | "name" : "SCHEMA", 2134 | "description" : "Indicates the directive is valid on a schema SDL definition.", 2135 | "isDeprecated" : false, 2136 | "deprecationReason" : null 2137 | }, { 2138 | "name" : "SCALAR", 2139 | "description" : "Indicates the directive is valid on a scalar SDL definition.", 2140 | "isDeprecated" : false, 2141 | "deprecationReason" : null 2142 | }, { 2143 | "name" : "OBJECT", 2144 | "description" : "Indicates the directive is valid on an object SDL definition.", 2145 | "isDeprecated" : false, 2146 | "deprecationReason" : null 2147 | }, { 2148 | "name" : "FIELD_DEFINITION", 2149 | "description" : "Indicates the directive is valid on a field SDL definition.", 2150 | "isDeprecated" : false, 2151 | "deprecationReason" : null 2152 | }, { 2153 | "name" : "ARGUMENT_DEFINITION", 2154 | "description" : "Indicates the directive is valid on a field argument SDL definition.", 2155 | "isDeprecated" : false, 2156 | "deprecationReason" : null 2157 | }, { 2158 | "name" : "INTERFACE", 2159 | "description" : "Indicates the directive is valid on an interface SDL definition.", 2160 | "isDeprecated" : false, 2161 | "deprecationReason" : null 2162 | }, { 2163 | "name" : "UNION", 2164 | "description" : "Indicates the directive is valid on an union SDL definition.", 2165 | "isDeprecated" : false, 2166 | "deprecationReason" : null 2167 | }, { 2168 | "name" : "ENUM", 2169 | "description" : "Indicates the directive is valid on an enum SDL definition.", 2170 | "isDeprecated" : false, 2171 | "deprecationReason" : null 2172 | }, { 2173 | "name" : "ENUM_VALUE", 2174 | "description" : "Indicates the directive is valid on an enum value SDL definition.", 2175 | "isDeprecated" : false, 2176 | "deprecationReason" : null 2177 | }, { 2178 | "name" : "INPUT_OBJECT", 2179 | "description" : "Indicates the directive is valid on an input object SDL definition.", 2180 | "isDeprecated" : false, 2181 | "deprecationReason" : null 2182 | }, { 2183 | "name" : "INPUT_FIELD_DEFINITION", 2184 | "description" : "Indicates the directive is valid on an input object field SDL definition.", 2185 | "isDeprecated" : false, 2186 | "deprecationReason" : null 2187 | } ], 2188 | "possibleTypes" : null 2189 | } ], 2190 | "directives" : [ { 2191 | "name" : "include", 2192 | "description" : "Directs the executor to include this field or fragment only when the `if` argument is true", 2193 | "locations" : [ "FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT" ], 2194 | "args" : [ { 2195 | "name" : "if", 2196 | "description" : "Included when true.", 2197 | "type" : { 2198 | "kind" : "NON_NULL", 2199 | "name" : null, 2200 | "ofType" : { 2201 | "kind" : "SCALAR", 2202 | "name" : "Boolean", 2203 | "ofType" : null 2204 | } 2205 | }, 2206 | "defaultValue" : null 2207 | } ], 2208 | "onOperation" : false, 2209 | "onFragment" : true, 2210 | "onField" : true 2211 | }, { 2212 | "name" : "skip", 2213 | "description" : "Directs the executor to skip this field or fragment when the `if`'argument is true.", 2214 | "locations" : [ "FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT" ], 2215 | "args" : [ { 2216 | "name" : "if", 2217 | "description" : "Skipped when true.", 2218 | "type" : { 2219 | "kind" : "NON_NULL", 2220 | "name" : null, 2221 | "ofType" : { 2222 | "kind" : "SCALAR", 2223 | "name" : "Boolean", 2224 | "ofType" : null 2225 | } 2226 | }, 2227 | "defaultValue" : null 2228 | } ], 2229 | "onOperation" : false, 2230 | "onFragment" : true, 2231 | "onField" : true 2232 | }, { 2233 | "name" : "defer", 2234 | "description" : "This directive allows results to be deferred during execution", 2235 | "locations" : [ "FIELD" ], 2236 | "args" : [ ], 2237 | "onOperation" : false, 2238 | "onFragment" : false, 2239 | "onField" : true 2240 | }, { 2241 | "name" : "aws_cognito_user_pools", 2242 | "description" : "Tells the service this field/object has access authorized by a Cognito User Pools token.", 2243 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2244 | "args" : [ { 2245 | "name" : "cognito_groups", 2246 | "description" : "List of cognito user pool groups which have access on this field", 2247 | "type" : { 2248 | "kind" : "LIST", 2249 | "name" : null, 2250 | "ofType" : { 2251 | "kind" : "SCALAR", 2252 | "name" : "String", 2253 | "ofType" : null 2254 | } 2255 | }, 2256 | "defaultValue" : null 2257 | } ], 2258 | "onOperation" : false, 2259 | "onFragment" : false, 2260 | "onField" : false 2261 | }, { 2262 | "name" : "aws_auth", 2263 | "description" : "Directs the schema to enforce authorization on a field", 2264 | "locations" : [ "FIELD_DEFINITION" ], 2265 | "args" : [ { 2266 | "name" : "cognito_groups", 2267 | "description" : "List of cognito user pool groups which have access on this field", 2268 | "type" : { 2269 | "kind" : "LIST", 2270 | "name" : null, 2271 | "ofType" : { 2272 | "kind" : "SCALAR", 2273 | "name" : "String", 2274 | "ofType" : null 2275 | } 2276 | }, 2277 | "defaultValue" : null 2278 | } ], 2279 | "onOperation" : false, 2280 | "onFragment" : false, 2281 | "onField" : false 2282 | }, { 2283 | "name" : "aws_subscribe", 2284 | "description" : "Tells the service which mutation triggers this subscription.", 2285 | "locations" : [ "FIELD_DEFINITION" ], 2286 | "args" : [ { 2287 | "name" : "mutations", 2288 | "description" : "List of mutations which will trigger this subscription when they are called.", 2289 | "type" : { 2290 | "kind" : "LIST", 2291 | "name" : null, 2292 | "ofType" : { 2293 | "kind" : "SCALAR", 2294 | "name" : "String", 2295 | "ofType" : null 2296 | } 2297 | }, 2298 | "defaultValue" : null 2299 | } ], 2300 | "onOperation" : false, 2301 | "onFragment" : false, 2302 | "onField" : false 2303 | }, { 2304 | "name" : "deprecated", 2305 | "description" : null, 2306 | "locations" : [ "FIELD_DEFINITION", "ENUM_VALUE" ], 2307 | "args" : [ { 2308 | "name" : "reason", 2309 | "description" : null, 2310 | "type" : { 2311 | "kind" : "SCALAR", 2312 | "name" : "String", 2313 | "ofType" : null 2314 | }, 2315 | "defaultValue" : "\"No longer supported\"" 2316 | } ], 2317 | "onOperation" : false, 2318 | "onFragment" : false, 2319 | "onField" : false 2320 | }, { 2321 | "name" : "aws_publish", 2322 | "description" : "Tells the service which subscriptions will be published to when this mutation is called. This directive is deprecated use @aws_susbscribe directive instead.", 2323 | "locations" : [ "FIELD_DEFINITION" ], 2324 | "args" : [ { 2325 | "name" : "subscriptions", 2326 | "description" : "List of subscriptions which will be published to when this mutation is called.", 2327 | "type" : { 2328 | "kind" : "LIST", 2329 | "name" : null, 2330 | "ofType" : { 2331 | "kind" : "SCALAR", 2332 | "name" : "String", 2333 | "ofType" : null 2334 | } 2335 | }, 2336 | "defaultValue" : null 2337 | } ], 2338 | "onOperation" : false, 2339 | "onFragment" : false, 2340 | "onField" : false 2341 | }, { 2342 | "name" : "aws_api_key", 2343 | "description" : "Tells the service this field/object has access authorized by an API key.", 2344 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2345 | "args" : [ ], 2346 | "onOperation" : false, 2347 | "onFragment" : false, 2348 | "onField" : false 2349 | }, { 2350 | "name" : "aws_oidc", 2351 | "description" : "Tells the service this field/object has access authorized by an OIDC token.", 2352 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2353 | "args" : [ ], 2354 | "onOperation" : false, 2355 | "onFragment" : false, 2356 | "onField" : false 2357 | }, { 2358 | "name" : "aws_iam", 2359 | "description" : "Tells the service this field/object has access authorized by sigv4 signing.", 2360 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2361 | "args" : [ ], 2362 | "onOperation" : false, 2363 | "onFragment" : false, 2364 | "onField" : false 2365 | } ] 2366 | } 2367 | } 2368 | } -------------------------------------------------------------------------------- /amplify-next/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const onCreatePost = /* GraphQL */ ` 5 | subscription OnCreatePost($username: String) { 6 | onCreatePost(username: $username) { 7 | id 8 | title 9 | content 10 | username 11 | coverImage 12 | createdAt 13 | updatedAt 14 | } 15 | } 16 | `; 17 | export const onUpdatePost = /* GraphQL */ ` 18 | subscription OnUpdatePost($username: String) { 19 | onUpdatePost(username: $username) { 20 | id 21 | title 22 | content 23 | username 24 | coverImage 25 | createdAt 26 | updatedAt 27 | } 28 | } 29 | `; 30 | export const onDeletePost = /* GraphQL */ ` 31 | subscription OnDeletePost($username: String) { 32 | onDeletePost(username: $username) { 33 | id 34 | title 35 | content 36 | username 37 | coverImage 38 | createdAt 39 | updatedAt 40 | } 41 | } 42 | `; 43 | -------------------------------------------------------------------------------- /amplify-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amplify-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@aws-amplify/ui-react": "^0.2.34", 12 | "@tailwindcss/typography": "^0.4.0", 13 | "autoprefixer": "^10.2.3", 14 | "aws-amplify": "^3.3.14", 15 | "next": "10.0.5", 16 | "postcss": "^8.2.4", 17 | "react": "17.0.1", 18 | "react-dom": "17.0.1", 19 | "react-markdown": "^5.0.3", 20 | "react-simplemde-editor": "^4.1.3", 21 | "tailwindcss": "^2.0.2", 22 | "uuid": "^8.3.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /amplify-next/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import '../configureAmplify' 3 | import Link from 'next/link' 4 | import { useState, useEffect } from 'react' 5 | import { Auth, Hub } from 'aws-amplify' 6 | 7 | function MyApp({ Component, pageProps }) { 8 | const [signedInUser, setSignedInUser] = useState(false) 9 | useEffect(() => { 10 | authListener() 11 | }) 12 | async function authListener() { 13 | Hub.listen('auth', (data) => { 14 | switch (data.payload.event) { 15 | case 'signIn': 16 | return setSignedInUser(true) 17 | case 'signOut': 18 | return setSignedInUser(false) 19 | } 20 | }) 21 | try { 22 | await Auth.currentAuthenticatedUser() 23 | setSignedInUser(true) 24 | } catch (err) {} 25 | } 26 | return ( 27 |
28 | 46 |
47 | 48 |
49 |
50 | ) 51 | } 52 | 53 | export default MyApp -------------------------------------------------------------------------------- /amplify-next/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200 5 | res.json({ name: 'John Doe' }) 6 | } 7 | -------------------------------------------------------------------------------- /amplify-next/pages/create-post.js: -------------------------------------------------------------------------------- 1 | import { withAuthenticator } from '@aws-amplify/ui-react' 2 | import { useState, useRef } from 'react' // new 3 | import { API, Storage } from 'aws-amplify' 4 | import { v4 as uuid } from 'uuid' 5 | import { useRouter } from 'next/router' 6 | import SimpleMDE from "react-simplemde-editor" 7 | import "easymde/dist/easymde.min.css" 8 | import { createPost } from '../graphql/mutations' 9 | 10 | const initialState = { title: '', content: '' } 11 | 12 | function CreatePost() { 13 | const [post, setPost] = useState(initialState) 14 | const [image, setImage] = useState(null) 15 | const hiddenFileInput = useRef(null); 16 | const { title, content } = post 17 | const router = useRouter() 18 | function onChange(e) { 19 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 20 | } 21 | async function createNewPost() { 22 | if (!title || !content) return 23 | const id = uuid() 24 | post.id = id 25 | // If there is an image uploaded, store it in S3 and add it to the post metadata 26 | if (image) { 27 | const fileName = `${image.name}_${uuid()}` 28 | post.coverImage = fileName 29 | await Storage.put(fileName, image) 30 | } 31 | 32 | await API.graphql({ 33 | query: createPost, 34 | variables: { input: post }, 35 | authMode: "AMAZON_COGNITO_USER_POOLS" 36 | }) 37 | router.push(`/posts/${id}`) 38 | } 39 | async function uploadImage() { 40 | hiddenFileInput.current.click(); 41 | } 42 | function handleChange (e) { 43 | const fileUploaded = e.target.files[0]; 44 | if (!fileUploaded) return 45 | setImage(fileUploaded) 46 | } 47 | return ( 48 |
49 |

Create new post

50 | 57 | { 58 | image && ( 59 | 60 | ) 61 | } 62 | setPost({ ...post, content: value })} /> 63 | 69 | 75 | 80 |
81 | ) 82 | } 83 | 84 | export default withAuthenticator(CreatePost) -------------------------------------------------------------------------------- /amplify-next/pages/edit-post/[id].js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | import { API, Storage } from 'aws-amplify' 3 | import { useRouter } from 'next/router' 4 | import SimpleMDE from "react-simplemde-editor" 5 | import "easymde/dist/easymde.min.css" 6 | import { v4 as uuid } from 'uuid' 7 | import { updatePost } from '../../graphql/mutations' 8 | import { getPost } from '../../graphql/queries' 9 | 10 | function EditPost() { 11 | const [post, setPost] = useState(null) 12 | const router = useRouter() 13 | const { id } = router.query 14 | const [coverImage, setCoverImage] = useState(null) 15 | const [localImage, setLocalImage] = useState(null) 16 | const fileInput = useRef(null) 17 | 18 | useEffect(() => { 19 | fetchPost() 20 | async function fetchPost() { 21 | if (!id) return 22 | const postData = await API.graphql({ query: getPost, variables: { id }}) 23 | console.log('postData: ', postData) 24 | setPost(postData.data.getPost) 25 | if (postData.data.getPost.coverImage) { 26 | updateCoverImage(postData.data.getPost.coverImage) 27 | } 28 | } 29 | }, [id]) 30 | if (!post) return null 31 | async function updateCoverImage(coverImage) { 32 | const imageKey = await Storage.get(coverImage) 33 | setCoverImage(imageKey) 34 | } 35 | async function uploadImage() { 36 | fileInput.current.click(); 37 | } 38 | function handleChange (e) { 39 | const fileUploaded = e.target.files[0]; 40 | if (!fileUploaded) return 41 | setCoverImage(fileUploaded) 42 | setLocalImage(URL.createObjectURL(fileUploaded)) 43 | } 44 | function onChange(e) { 45 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 46 | } 47 | const { title, content } = post 48 | async function updateCurrentPost() { 49 | if (!title || !content) return 50 | const postUpdated = { 51 | id, content, title 52 | } 53 | // check to see if there is a cover image and that it has been updated 54 | if (coverImage && localImage) { 55 | const fileName = `${coverImage.name}_${uuid()}` 56 | postUpdated.coverImage = fileName 57 | await Storage.put(fileName, coverImage) 58 | } 59 | await API.graphql({ 60 | query: updatePost, 61 | variables: { input: postUpdated }, 62 | authMode: "AMAZON_COGNITO_USER_POOLS" 63 | }) 64 | console.log('post successfully updated!') 65 | router.push('/my-posts') 66 | } 67 | return ( 68 |
69 |

Edit post

70 | { 71 | coverImage && 72 | } 73 | 80 | setPost({ ...post, content: value })} /> 81 | 87 | 93 | 96 |
97 | ) 98 | } 99 | 100 | export default EditPost -------------------------------------------------------------------------------- /amplify-next/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import Link from 'next/link' 3 | import { API, Storage } from 'aws-amplify' 4 | import { listPosts } from '../graphql/queries' 5 | 6 | export default function Home() { 7 | const [posts, setPosts] = useState([]) 8 | useEffect(() => { 9 | fetchPosts() 10 | }, []) 11 | async function fetchPosts() { 12 | const postData = await API.graphql({ 13 | query: listPosts 14 | }) 15 | const { items } = postData.data.listPosts 16 | // Fetch images from S3 for posts that contain a cover image 17 | const postsWithImages = await Promise.all(items.map(async post => { 18 | if (post.coverImage) { 19 | post.coverImage = await Storage.get(post.coverImage) 20 | } 21 | return post 22 | })) 23 | setPosts(postsWithImages) 24 | } 25 | return ( 26 |
27 |

Posts

28 | { 29 | posts.map((post, index) => ( 30 | 31 |
32 | { 33 | post.coverImage && 34 | } 35 |
36 |

{post.title}

37 |

Author: {post.username}

38 |
39 |
40 | ) 41 | ) 42 | } 43 |
44 | ) 45 | } -------------------------------------------------------------------------------- /amplify-next/pages/my-posts.js: -------------------------------------------------------------------------------- 1 | // pages/my-posts.js 2 | import { useState, useEffect } from 'react' 3 | import Link from 'next/link' 4 | import { API, Auth } from 'aws-amplify' 5 | import { postsByUsername } from '../graphql/queries' 6 | import { deletePost as deletePostMutation } from '../graphql/mutations' 7 | 8 | export default function MyPosts() { 9 | const [posts, setPosts] = useState([]) 10 | useEffect(() => { 11 | fetchPosts() 12 | }, []) 13 | async function fetchPosts() { 14 | const { username } = await Auth.currentAuthenticatedUser() 15 | const postData = await API.graphql({ 16 | query: postsByUsername, variables: { username } 17 | }) 18 | setPosts(postData.data.postsByUsername.items) 19 | } 20 | async function deletePost(id) { 21 | await API.graphql({ 22 | query: deletePostMutation, 23 | variables: { input: { id } }, 24 | authMode: "AMAZON_COGNITO_USER_POOLS" 25 | }) 26 | fetchPosts() 27 | } 28 | return ( 29 |
30 |

My Posts

31 | { 32 | posts.map((post, index) => ( 33 |
34 |

{post.title}

35 |

Author: {post.username}

36 | Edit Post 37 | View Post 38 | 42 |
43 | )) 44 | } 45 |
46 | ) 47 | } -------------------------------------------------------------------------------- /amplify-next/pages/posts/[id].js: -------------------------------------------------------------------------------- 1 | import { API, Storage } from 'aws-amplify' 2 | import { useState, useEffect } from 'react' 3 | import { useRouter } from 'next/router' 4 | import ReactMarkdown from 'react-markdown' 5 | import { listPosts, getPost } from '../../graphql/queries' 6 | 7 | export default function Post({ post }) { 8 | const [coverImage, setCoverImage] = useState(null) 9 | useEffect(() => { 10 | updateCoverImage() 11 | }, []) 12 | async function updateCoverImage() { 13 | if (post.coverImage) { 14 | const imageKey = await Storage.get(post.coverImage) 15 | setCoverImage(imageKey) 16 | } 17 | } 18 | console.log('post: ', post) 19 | const router = useRouter() 20 | if (router.isFallback) { 21 | return
Loading...
22 | } 23 | return ( 24 |
25 |

{post.title}

26 | { 27 | coverImage && 28 | } 29 |

by {post.username}

30 |
31 | 32 |
33 |
34 | ) 35 | } 36 | 37 | export async function getStaticPaths() { 38 | const postData = await API.graphql({ 39 | query: listPosts 40 | }) 41 | const paths = postData.data.listPosts.items.map(post => ({ params: { id: post.id }})) 42 | return { 43 | paths, 44 | fallback: true 45 | } 46 | } 47 | 48 | export async function getStaticProps ({ params }) { 49 | const { id } = params 50 | const postData = await API.graphql({ 51 | query: getPost, variables: { id } 52 | }) 53 | return { 54 | props: { 55 | post: postData.data.getPost 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /amplify-next/pages/profile.js: -------------------------------------------------------------------------------- 1 | import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react' 2 | import { Auth } from 'aws-amplify' 3 | import { useState, useEffect } from 'react' 4 | 5 | function Profile() { 6 | const [user, setUser] = useState(null) 7 | useEffect(() => { 8 | checkUser() 9 | }, []) 10 | async function checkUser() { 11 | const user = await Auth.currentAuthenticatedUser() 12 | setUser(user) 13 | } 14 | if (!user) return null 15 | return ( 16 |
17 |

Profile

18 |

Username: {user.username}

19 |

Email: {user.attributes.email}

20 | 21 |
22 | ) 23 | } 24 | 25 | export default withAuthenticator(Profile) -------------------------------------------------------------------------------- /amplify-next/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /amplify-next/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/amplify-next/public/favicon.ico -------------------------------------------------------------------------------- /amplify-next/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /amplify-next/serverless.yml: -------------------------------------------------------------------------------- 1 | nextamplified: 2 | component: "@sls-next/serverless-component@1.18.0" -------------------------------------------------------------------------------- /amplify-next/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer img { 29 | margin-left: 0.5rem; 30 | } 31 | 32 | .footer a { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .title a { 39 | color: #0070f3; 40 | text-decoration: none; 41 | } 42 | 43 | .title a:hover, 44 | .title a:focus, 45 | .title a:active { 46 | text-decoration: underline; 47 | } 48 | 49 | .title { 50 | margin: 0; 51 | line-height: 1.15; 52 | font-size: 4rem; 53 | } 54 | 55 | .title, 56 | .description { 57 | text-align: center; 58 | } 59 | 60 | .description { 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 800px; 80 | margin-top: 3rem; 81 | } 82 | 83 | .card { 84 | margin: 1rem; 85 | flex-basis: 45%; 86 | padding: 1.5rem; 87 | text-align: left; 88 | color: inherit; 89 | text-decoration: none; 90 | border: 1px solid #eaeaea; 91 | border-radius: 10px; 92 | transition: color 0.15s ease, border-color 0.15s ease; 93 | } 94 | 95 | .card:hover, 96 | .card:focus, 97 | .card:active { 98 | color: #0070f3; 99 | border-color: #0070f3; 100 | } 101 | 102 | .card h3 { 103 | margin: 0 0 1rem 0; 104 | font-size: 1.5rem; 105 | } 106 | 107 | .card p { 108 | margin: 0; 109 | font-size: 1.25rem; 110 | line-height: 1.5; 111 | } 112 | 113 | .logo { 114 | height: 1em; 115 | } 116 | 117 | @media (max-width: 600px) { 118 | .grid { 119 | width: 100%; 120 | flex-direction: column; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /amplify-next/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --amplify-primary-color: #2563EB; 7 | --amplify-primary-tint: #2563EB; 8 | --amplify-primary-shade: #2563EB; 9 | } -------------------------------------------------------------------------------- /amplify-next/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require('@tailwindcss/typography') 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /images/app-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/images/app-2.png -------------------------------------------------------------------------------- /images/app-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/images/app-3.png -------------------------------------------------------------------------------- /images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/images/app.png -------------------------------------------------------------------------------- /images/app4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/images/app4.png -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/images/banner.jpg -------------------------------------------------------------------------------- /images/update-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-amplify-workshop/164c28d41a133598f1eb76aa436863ebed32d93f/images/update-api.png --------------------------------------------------------------------------------