├── .gitignore ├── .graphqlconfig.yml ├── .vscode └── settings.json ├── README.md ├── amplify ├── .config │ └── project-config.json ├── backend │ ├── api │ │ └── photoalbums │ │ │ ├── parameters.json │ │ │ ├── schema.graphql │ │ │ ├── stacks │ │ │ └── CustomResources.json │ │ │ └── transform.conf.json │ ├── auth │ │ └── photoalbums9240b8c0 │ │ │ ├── parameters.json │ │ │ └── photoalbums9240b8c0-cloudformation-template.yml │ ├── backend-config.json │ ├── function │ │ └── S3Triggerb01fd26e │ │ │ ├── S3Triggerb01fd26e-cloudformation-template.json │ │ │ └── src │ │ │ ├── event.json │ │ │ ├── index.js │ │ │ ├── package-lock.json │ │ │ └── package.json │ ├── hosting │ │ └── S3AndCloudFront │ │ │ ├── parameters.json │ │ │ └── template.json │ └── storage │ │ └── photoalbumsstorage │ │ ├── parameters.json │ │ ├── s3-cloudformation-template.json │ │ └── storage-params.json └── team-provider-info.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── graphql ├── mutations.js ├── queries.js ├── schema.json └── subscriptions.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js └── setupTests.js /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #amplify 26 | amplify/\#current-cloud-backend 27 | amplify/.config/local-* 28 | amplify/mock-data 29 | amplify/backend/amplify-meta.json 30 | amplify/backend/awscloudformation 31 | build/ 32 | dist/ 33 | node_modules/ 34 | aws-exports.js 35 | awsconfiguration.json 36 | amplifyconfiguration.json 37 | amplify-gradle-config.json 38 | amplifyxc.config -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | photoalbums: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [Goals](#goals) 2 | - [Install & configure AWS amplify:](#install-amp-configure-aws-amplify) 3 | - [Setup a React app with CRA](#setup-a-react-app-with-cra) 4 | - [Add Semantic UI React](#add-semantic-ui-react) 5 | - [Replace placeholder UI with our own](#replace-placeholder-ui-with-our-own) 6 | - [Initialize Amplify](#initialize-amplify) 7 | - [Add a GraphQL API with Amplify](#add-a-graphql-api-with-amplify) 8 | - [Add an AWS AppSync API](#add-an-aws-appsync-api) 9 | - [Manage Albums UI](#manage-albums-ui) 10 | - [Add Authentication](#add-authentication) 11 | - [Setup the auth front-end](#setup-the-auth-front-end) 12 | - [Recap : What we changed in App.js](#recap--what-we-changed-in-appjs) 13 | - [Try it out : Create an account](#try-it-out--create-an-account) 14 | - [Connect the app to the AppSync API](#connect-the-app-to-the-appsync-api) 15 | - [Allow users to create albums](#allow-users-to-create-albums) 16 | - [Show a live list of albums](#show-a-live-list-of-albums) 17 | - [Allow users to click into an album to view its details](#allow-users-to-click-into-an-album-to-view-its-details) 18 | - [Try out the app](#try-out-the-app) 19 | - [Add Cloud Storage](#add-cloud-storage) 20 | - [Manage photos](#manage-photos) 21 | - [Refactor : move auth to React Context](#refactor--move-auth-to-react-context) 22 | - [Extract labels from images using AI](#extract-labels-from-images-using-ai) 23 | - [Store labels in db](#store-labels-in-db) 24 | - [Deploy](#deploy) 25 | 26 | # Goals 27 | 28 | In this workshop, we’ll build an app with quite a few features, including: 29 | 30 | - Allowing user sign up and authentication, so we know who owns which photo albums 31 | 32 | - Building an API server, so our front end has a way to load the appropriate albums and photos to show a given user 33 | 34 | - Storing data about albums, photos, and permissions of who can view what, so that our API has a fast and reliable place to query and save data to 35 | 36 | - Storing and serving photos, so we have a place to put all of the photos that users are uploading to albums 37 | 38 | - Automatically detecting relevant labels for each uploaded photo and storing them. 39 | 40 | # Install & configure AWS amplify: 41 | 42 | ```sh 43 | npm install -g @aws-amplify/cli 44 | 45 | amplify configure 46 | ``` 47 | 48 | More info on getting started [here](https://aws-amplify.github.io/docs/cli-toolchain/quickstart?sdk=js). 49 | 50 | # Setup a React app with CRA 51 | 52 | ``` 53 | npx create-react-app photoalbums --use-npm; say done 54 | 55 | cd photoalbums 56 | 57 | git init 58 | 59 | git add --all 60 | 61 | git commit -m "initial react app with CRA" 62 | 63 | npm start 64 | ``` 65 | 66 | ## Add Semantic UI React 67 | 68 | Semantic UI components for React provide components that will help us quickly build a nice UI interface. 69 | 70 | ```sh 71 | npm i semantic-ui-react 72 | ``` 73 | 74 | In `public/index.html` add a link to the semantic-ui stylesheet from a CDN 75 | 76 | ```html 77 | 78 | 79 | 80 | 81 | 82 | 83 | ``` 84 | 85 | And restart the app : 86 | 87 | ```sh 88 | npm start 89 | ``` 90 | 91 | Nothing should have changed by now 92 | 93 | ## Replace placeholder UI with our own 94 | 95 | In `src/App.js` delete the entire file and replace it with : 96 | 97 | ```jsx 98 | // src/App.js 99 | 100 | import React from "react"; 101 | import { Header } from "semantic-ui-react"; 102 | 103 | const App = () => { 104 | return ( 105 |
106 |
Hello World!
107 |
108 | ); 109 | }; 110 | 111 | export default App; 112 | ``` 113 | 114 | As expected, the app now consists of a header that says Hello World. 115 | 116 | Let's commit our changes before continuing : 117 | 118 | ```sh 119 | git add --all 120 | git commit -m "integrate react-semantic-ui and add hello world example" 121 | ``` 122 | 123 | # Initialize Amplify 124 | 125 | ```sh 126 | amplify init 127 | 128 | ? Enter a name for the project photoalbums 129 | ? Enter a name for the environment dev 130 | ? Choose your default editor: Visual Studio Code 131 | ? Choose the type of app that you're building javascript 132 | Please tell us about your project 133 | ? What javascript framework are you using react 134 | ? Source Directory Path: src 135 | ? Distribution Directory Path: build 136 | ? Build Command: npm run-script build 137 | ? Start Command: npm run-script start 138 | ? Do you want to use an AWS profile? Yes 139 | ? Please choose the profile you want to use defaultoraprofile 140 | 141 | ``` 142 | 143 | This will create an amplify project locally and on the cloud that we will build on for the rest of our project. 144 | 145 | Before we continue let's commit our progress : 146 | 147 | ```sh 148 | git add --all 149 | 150 | git commit -m "added amplify setup to react app" 151 | ``` 152 | 153 | # Add a GraphQL API with Amplify 154 | 155 | We now want to create an API for creating albums entities. 156 | 157 | These albums will only have a name at first. 158 | 159 | > To build our API we’ll use AWS AppSync, a managed GraphQL service for building data-driven apps. 160 | > If you’re not yet familiar with the basics of GraphQL, you should take a few minutes and check out https://graphql.github.io/learn/ before continuing, 161 | > or use the site to refer back to when you have questions as you read along. 162 | 163 | ## Add an AWS AppSync API 164 | 165 | ```sh 166 | amplify add api 167 | 168 | ? Please select from one of the below mentioned services: GraphQL 169 | ? Provide API name: photoalbums 170 | ? Choose the default authorization type for the API Amazon Cognito User Pool 171 | Using service: Cognito, provided by: awscloudformation 172 | 173 | The current configured provider is Amazon Cognito. 174 | 175 | ? Do you want to use the default authentication and security configuration? Default configuration 176 | Warning: you will not be able to edit these selections. 177 | ? How do you want users to be able to sign in? Username 178 | ? Do you want to configure advanced settings? No, I am done. 179 | Successfully added auth resource 180 | ? Do you want to configure advanced settings for the GraphQL API (Use arrow keys) 181 | ❯ No, I am done. 182 | Yes, I want to make some additional changes. 183 | ? Do you have an annotated GraphQL schema? (y/N) N 184 | ? Do you want a guided schema creation? Yes 185 | Single object with fields (e.g., “Todo” with ID, name, description) 186 | ❯ One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”) 187 | Objects with fine-grained access control (e.g., a project management app with 188 | owner-based authorization) 189 | ? Do you want to edit the schema now? (Y/n) Y 190 | ? Press enter to continue 191 | ``` 192 | 193 | Enter the following schema : 194 | 195 | ```graphql 196 | type Album @model @auth(rules: [{ allow: owner }]) { 197 | id: ID! 198 | name: String! 199 | createdAt: String! 200 | photos: [Photo] @connection(name: "AlbumPhotos") 201 | } 202 | 203 | type Photo @model @auth(rules: [{ allow: owner }]) { 204 | id: ID! 205 | album: Album @connection(name: "AlbumPhotos") 206 | bucket: String! 207 | name: String! 208 | createdAt: String! 209 | } 210 | ``` 211 | 212 | and press enter. 213 | 214 | Before we continue let's commit our changes 215 | 216 | ```sh 217 | git add --all 218 | git commit -m "initialized offline amplify api" 219 | ``` 220 | 221 | And we can then push our changes to the cloud to have Amplify setup the API for us 222 | 223 | ```sh 224 | amplify push 225 | 226 | ? Are you sure you want to continue? Yes 227 | ? Do you want to generate code for your newly created GraphQL API (Y/n) Y 228 | ``` 229 | 230 | In addition to the generated API, Amplify generated some code for us that we will examine later. But meanwhile let's commit our changes. 231 | 232 | ```sh 233 | git add --all 234 | git commit -m "added amplify codegen and deployed appsync api" 235 | ``` 236 | 237 | ## Manage Albums UI 238 | 239 | Let’s update our front-end to: 240 | 241 | - allow users to create albums 242 | - show a list of albums 243 | - allow users to click into an album to view its details 244 | 245 | To do that we'll need multiple routes. We can use `react-router-dom` to create and manage routes. 246 | 247 | So let's install the dependencies 248 | 249 | ```sh 250 | npm i react-router-dom @aws-amplify/datastore @aws-amplify/core lodash 251 | ``` 252 | 253 | First we setup the UI code without any interaction with the API: 254 | 255 | ```jsx 256 | import React from "react"; 257 | import { Grid, Header, Input, List, Segment } from "semantic-ui-react"; 258 | import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"; 259 | import sortBy from "lodash/sortBy"; 260 | 261 | const NewAlbum = () => { 262 | const [albumName, setAlbumName] = React.useState(""); 263 | const handleSubmit = () => { 264 | console.log(`Creating album ${albumName} `); 265 | setAlbumName(""); 266 | }; 267 | const handleChange = event => { 268 | setAlbumName(event.target.value); 269 | }; 270 | return ( 271 | 272 |
Add a new album
273 | 283 |
284 | ); 285 | }; 286 | 287 | const AlbumsList = ({ albums = [] }) => { 288 | return ( 289 | 290 |
My Albums
291 | 292 | {sortBy(albums, ["createdAt"]).map(album => ( 293 | 294 | {album.name} 295 | 296 | ))} 297 | 298 |
299 | ); 300 | }; 301 | const AlbumDetailsLoader = ({ id }) => { 302 | const [isLoading] = React.useState(true); 303 | const [album] = React.useState({}); 304 | if (isLoading) { 305 | return
Loading...
; 306 | } 307 | return ; 308 | }; 309 | 310 | const AlbumDetails = ({ album }) => { 311 | return ( 312 | 313 |
{album.name}
314 |

TODO: Allow photo uploads

315 |

TODO: Show photos for this album

316 |
317 | ); 318 | }; 319 | 320 | const AlbumsListLoader = () => { 321 | const [isLoading] = React.useState(true); 322 | const [albums] = React.useState([]); 323 | if (isLoading) return null; 324 | return ; 325 | }; 326 | 327 | const App = () => { 328 | return ( 329 | 330 | 331 | 332 | 333 | 334 | 335 | ( 338 |
339 | Back to Albums list 340 |
341 | )} 342 | /> 343 | ( 346 | 347 | )} 348 | /> 349 |
350 |
351 |
352 | ); 353 | }; 354 | 355 | export default App; 356 | ``` 357 | 358 | Running `npm start` now should show us the app without any logic attached to it. 359 | 360 | Then let's commit our changes to git 361 | 362 | ```sh 363 | git add --all 364 | git commit -m "added UI code without connection to API." 365 | ``` 366 | 367 | Before we connect to the API we will need to allow users to authenticate. 368 | 369 | # Add Authentication 370 | 371 | Amplify makes it fast to add industrial strength authentication to our app. 372 | 373 | ## Setup the auth front-end 374 | 375 | Amplify provides a solid set of tools that make this step extremely straight-forward. 376 | 377 | We start by adding amplify front end dependencies : 378 | 379 | ```sh 380 | npm install --save aws-amplify aws-amplify-react 381 | ``` 382 | 383 | And then we'll use the `withAuthenticator` higher-order React components to wrap our existing app component. This will take care of rendering a simple UI for letting users sign up, confirm their account, sign in, sign out, or reset their password. 384 | 385 | In `src/App.js` import `withAuthenticator`and instead of exporting App we export withAuthenticator(App) 386 | 387 | ```jsx 388 | // src/App.js 389 | 390 | // ... 391 | import { withAuthenticator } from "aws-amplify-react"; 392 | import Amplify from "@aws-amplify/core"; 393 | // This was added by amplify when we initialized it and added auth. 394 | import aws_exports from "./aws-exports"; 395 | // We use the generated file to config Amplify with our desired settings 396 | Amplify.configure(aws_exports); 397 | 398 | // ... 399 | 400 | const App = () => { 401 | // ... 402 | // ... 403 | // ... 404 | }; 405 | 406 | export default withAuthenticator(App, { includeGreetings: true }); 407 | ``` 408 | 409 | We can commit these small changes that adds authentication to our front-end UI 410 | 411 | ```sh 412 | git add --all 413 | 414 | git commit -m "add authentication ui" 415 | ``` 416 | 417 | ## Recap : What we changed in App.js 418 | 419 | - Imported and configured the AWS Amplify JS library 420 | 421 | - Imported the withAuthenticator higher order component from aws-amplify-react 422 | 423 | - Wrapped the App component using withAuthenticator 424 | 425 | ## Try it out : Create an account 426 | 427 | Enter your information with a valid email (it will be used to send you a verification code). 428 | 429 | Then login with the account you've created and you will get back to a page containing the App component and a header with your username and a link to log you out. 430 | 431 | # Connect the app to the AppSync API 432 | 433 | ## Allow users to create albums 434 | 435 | We start by importing createAlbum mutation and : 436 | 437 | ```jsx 438 | import React from "react"; 439 | import { Grid, Header, Input, List, Segment } from "semantic-ui-react"; 440 | import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"; 441 | import sortBy from "lodash/sortBy"; 442 | import { withAuthenticator } from "aws-amplify-react"; 443 | import { createAlbum } from "./graphql/mutations"; 444 | import API, { graphqlOperation } from "@aws-amplify/api"; 445 | import Amplify from "aws-amplify"; 446 | import awsconfig from "./aws-exports"; 447 | Amplify.configure(awsconfig); 448 | 449 | const NewAlbum = () => { 450 | const [albumName, setAlbumName] = React.useState(""); 451 | const handleSubmit = async () => { 452 | console.log(`Creating album ${albumName} `); 453 | await API.graphql( 454 | graphqlOperation(createAlbum, { 455 | input: { 456 | name: albumName, 457 | createdAt: `${Date.now()}` 458 | } 459 | }) 460 | ); 461 | setAlbumName(""); 462 | }; 463 | const handleChange = event => { 464 | setAlbumName(event.target.value); 465 | }; 466 | return ( 467 | 468 |
Add a new album
469 | 479 |
480 | ); 481 | }; 482 | ``` 483 | 484 | ## Show a live list of albums 485 | 486 | In the previous section we've seen how to add new albums, the next logical step is to display a list of the added albums. 487 | 488 | To do that we'll update our AlbumsListLoader to fetch albums data and listen to new updates. 489 | 490 | ```jsx 491 | import Amplify, { Auth } from "aws-amplify"; 492 | 493 | import { onCreateAlbum } from "./graphql/subscriptions"; 494 | import { listAlbums } from "./graphql/queries"; 495 | 496 | const AlbumsListLoader = () => { 497 | const [isLoading, setIsLoading] = React.useState(true); 498 | const [albums, setAlbums] = React.useState([]); 499 | React.useEffect(() => { 500 | setIsLoading(true); 501 | // Get initial albums list 502 | API.graphql(graphqlOperation(listAlbums)).then(albs => { 503 | setAlbums(albs.data.listAlbums.items); 504 | setIsLoading(false); 505 | }); 506 | 507 | Auth.currentAuthenticatedUser().then(user => { 508 | // Listen to new albums being added 509 | API.graphql( 510 | graphqlOperation(onCreateAlbum, { owner: user.username }) 511 | ).subscribe(newAlbum => { 512 | const albumRecord = newAlbum.value.data.onCreateAlbum; 513 | setAlbums(albs => [...albs, albumRecord]); 514 | }); 515 | }); 516 | }, []); 517 | if (isLoading) return null; 518 | return ; 519 | }; 520 | ``` 521 | 522 | ## Allow users to click into an album to view its details 523 | 524 | ```jsx 525 | import { listAlbums, getAlbum } from "./graphql/queries"; 526 | 527 | const AlbumDetailsLoader = ({ id }) => { 528 | const [isLoading, setIsLoading] = React.useState(false); 529 | const [album, setAlbum] = React.useState({}); 530 | 531 | React.useEffect(() => { 532 | setIsLoading(true); 533 | API.graphql(graphqlOperation(getAlbum, { id })).then(albumDetails => { 534 | setIsLoading(false); 535 | setAlbum(albumDetails.data.getAlbum); 536 | }); 537 | }, [id]); 538 | 539 | if (isLoading) { 540 | return
Loading...
; 541 | } 542 | return ; 543 | }; 544 | ``` 545 | 546 | By now we have a running app with a secure infinitely scalable API that syncs our data to DynamoDB on AWS . 547 | 548 | Let's do a quick commit : 549 | 550 | ```sh 551 | git add --all 552 | git commit -m "working app with AppSync" 553 | ``` 554 | 555 | And now to add storage so we can store uploaded images in the cloud. 556 | 557 | ## Try out the app 558 | 559 | Check out the app now and try out the new features: 560 | 561 | View the list of albums 562 | 563 | Create a new album and see it appear in the albums list 564 | 565 | Click into an album to see the beginnings of our Album details view 566 | 567 | When viewing an Album, click ‘Back to Albums list’ to go home 568 | 569 | # Add Cloud Storage 570 | 571 | We’ll need a place to store all of the photos that get uploaded to our albums. Amazon Simple Storage Service (S3) is a great option for this and Amplify’s Storage module makes setting up and working with S3 very easy 572 | 573 | We'll start by adding the storage category to the amplify app. 574 | 575 | ```sh 576 | $ amplify add storage 577 | 578 | 579 | ? Please select from one of the below mentioned services: 580 | 581 | Content (Images, audio, video, etc.) 582 | 583 | 584 | ? Please provide a friendly name for your resource that will be used to label this category in the project: photoalbumsstorage 585 | 586 | 587 | ? Please provide bucket name: 588 | 589 | 590 | ? Who should have access: Auth and guest users 591 | 592 | 593 | ? What kind of access do you want for Authenticated users? 594 | ◉ create/update 595 | ◉ read 596 | ◉ delete 597 | 598 | 599 | ? What kind of access do you want for Guest users? 600 | ◯ create/update 601 | ◉ read 602 | ◯ delete 603 | 604 | 605 | ? Do you want to add a Lambda Trigger for your S3 Bucket? No 606 | 607 | 608 | ? Select from the following options 609 | Create a new function 610 | 611 | 612 | ? Do you want to edit the local S3Triggerxxxxxxx lambda function now? (Y/n) 613 | No 614 | ``` 615 | 616 | And then we push our resources to the cloud as we usually do with : 617 | 618 | ```sh 619 | amplify push 620 | ``` 621 | 622 | And then commit our changes : 623 | 624 | ```sh 625 | git add --all 626 | git commit -m "added storage category and sample lambda function" 627 | ``` 628 | 629 | ## Manage photos 630 | 631 | To be able to upload photos we'll create a S3ImageUpload component. 632 | 633 | We'll need to bring in a small dependency to help us generate photo names : 634 | 635 | ```sh 636 | npm install --save uuid 637 | ``` 638 | 639 | ```jsx 640 | // Rest of deps 641 | import { Storage } from "aws-amplify"; 642 | import { S3Image } from "aws-amplify-react"; 643 | import { v4 as uuid } from "uuid"; 644 | import { Grid, Header, Input, List, Segment, Form } from "semantic-ui-react"; 645 | 646 | const uploadFile = async (event, albumId) => { 647 | const { 648 | target: { value, files } 649 | } = event; 650 | const user = await Auth.currentAuthenticatedUser(); 651 | const fileForUpload = files[0]; 652 | const file = fileForUpload || value; 653 | const extension = file.name.split(".")[1]; 654 | const { type: mimeType } = file; 655 | const key = `images/${uuid()}${albumId}.${extension}`; 656 | try { 657 | await Storage.put(key, file, { 658 | contentType: mimeType, 659 | metadata: { 660 | owner: user.username, 661 | albumId 662 | } 663 | }); 664 | console.log("successfully uploaded image!"); 665 | } catch (err) { 666 | console.log("error: ", err); 667 | } 668 | }; 669 | 670 | const S3ImageUpload = ({ albumId }) => { 671 | const [isUploading, setIsUploading] = React.useState(false); 672 | const onChange = async event => { 673 | setIsUploading(true); 674 | 675 | let files = []; 676 | for (var i = 0; i < event.target.files.length; i++) { 677 | files.push(event.target.files.item(i)); 678 | } 679 | await Promise.all(files.map(f => uploadFile(event, albumId))); 680 | 681 | setIsUploading(false); 682 | }; 683 | return ( 684 |
685 | document.getElementById("add-image-file-input").click()} 687 | disabled={isUploading} 688 | icon="file image outline" 689 | content={isUploading ? "Uploading..." : "Add Images"} 690 | /> 691 | 699 |
700 | ); 701 | }; 702 | 703 | const AlbumDetails = ({ album }) => { 704 | return ( 705 | 706 |
{album.name}
707 | 708 |

TODO: Show photos for this album

709 |
710 | ); 711 | }; 712 | ``` 713 | 714 | By adding this code we should be able to upload an image and view it in the AWS bucket we specified when setting up the Storage category. Check your s3 bucket (https://s3.console.aws.amazon.com/s3/home) to make sure your file is being uploaded as expected. 715 | 716 | We still need to : 717 | 718 | - Save the picture information as part of the album 719 | - Retrieve and display pictures of an album 720 | 721 | Let's start by saving the picture information to our AppSync API. 722 | 723 | We will need to modify our uploadFile method : 724 | 725 | ```js 726 | import { createAlbum, createPhoto } from "./graphql/mutations"; 727 | 728 | const uploadFile = async (event, albumId) => { 729 | const { 730 | target: { value, files } 731 | } = event; 732 | const fileForUpload = files[0]; 733 | const file = fileForUpload || value; 734 | const extension = file.name.split(".")[1]; 735 | const { type: mimeType } = file; 736 | const key = `images/${uuid()}${albumId}.${extension}`; 737 | try { 738 | await Storage.put(key, file, { 739 | contentType: mimeType 740 | }); 741 | console.log("successfully uploaded image!"); 742 | } catch (err) { 743 | console.log("error: ", err); 744 | } 745 | await API.graphql( 746 | graphqlOperation(createPhoto, { 747 | input: { 748 | bucket: awsconfig.aws_user_files_s3_bucket, 749 | name: key, 750 | createdAt: `${Date.now()}`, 751 | photoAlbumId: albumId 752 | } 753 | }) 754 | ); 755 | }; 756 | ``` 757 | 758 | And to retrieve and display pictures of an album: 759 | 760 | ```jsx 761 | import { 762 | Grid, 763 | Header, 764 | Input, 765 | List, 766 | Segment, 767 | Form, 768 | Divider 769 | } from "semantic-ui-react"; 770 | 771 | const PhotosList = ({ photos }) => { 772 | return ( 773 |
774 |
788 | ); 789 | }; 790 | 791 | const AlbumDetails = ({ album }) => { 792 | return ( 793 | 794 |
{album.name}
795 | 796 | 797 |
798 | ); 799 | }; 800 | ``` 801 | 802 | By now we still need to refresh the page to see the uploaded image on the page. 803 | 804 | To directly have the photos be updated, we can either setup a subscription to new photo creation or to change events in the album. 805 | 806 | Let's listen to the onCreatePhoto event for a more granular update. 807 | 808 | But before we do, a quick commit : 809 | 810 | ```sh 811 | git add --all 812 | git commit -m "added s3 upload and display photo functionality" 813 | ``` 814 | 815 | AlbumDetailsLoader becomes 816 | 817 | ```jsx 818 | const AlbumDetailsLoader = ({ id }) => { 819 | const [isLoading, setIsLoading] = React.useState(false); 820 | const [album, setAlbum] = React.useState({}); 821 | 822 | React.useEffect(() => { 823 | setIsLoading(true); 824 | API.graphql(graphqlOperation(getAlbum, { id })).then(albumDetails => { 825 | setIsLoading(false); 826 | setAlbum(albumDetails.data.getAlbum); 827 | }); 828 | Auth.currentAuthenticatedUser().then(user => { 829 | API.graphql( 830 | graphqlOperation(onCreatePhoto, { owner: user.username }) 831 | ).subscribe(photo => { 832 | const newPhoto = photo.value.data.onCreatePhoto; 833 | setAlbum(alb => { 834 | return { ...alb, photos: { items: [newPhoto, ...alb.photos.items] } }; 835 | }); 836 | }); 837 | }); 838 | }, [id]); 839 | 840 | if (isLoading) { 841 | return
Loading...
; 842 | } 843 | return ; 844 | }; 845 | ``` 846 | 847 | # Refactor : move auth to React Context 848 | 849 | Up till now we've been fetching the user's username in multiple components, in order to avoid having promises everywhere in our `useEffect`s let's store the user auth data in context. 850 | 851 | To do that we'll store authentication credentials at the top level of our app 852 | 853 | In our App component 854 | 855 | ```jsx 856 | const UserContext = React.createContext({ username: null }); 857 | 858 | const App = () => { 859 | const [user, setUser] = React.useState({ username: null }); 860 | React.useEffect(() => { 861 | Auth.currentAuthenticatedUser().then(user => { 862 | setUser(user); 863 | }); 864 | }, []); 865 | return ( 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | ( 876 |
877 | Back to Albums list 878 |
879 | )} 880 | /> 881 | ( 884 | 885 | )} 886 | /> 887 |
888 |
889 |
890 |
891 | ); 892 | }; 893 | ``` 894 | 895 | And we can then retrieve the username by using `useContext` for example AlbumsListLoader's effect becomes : 896 | 897 | ```jsx 898 | const AlbumsListLoader = () => { 899 | const [isLoading, setIsLoading] = React.useState(true); 900 | const [albums, setAlbums] = React.useState([]); 901 | const { username } = React.useContext(UserContext); 902 | React.useEffect(() => { 903 | let isMounted = true; 904 | if (!username) return; 905 | setIsLoading(true); 906 | API.graphql(graphqlOperation(listAlbums)).then(albs => { 907 | if (!isMounted) return; 908 | setAlbums(albs.data.listAlbums.items); 909 | setIsLoading(false); 910 | }); 911 | const sub = API.graphql( 912 | graphqlOperation(onCreateAlbum, { owner: username }) 913 | ).subscribe(newAlbum => { 914 | const albumRecord = newAlbum.value.data.onCreateAlbum; 915 | setAlbums(albs => [...albs, albumRecord]); 916 | }); 917 | return () => { 918 | sub.unsubscribe(); 919 | isMounted = false; 920 | }; 921 | }, [username]); 922 | if (isLoading) return null; 923 | return ; 924 | }; 925 | ``` 926 | 927 | And we commit before moving to the last part: 928 | 929 | ```sh 930 | git add --all 931 | git commit -m "moved auth data to context and cleanup in effects" 932 | ``` 933 | 934 | # Extract labels from images using AI 935 | 936 | In our graphQL schema `amplify/backend/api/photoalbums/schema.graphql` let's tell amplify that we're interested in turning images to text. 937 | 938 | ```graphql 939 | type Album @model @auth(rules: [{ allow: owner }]) { 940 | id: ID! 941 | name: String! 942 | createdAt: String! 943 | photos: [Photo] @connection(name: "AlbumPhotos") 944 | } 945 | 946 | type Photo @model @auth(rules: [{ allow: owner }]) { 947 | id: ID! 948 | album: Album @connection(name: "AlbumPhotos") 949 | bucket: String! 950 | name: String! 951 | createdAt: String! 952 | } 953 | 954 | type Query { 955 | convertImageToText: String @predictions(actions: [identifyLabels]) 956 | } 957 | ``` 958 | 959 | Then we ask amplify to create the needed resources on the cloud 960 | 961 | ```sh 962 | amplify push 963 | ``` 964 | 965 | Now we can extract labels from uploaded images by simply running a graphQL query on our API. 966 | 967 | In code this looks like the following : 968 | 969 | ```jsx 970 | import { listAlbums, getAlbum, convertImageToText } from "./graphql/queries"; 971 | 972 | const uploadFile = async (event, albumId, username) => { 973 | const { 974 | target: { value, files } 975 | } = event; 976 | const fileForUpload = files[0]; 977 | const file = fileForUpload || value; 978 | const extension = file.name.split(".")[1]; 979 | const { type: mimeType } = file; 980 | const key = `images/${uuid()}${albumId}.${extension}`; 981 | let s3Obj; 982 | try { 983 | s3Obj = await Storage.put(key, file, { 984 | contentType: mimeType, 985 | metadata: { 986 | owner: username, 987 | albumId 988 | } 989 | }); 990 | console.log("successfully uploaded image!"); 991 | } catch (err) { 992 | console.log("error: ", err); 993 | return; 994 | } 995 | const s3ImageKey = s3Obj.key; 996 | const predictionResult = await API.graphql( 997 | graphqlOperation(convertImageToText, { 998 | input: { 999 | identifyLabels: { 1000 | key: s3ImageKey 1001 | } 1002 | } 1003 | }) 1004 | ); 1005 | const imageLabels = predictionResult.data.convertImageToText; 1006 | console.warn({ imageLabels }); 1007 | 1008 | await API.graphql( 1009 | graphqlOperation(createPhoto, { 1010 | input: { 1011 | bucket: awsconfig.aws_user_files_s3_bucket, 1012 | name: key, 1013 | createdAt: `${Date.now()}`, 1014 | photoAlbumId: albumId 1015 | } 1016 | }) 1017 | ); 1018 | }; 1019 | ``` 1020 | 1021 | If you try it, it should output an array of labels describing the image. 1022 | 1023 | ## Store labels in db 1024 | 1025 | As a last step let's add an array of strings to the graphql Photo type and display it in our UI. 1026 | 1027 | We start by modifying the Photo graphQL type 1028 | 1029 | ```graphql 1030 | type Photo @model @auth(rules: [{ allow: owner }]) { 1031 | id: ID! 1032 | album: Album @connection(name: "AlbumPhotos") 1033 | bucket: String! 1034 | name: String! 1035 | createdAt: String! 1036 | labels: [String] 1037 | } 1038 | ``` 1039 | 1040 | and push 1041 | 1042 | ```sh 1043 | amplify push 1044 | ``` 1045 | 1046 | And last we write the labels to our photo object after running the prediction in the `uploadFile` method 1047 | 1048 | ```js 1049 | const s3ImageKey = s3Obj.key; 1050 | const predictionResult = await API.graphql( 1051 | graphqlOperation(convertImageToText, { 1052 | input: { 1053 | identifyLabels: { 1054 | key: s3ImageKey 1055 | } 1056 | } 1057 | }) 1058 | ); 1059 | const imageLabels = predictionResult.data.convertImageToText; 1060 | console.warn({ imageLabels }); 1061 | 1062 | await API.graphql( 1063 | graphqlOperation(createPhoto, { 1064 | input: { 1065 | bucket: awsconfig.aws_user_files_s3_bucket, 1066 | name: key, 1067 | createdAt: `${Date.now()}`, 1068 | photoAlbumId: albumId, 1069 | labels: imageLabels 1070 | } 1071 | }) 1072 | ); 1073 | ``` 1074 | 1075 | And in our PhotosList component : 1076 | 1077 | ```jsx 1078 | const PhotosList = ({ photos }) => { 1079 | return ( 1080 |
1081 |
1100 | ); 1101 | }; 1102 | ``` 1103 | 1104 | And our final commit : 1105 | 1106 | ```sh 1107 | git add --all 1108 | git commit -m "added image labeling + labels storing and displaying" 1109 | ``` 1110 | 1111 | # Deploy 1112 | 1113 | We've built a simple single page application that can be deployed to any static file server. 1114 | 1115 | To stick with AWS let's use Amplify to deploy it to a share-able URL, while keeping in mind that any provider would do. 1116 | 1117 | ```sh 1118 | amplify hosting add 1119 | ? Select the environment setup: DEV (S3 only with HTTP) 1120 | ? hosting bucket name photoalbums-20191221083147-hostingbucket 1121 | ? index doc for the website index.html 1122 | ? error doc for the website index.html 1123 | 1124 | amplify publish 1125 | 1126 | ``` 1127 | 1128 | And this deploys our site to a unique development URL : http://photoalbums-20191221083147-hostingbucket-dev.s3-website-us-east-1.amazonaws.com/ 1129 | -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "photoalbums", 3 | "version": "2.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /amplify/backend/api/photoalbums/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "photoalbums", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": "false", 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authphotoalbums9240b8c0", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /amplify/backend/api/photoalbums/schema.graphql: -------------------------------------------------------------------------------- 1 | type Album @model @auth(rules: [{ allow: owner }]) { 2 | id: ID! 3 | name: String! 4 | createdAt: String! 5 | photos: [Photo] @connection(name: "AlbumPhotos") 6 | } 7 | 8 | type Photo @model @auth(rules: [{ allow: owner }]) { 9 | id: ID! 10 | album: Album @connection(name: "AlbumPhotos") 11 | bucket: String! 12 | name: String! 13 | createdAt: String! 14 | labels: [String] 15 | } 16 | 17 | type Query { 18 | convertImageToText: String @predictions(actions: [identifyLabels]) 19 | } 20 | -------------------------------------------------------------------------------- /amplify/backend/api/photoalbums/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": [ 50 | "true", 51 | "false" 52 | ] 53 | } 54 | }, 55 | "Outputs": { 56 | "EmptyOutput": { 57 | "Description": "An empty output. You may delete this if you have at least one resource above.", 58 | "Value": "" 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /amplify/backend/api/photoalbums/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5 3 | } -------------------------------------------------------------------------------- /amplify/backend/auth/photoalbums9240b8c0/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "photoalbums9240b8c0_identitypool_9240b8c0", 3 | "allowUnauthenticatedIdentities": true, 4 | "resourceNameTruncated": "photoa9240b8c0", 5 | "userPoolName": "photoalbums9240b8c0_userpool_9240b8c0", 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": "photoa9240b8c0_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "resourceName": "photoalbums9240b8c0", 34 | "authSelections": "identityPoolAndUserPool", 35 | "authRoleArn": { 36 | "Fn::GetAtt": [ 37 | "AuthRole", 38 | "Arn" 39 | ] 40 | }, 41 | "unauthRoleArn": { 42 | "Fn::GetAtt": [ 43 | "UnauthRole", 44 | "Arn" 45 | ] 46 | }, 47 | "useDefault": "default", 48 | "userPoolGroupList": [], 49 | "dependsOn": [] 50 | } -------------------------------------------------------------------------------- /amplify/backend/auth/photoalbums9240b8c0/photoalbums9240b8c0-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 | allowUnauthenticatedIdentities: 18 | Type: String 19 | 20 | resourceNameTruncated: 21 | Type: String 22 | 23 | userPoolName: 24 | Type: String 25 | 26 | autoVerifiedAttributes: 27 | Type: CommaDelimitedList 28 | 29 | mfaConfiguration: 30 | Type: String 31 | 32 | mfaTypes: 33 | Type: CommaDelimitedList 34 | 35 | smsAuthenticationMessage: 36 | Type: String 37 | 38 | smsVerificationMessage: 39 | Type: String 40 | 41 | emailVerificationSubject: 42 | Type: String 43 | 44 | emailVerificationMessage: 45 | Type: String 46 | 47 | defaultPasswordPolicy: 48 | Type: String 49 | 50 | passwordPolicyMinLength: 51 | Type: Number 52 | 53 | passwordPolicyCharacters: 54 | Type: CommaDelimitedList 55 | 56 | requiredAttributes: 57 | Type: CommaDelimitedList 58 | 59 | userpoolClientGenerateSecret: 60 | Type: String 61 | 62 | userpoolClientRefreshTokenValidity: 63 | Type: Number 64 | 65 | userpoolClientWriteAttributes: 66 | Type: CommaDelimitedList 67 | 68 | userpoolClientReadAttributes: 69 | Type: CommaDelimitedList 70 | 71 | userpoolClientLambdaRole: 72 | Type: String 73 | 74 | userpoolClientSetAttributes: 75 | Type: String 76 | 77 | resourceName: 78 | Type: String 79 | 80 | authSelections: 81 | Type: String 82 | 83 | useDefault: 84 | Type: String 85 | 86 | userPoolGroupList: 87 | Type: CommaDelimitedList 88 | 89 | dependsOn: 90 | Type: CommaDelimitedList 91 | 92 | Conditions: 93 | ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ] 94 | 95 | Resources: 96 | 97 | 98 | # BEGIN SNS ROLE RESOURCE 99 | SNSRole: 100 | # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process 101 | Type: AWS::IAM::Role 102 | Properties: 103 | RoleName: !If [ShouldNotCreateEnvResources, 'photoa9240b8c0_sns-role', !Join ['',[ 'sns', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 104 | AssumeRolePolicyDocument: 105 | Version: "2012-10-17" 106 | Statement: 107 | - Sid: "" 108 | Effect: "Allow" 109 | Principal: 110 | Service: "cognito-idp.amazonaws.com" 111 | Action: 112 | - "sts:AssumeRole" 113 | Condition: 114 | StringEquals: 115 | sts:ExternalId: photoa9240b8c0_role_external_id 116 | Policies: 117 | - 118 | PolicyName: photoa9240b8c0-sns-policy 119 | PolicyDocument: 120 | Version: "2012-10-17" 121 | Statement: 122 | - 123 | Effect: "Allow" 124 | Action: 125 | - "sns:Publish" 126 | Resource: "*" 127 | # BEGIN USER POOL RESOURCES 128 | UserPool: 129 | # Created upon user selection 130 | # Depends on SNS Role for Arn if MFA is enabled 131 | Type: AWS::Cognito::UserPool 132 | UpdateReplacePolicy: Retain 133 | Properties: 134 | UserPoolName: !If [ShouldNotCreateEnvResources, !Ref userPoolName, !Join ['',[!Ref userPoolName, '-', !Ref env]]] 135 | 136 | Schema: 137 | 138 | - 139 | Name: email 140 | Required: true 141 | Mutable: true 142 | 143 | 144 | 145 | 146 | AutoVerifiedAttributes: !Ref autoVerifiedAttributes 147 | 148 | 149 | EmailVerificationMessage: !Ref emailVerificationMessage 150 | EmailVerificationSubject: !Ref emailVerificationSubject 151 | 152 | Policies: 153 | PasswordPolicy: 154 | MinimumLength: !Ref passwordPolicyMinLength 155 | RequireLowercase: false 156 | RequireNumbers: false 157 | RequireSymbols: false 158 | RequireUppercase: false 159 | 160 | MfaConfiguration: !Ref mfaConfiguration 161 | SmsVerificationMessage: !Ref smsVerificationMessage 162 | SmsConfiguration: 163 | SnsCallerArn: !GetAtt SNSRole.Arn 164 | ExternalId: photoa9240b8c0_role_external_id 165 | 166 | 167 | UserPoolClientWeb: 168 | # Created provide application access to user pool 169 | # Depends on UserPool for ID reference 170 | Type: "AWS::Cognito::UserPoolClient" 171 | Properties: 172 | ClientName: photoa9240b8c0_app_clientWeb 173 | 174 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 175 | UserPoolId: !Ref UserPool 176 | DependsOn: UserPool 177 | UserPoolClient: 178 | # Created provide application access to user pool 179 | # Depends on UserPool for ID reference 180 | Type: "AWS::Cognito::UserPoolClient" 181 | Properties: 182 | ClientName: photoa9240b8c0_app_client 183 | 184 | GenerateSecret: !Ref userpoolClientGenerateSecret 185 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 186 | UserPoolId: !Ref UserPool 187 | DependsOn: UserPool 188 | # BEGIN USER POOL LAMBDA RESOURCES 189 | UserPoolClientRole: 190 | # Created to execute Lambda which gets userpool app client config values 191 | Type: 'AWS::IAM::Role' 192 | Properties: 193 | RoleName: !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',['upClientLambdaRole', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 194 | AssumeRolePolicyDocument: 195 | Version: '2012-10-17' 196 | Statement: 197 | - Effect: Allow 198 | Principal: 199 | Service: 200 | - lambda.amazonaws.com 201 | Action: 202 | - 'sts:AssumeRole' 203 | DependsOn: UserPoolClient 204 | UserPoolClientLambda: 205 | # Lambda which gets userpool app client config values 206 | # Depends on UserPool for id 207 | # Depends on UserPoolClientRole for role ARN 208 | Type: 'AWS::Lambda::Function' 209 | Properties: 210 | Code: 211 | ZipFile: !Join 212 | - |+ 213 | - - 'const response = require(''cfn-response'');' 214 | - 'const aws = require(''aws-sdk'');' 215 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 216 | - 'exports.handler = (event, context, callback) => {' 217 | - ' if (event.RequestType == ''Delete'') { ' 218 | - ' response.send(event, context, response.SUCCESS, {})' 219 | - ' }' 220 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 221 | - ' const params = {' 222 | - ' ClientId: event.ResourceProperties.clientId,' 223 | - ' UserPoolId: event.ResourceProperties.userpoolId' 224 | - ' };' 225 | - ' identity.describeUserPoolClient(params).promise()' 226 | - ' .then((res) => {' 227 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});' 228 | - ' })' 229 | - ' .catch((err) => {' 230 | - ' response.send(event, context, response.FAILED, {err});' 231 | - ' });' 232 | - ' }' 233 | - '};' 234 | Handler: index.handler 235 | Runtime: nodejs8.10 236 | Timeout: '300' 237 | Role: !GetAtt 238 | - UserPoolClientRole 239 | - Arn 240 | DependsOn: UserPoolClientRole 241 | UserPoolClientLambdaPolicy: 242 | # Sets userpool policy for the role that executes the Userpool Client Lambda 243 | # Depends on UserPool for Arn 244 | # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing 245 | Type: 'AWS::IAM::Policy' 246 | Properties: 247 | PolicyName: photoa9240b8c0_userpoolclient_lambda_iam_policy 248 | Roles: 249 | - !Ref UserPoolClientRole 250 | PolicyDocument: 251 | Version: '2012-10-17' 252 | Statement: 253 | - Effect: Allow 254 | Action: 255 | - 'cognito-idp:DescribeUserPoolClient' 256 | Resource: !GetAtt UserPool.Arn 257 | DependsOn: UserPoolClientLambda 258 | UserPoolClientLogPolicy: 259 | # Sets log policy for the role that executes the Userpool Client Lambda 260 | # Depends on UserPool for Arn 261 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 262 | Type: 'AWS::IAM::Policy' 263 | Properties: 264 | PolicyName: photoa9240b8c0_userpoolclient_lambda_log_policy 265 | Roles: 266 | - !Ref UserPoolClientRole 267 | PolicyDocument: 268 | Version: 2012-10-17 269 | Statement: 270 | - Effect: Allow 271 | Action: 272 | - 'logs:CreateLogGroup' 273 | - 'logs:CreateLogStream' 274 | - 'logs:PutLogEvents' 275 | Resource: !Sub 276 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 277 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref UserPoolClientLambda} 278 | DependsOn: UserPoolClientLambdaPolicy 279 | UserPoolClientInputs: 280 | # Values passed to Userpool client Lambda 281 | # Depends on UserPool for Id 282 | # Depends on UserPoolClient for Id 283 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 284 | Type: 'Custom::LambdaCallout' 285 | Properties: 286 | ServiceToken: !GetAtt UserPoolClientLambda.Arn 287 | clientId: !Ref UserPoolClient 288 | userpoolId: !Ref UserPool 289 | DependsOn: UserPoolClientLogPolicy 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | # BEGIN IDENTITY POOL RESOURCES 298 | 299 | 300 | IdentityPool: 301 | # Always created 302 | Type: AWS::Cognito::IdentityPool 303 | Properties: 304 | IdentityPoolName: !If [ShouldNotCreateEnvResources, 'photoalbums9240b8c0_identitypool_9240b8c0', !Join ['',['photoalbums9240b8c0_identitypool_9240b8c0', '__', !Ref env]]] 305 | 306 | CognitoIdentityProviders: 307 | - ClientId: !Ref UserPoolClient 308 | ProviderName: !Sub 309 | - cognito-idp.${region}.amazonaws.com/${client} 310 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 311 | - ClientId: !Ref UserPoolClientWeb 312 | ProviderName: !Sub 313 | - cognito-idp.${region}.amazonaws.com/${client} 314 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 315 | 316 | AllowUnauthenticatedIdentities: !Ref allowUnauthenticatedIdentities 317 | 318 | 319 | DependsOn: UserPoolClientInputs 320 | 321 | 322 | IdentityPoolRoleMap: 323 | # Created to map Auth and Unauth roles to the identity pool 324 | # Depends on Identity Pool for ID ref 325 | Type: AWS::Cognito::IdentityPoolRoleAttachment 326 | Properties: 327 | IdentityPoolId: !Ref IdentityPool 328 | Roles: 329 | unauthenticated: !Ref unauthRoleArn 330 | authenticated: !Ref authRoleArn 331 | DependsOn: IdentityPool 332 | 333 | 334 | Outputs : 335 | 336 | IdentityPoolId: 337 | Value: !Ref 'IdentityPool' 338 | Description: Id for the identity pool 339 | IdentityPoolName: 340 | Value: !GetAtt IdentityPool.Name 341 | 342 | 343 | 344 | 345 | UserPoolId: 346 | Value: !Ref 'UserPool' 347 | Description: Id for the user pool 348 | UserPoolName: 349 | Value: !Ref userPoolName 350 | AppClientIDWeb: 351 | Value: !Ref 'UserPoolClientWeb' 352 | Description: The user pool app client id for web 353 | AppClientID: 354 | Value: !Ref 'UserPoolClient' 355 | Description: The user pool app client id 356 | AppClientSecret: 357 | Value: !GetAtt UserPoolClientInputs.appSecret 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "photoalbums9240b8c0": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [] 7 | } 8 | }, 9 | "api": { 10 | "photoalbums": { 11 | "service": "AppSync", 12 | "providerPlugin": "awscloudformation", 13 | "output": { 14 | "authConfig": { 15 | "additionalAuthenticationProviders": [], 16 | "defaultAuthentication": { 17 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 18 | "userPoolConfig": { 19 | "userPoolId": "authphotoalbums9240b8c0" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | "function": { 27 | "S3Triggerb01fd26e": { 28 | "service": "Lambda", 29 | "providerPlugin": "awscloudformation", 30 | "build": true 31 | } 32 | }, 33 | "storage": { 34 | "photoalbumsstorage": { 35 | "service": "S3", 36 | "providerPlugin": "awscloudformation", 37 | "dependsOn": [ 38 | { 39 | "category": "function", 40 | "resourceName": "S3Triggerb01fd26e", 41 | "attributes": [ 42 | "Name", 43 | "Arn", 44 | "LambdaExecutionRole" 45 | ] 46 | } 47 | ] 48 | } 49 | }, 50 | "hosting": { 51 | "S3AndCloudFront": { 52 | "service": "S3AndCloudFront", 53 | "providerPlugin": "awscloudformation" 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Triggerb01fd26e/S3Triggerb01fd26e-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "env": { 6 | "Type": "String" 7 | } 8 | }, 9 | "Conditions": { 10 | "ShouldNotCreateEnvResources": { 11 | "Fn::Equals": [ 12 | { 13 | "Ref": "env" 14 | }, 15 | "NONE" 16 | ] 17 | } 18 | }, 19 | "Resources": { 20 | "LambdaFunction": { 21 | "Type": "AWS::Lambda::Function", 22 | "Metadata": { 23 | "aws:asset:path": "./src", 24 | "aws:asset:property": "Code" 25 | }, 26 | "Properties": { 27 | "Handler": "index.handler", 28 | "FunctionName": { 29 | "Fn::If": [ 30 | "ShouldNotCreateEnvResources", 31 | "S3Triggerb01fd26e", 32 | { 33 | "Fn::Join": [ 34 | "", 35 | [ 36 | "S3Triggerb01fd26e", 37 | "-", 38 | { 39 | "Ref": "env" 40 | } 41 | ] 42 | ] 43 | } 44 | ] 45 | }, 46 | "Environment": { 47 | "Variables": { 48 | "ENV": { 49 | "Ref": "env" 50 | } 51 | } 52 | }, 53 | "Role": { 54 | "Fn::GetAtt": [ 55 | "LambdaExecutionRole", 56 | "Arn" 57 | ] 58 | }, 59 | "Runtime": "nodejs8.10", 60 | "Timeout": "25", 61 | "Code": { 62 | "S3Bucket": "amplify-photoalbums-dev-193536-deployment", 63 | "S3Key": "amplify-builds/S3Triggerb01fd26e-36706563445347654d67-build.zip" 64 | } 65 | } 66 | }, 67 | "LambdaExecutionRole": { 68 | "Type": "AWS::IAM::Role", 69 | "Properties": { 70 | "RoleName": { 71 | "Fn::If": [ 72 | "ShouldNotCreateEnvResources", 73 | "S3Triggerb01fd26eLambdaRoleb01fd26e", 74 | { 75 | "Fn::Join": [ 76 | "", 77 | [ 78 | "S3Triggerb01fd26eLambdaRoleb01fd26e", 79 | "-", 80 | { 81 | "Ref": "env" 82 | } 83 | ] 84 | ] 85 | } 86 | ] 87 | }, 88 | "AssumeRolePolicyDocument": { 89 | "Version": "2012-10-17", 90 | "Statement": [ 91 | { 92 | "Effect": "Allow", 93 | "Principal": { 94 | "Service": [ 95 | "lambda.amazonaws.com" 96 | ] 97 | }, 98 | "Action": [ 99 | "sts:AssumeRole" 100 | ] 101 | } 102 | ] 103 | } 104 | } 105 | }, 106 | "lambdaexecutionpolicy": { 107 | "DependsOn": [ 108 | "LambdaExecutionRole" 109 | ], 110 | "Type": "AWS::IAM::Policy", 111 | "Properties": { 112 | "PolicyName": "lambda-execution-policy", 113 | "Roles": [ 114 | { 115 | "Ref": "LambdaExecutionRole" 116 | } 117 | ], 118 | "PolicyDocument": { 119 | "Version": "2012-10-17", 120 | "Statement": [ 121 | { 122 | "Effect": "Allow", 123 | "Action": [ 124 | "logs:CreateLogGroup", 125 | "logs:CreateLogStream", 126 | "logs:PutLogEvents" 127 | ], 128 | "Resource": { 129 | "Fn::Sub": [ 130 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 131 | { 132 | "region": { 133 | "Ref": "AWS::Region" 134 | }, 135 | "account": { 136 | "Ref": "AWS::AccountId" 137 | }, 138 | "lambda": { 139 | "Ref": "LambdaFunction" 140 | } 141 | } 142 | ] 143 | } 144 | } 145 | ] 146 | } 147 | } 148 | } 149 | }, 150 | "Outputs": { 151 | "Name": { 152 | "Value": { 153 | "Ref": "LambdaFunction" 154 | } 155 | }, 156 | "Arn": { 157 | "Value": { 158 | "Fn::GetAtt": [ 159 | "LambdaFunction", 160 | "Arn" 161 | ] 162 | } 163 | }, 164 | "Region": { 165 | "Value": { 166 | "Ref": "AWS::Region" 167 | } 168 | }, 169 | "LambdaExecutionRole": { 170 | "Value": { 171 | "Ref": "LambdaExecutionRole" 172 | } 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Triggerb01fd26e/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "value1", 3 | "key2": "value2", 4 | "key3": "value3" 5 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Triggerb01fd26e/src/index.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(event, context) { 2 | //eslint-disable-line 3 | console.log('Received S3 event:', JSON.stringify(event, null, 2)); 4 | // Get the object from the event and show its content type 5 | const bucket = event.Records[0].s3.bucket.name; //eslint-disable-line 6 | const key = event.Records[0].s3.object.key; //eslint-disable-line 7 | console.log(`Bucket: ${bucket}`, `Key: ${key}`); 8 | context.done(null, 'Successfully processed S3 event'); // SUCCESS with message 9 | }; 10 | -------------------------------------------------------------------------------- /amplify/backend/function/S3Triggerb01fd26e/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "S3Triggerb01fd26e", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /amplify/backend/function/S3Triggerb01fd26e/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "S3Triggerb01fd26e", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0" 7 | } 8 | -------------------------------------------------------------------------------- /amplify/backend/hosting/S3AndCloudFront/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucketName": "photoalbums-20191221083147-hostingbucket" 3 | } -------------------------------------------------------------------------------- /amplify/backend/hosting/S3AndCloudFront/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Hosting resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "env": { 6 | "Type": "String" 7 | }, 8 | "bucketName": { 9 | "Type": "String" 10 | } 11 | }, 12 | "Conditions": { 13 | "ShouldNotCreateEnvResources": { 14 | "Fn::Equals": [ 15 | { 16 | "Ref": "env" 17 | }, 18 | "NONE" 19 | ] 20 | } 21 | }, 22 | "Resources": { 23 | "S3Bucket": { 24 | "Type": "AWS::S3::Bucket", 25 | "DeletionPolicy": "Retain", 26 | "Properties": { 27 | "BucketName": { 28 | "Fn::If": [ 29 | "ShouldNotCreateEnvResources", 30 | { 31 | "Ref": "bucketName" 32 | }, 33 | { 34 | "Fn::Join": [ 35 | "", 36 | [ 37 | { 38 | "Ref": "bucketName" 39 | }, 40 | "-", 41 | { 42 | "Ref": "env" 43 | } 44 | ] 45 | ] 46 | } 47 | ] 48 | }, 49 | "AccessControl": "Private", 50 | "WebsiteConfiguration": { 51 | "IndexDocument": "index.html", 52 | "ErrorDocument": "index.html" 53 | }, 54 | "CorsConfiguration": { 55 | "CorsRules": [ 56 | { 57 | "AllowedHeaders": [ 58 | "Authorization", 59 | "Content-Length" 60 | ], 61 | "AllowedMethods": [ 62 | "GET" 63 | ], 64 | "AllowedOrigins": [ 65 | "*" 66 | ], 67 | "MaxAge": 3000 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | }, 74 | "Outputs": { 75 | "Region": { 76 | "Value": { 77 | "Ref": "AWS::Region" 78 | } 79 | }, 80 | "HostingBucketName": { 81 | "Description": "Hosting bucket name", 82 | "Value": { 83 | "Ref": "S3Bucket" 84 | } 85 | }, 86 | "WebsiteURL": { 87 | "Value": { 88 | "Fn::GetAtt": [ 89 | "S3Bucket", 90 | "WebsiteURL" 91 | ] 92 | }, 93 | "Description": "URL for website hosted on S3" 94 | }, 95 | "S3BucketSecureURL": { 96 | "Value": { 97 | "Fn::Join": [ 98 | "", 99 | [ 100 | "https://", 101 | { 102 | "Fn::GetAtt": [ 103 | "S3Bucket", 104 | "DomainName" 105 | ] 106 | } 107 | ] 108 | ] 109 | }, 110 | "Description": "Name of S3 bucket to hold website content" 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /amplify/backend/storage/photoalbumsstorage/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucketName": "photoalbumsc08070c6ed664516be44ae077bcff5b8", 3 | "authPolicyName": "s3_amplify_fc6063de", 4 | "unauthPolicyName": "s3_amplify_fc6063de", 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_9e41d198", 23 | "s3PermissionsAuthenticatedUploads": "s3:PutObject", 24 | "s3UploadsPolicy": "Uploads_policy_9e41d198", 25 | "s3PermissionsAuthenticatedProtected": "s3:PutObject,s3:GetObject,s3:DeleteObject", 26 | "s3ProtectedPolicy": "Protected_policy_51364eb4", 27 | "s3PermissionsAuthenticatedPrivate": "s3:PutObject,s3:GetObject,s3:DeleteObject", 28 | "s3PrivatePolicy": "Private_policy_51364eb4", 29 | "AuthenticatedAllowList": "ALLOW", 30 | "s3ReadPolicy": "read_policy_9e41d198", 31 | "s3PermissionsGuestPublic": "s3:GetObject", 32 | "s3PermissionsGuestUploads": "DISALLOW", 33 | "GuestAllowList": "ALLOW", 34 | "triggerFunction": "S3Triggerb01fd26e" 35 | } -------------------------------------------------------------------------------- /amplify/backend/storage/photoalbumsstorage/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 | "functionS3Triggerb01fd26eName": { 89 | "Type": "String", 90 | "Default": "functionS3Triggerb01fd26eName" 91 | }, 92 | 93 | "functionS3Triggerb01fd26eArn": { 94 | "Type": "String", 95 | "Default": "functionS3Triggerb01fd26eArn" 96 | }, 97 | 98 | "functionS3Triggerb01fd26eLambdaExecutionRole": { 99 | "Type": "String", 100 | "Default": "functionS3Triggerb01fd26eLambdaExecutionRole" 101 | } 102 | 103 | 104 | 105 | 106 | }, 107 | "Conditions": { 108 | "ShouldNotCreateEnvResources": { 109 | "Fn::Equals": [ 110 | { 111 | "Ref": "env" 112 | }, 113 | "NONE" 114 | ] 115 | }, 116 | "CreateAuthPublic": { 117 | "Fn::Not" : [{ 118 | "Fn::Equals" : [ 119 | {"Ref" : "s3PermissionsAuthenticatedPublic"}, 120 | "DISALLOW" 121 | ] 122 | }] 123 | }, 124 | "CreateAuthProtected": { 125 | "Fn::Not" : [{ 126 | "Fn::Equals" : [ 127 | {"Ref" : "s3PermissionsAuthenticatedProtected"}, 128 | "DISALLOW" 129 | ] 130 | }] 131 | }, 132 | "CreateAuthPrivate": { 133 | "Fn::Not" : [{ 134 | "Fn::Equals" : [ 135 | {"Ref" : "s3PermissionsAuthenticatedPrivate"}, 136 | "DISALLOW" 137 | ] 138 | }] 139 | }, 140 | "CreateAuthUploads": { 141 | "Fn::Not" : [{ 142 | "Fn::Equals" : [ 143 | {"Ref" : "s3PermissionsAuthenticatedUploads"}, 144 | "DISALLOW" 145 | ] 146 | }] 147 | }, 148 | "CreateGuestPublic": { 149 | "Fn::Not" : [{ 150 | "Fn::Equals" : [ 151 | {"Ref" : "s3PermissionsGuestPublic"}, 152 | "DISALLOW" 153 | ] 154 | }] 155 | }, 156 | "CreateGuestUploads": { 157 | "Fn::Not" : [{ 158 | "Fn::Equals" : [ 159 | {"Ref" : "s3PermissionsGuestUploads"}, 160 | "DISALLOW" 161 | ] 162 | }] 163 | }, 164 | "AuthReadAndList": { 165 | "Fn::Not" : [{ 166 | "Fn::Equals" : [ 167 | {"Ref" : "AuthenticatedAllowList"}, 168 | "DISALLOW" 169 | ] 170 | }] 171 | }, 172 | "GuestReadAndList": { 173 | "Fn::Not" : [{ 174 | "Fn::Equals" : [ 175 | {"Ref" : "GuestAllowList"}, 176 | "DISALLOW" 177 | ] 178 | }] 179 | } 180 | }, 181 | "Resources": { 182 | "S3Bucket": { 183 | "Type": "AWS::S3::Bucket", 184 | 185 | "DependsOn": [ 186 | "TriggerPermissions" 187 | ], 188 | 189 | "DeletionPolicy" : "Retain", 190 | "Properties": { 191 | "BucketName": { 192 | "Fn::If": [ 193 | "ShouldNotCreateEnvResources", 194 | { 195 | "Ref": "bucketName" 196 | }, 197 | { 198 | "Fn::Join": [ 199 | "", 200 | [ 201 | { 202 | "Ref": "bucketName" 203 | }, 204 | { 205 | "Fn::Select": [ 206 | 3, 207 | { 208 | "Fn::Split": [ 209 | "-", 210 | { 211 | "Ref": "AWS::StackName" 212 | } 213 | ] 214 | } 215 | ] 216 | }, 217 | "-", 218 | { 219 | "Ref": "env" 220 | } 221 | ] 222 | ] 223 | } 224 | ] 225 | }, 226 | 227 | "NotificationConfiguration": { 228 | "LambdaConfigurations": [ 229 | { 230 | "Event": "s3:ObjectCreated:*", 231 | "Function": { 232 | "Ref": "functionS3Triggerb01fd26eArn" 233 | } 234 | }, 235 | { 236 | "Event": "s3:ObjectRemoved:*", 237 | "Function": { 238 | "Ref": "functionS3Triggerb01fd26eArn" 239 | } 240 | } 241 | ] 242 | }, 243 | 244 | "CorsConfiguration": { 245 | "CorsRules": [ 246 | { 247 | "AllowedHeaders": [ 248 | "*" 249 | ], 250 | "AllowedMethods": [ 251 | "GET", 252 | "HEAD", 253 | "PUT", 254 | "POST", 255 | "DELETE" 256 | ], 257 | "AllowedOrigins": [ 258 | "*" 259 | ], 260 | "ExposedHeaders": [ 261 | "x-amz-server-side-encryption", 262 | "x-amz-request-id", 263 | "x-amz-id-2", 264 | "ETag" 265 | ], 266 | "Id": "S3CORSRuleId1", 267 | "MaxAge": "3000" 268 | } 269 | ] 270 | } 271 | } 272 | }, 273 | 274 | 275 | "TriggerPermissions": { 276 | "Type": "AWS::Lambda::Permission", 277 | "Properties": { 278 | "Action": "lambda:InvokeFunction", 279 | "FunctionName": { 280 | "Ref": "functionS3Triggerb01fd26eName" 281 | }, 282 | "Principal": "s3.amazonaws.com", 283 | "SourceAccount": { 284 | "Ref": "AWS::AccountId" 285 | }, 286 | "SourceArn": { 287 | "Fn::Join": [ 288 | "", 289 | [ 290 | "arn:aws:s3:::", 291 | { 292 | "Fn::If": [ 293 | "ShouldNotCreateEnvResources", 294 | { 295 | "Ref": "bucketName" 296 | }, 297 | { 298 | "Fn::Join": [ 299 | "", 300 | [ 301 | { 302 | "Ref": "bucketName" 303 | }, 304 | { 305 | "Fn::Select": [ 306 | 3, 307 | { 308 | "Fn::Split": [ 309 | "-", 310 | { 311 | "Ref": "AWS::StackName" 312 | } 313 | ] 314 | } 315 | ] 316 | }, 317 | "-", 318 | { 319 | "Ref": "env" 320 | } 321 | ] 322 | ] 323 | } 324 | ] 325 | } 326 | ] 327 | ] 328 | } 329 | } 330 | }, 331 | "S3TriggerBucketPolicy": { 332 | "DependsOn": [ 333 | "S3Bucket" 334 | ], 335 | "Type": "AWS::IAM::Policy", 336 | "Properties": { 337 | "PolicyName": "amplify-lambda-execution-policy", 338 | "Roles": [ 339 | { 340 | "Ref": "functionS3Triggerb01fd26eLambdaExecutionRole" 341 | } 342 | ], 343 | "PolicyDocument": { 344 | "Version": "2012-10-17", 345 | "Statement": [ 346 | { 347 | "Effect": "Allow", 348 | "Action": [ 349 | "s3:PutObject", 350 | "s3:GetObject", 351 | "s3:ListBucket", 352 | "s3:DeleteObject" 353 | ], 354 | "Resource": [ 355 | { 356 | "Fn::Join": [ 357 | "", 358 | [ 359 | "arn:aws:s3:::", 360 | { 361 | "Ref": "S3Bucket" 362 | }, 363 | "/*" 364 | ] 365 | ] 366 | } 367 | ] 368 | } 369 | ] 370 | } 371 | } 372 | }, 373 | 374 | "S3AuthPublicPolicy": { 375 | "DependsOn": [ 376 | "S3Bucket" 377 | ], 378 | "Condition": "CreateAuthPublic", 379 | "Type": "AWS::IAM::Policy", 380 | "Properties": { 381 | "PolicyName": { 382 | "Ref": "s3PublicPolicy" 383 | }, 384 | "Roles": [ 385 | { 386 | "Ref": "authRoleName" 387 | } 388 | ], 389 | "PolicyDocument": { 390 | "Version": "2012-10-17", 391 | "Statement": [ 392 | { 393 | "Effect": "Allow", 394 | "Action": { 395 | "Fn::Split" : [ "," , { 396 | "Ref": "s3PermissionsAuthenticatedPublic" 397 | } ] 398 | }, 399 | "Resource": [ 400 | { 401 | "Fn::Join": [ 402 | "", 403 | [ 404 | "arn:aws:s3:::", 405 | { 406 | "Ref": "S3Bucket" 407 | }, 408 | "/public/*" 409 | ] 410 | ] 411 | } 412 | ] 413 | } 414 | ] 415 | } 416 | } 417 | }, 418 | "S3AuthProtectedPolicy": { 419 | "DependsOn": [ 420 | "S3Bucket" 421 | ], 422 | "Condition": "CreateAuthProtected", 423 | "Type": "AWS::IAM::Policy", 424 | "Properties": { 425 | "PolicyName": { 426 | "Ref": "s3ProtectedPolicy" 427 | }, 428 | "Roles": [ 429 | { 430 | "Ref": "authRoleName" 431 | } 432 | ], 433 | "PolicyDocument": { 434 | "Version": "2012-10-17", 435 | "Statement": [ 436 | { 437 | "Effect": "Allow", 438 | "Action": { 439 | "Fn::Split" : [ "," , { 440 | "Ref": "s3PermissionsAuthenticatedProtected" 441 | } ] 442 | }, 443 | "Resource": [ 444 | { 445 | "Fn::Join": [ 446 | "", 447 | [ 448 | "arn:aws:s3:::", 449 | { 450 | "Ref": "S3Bucket" 451 | }, 452 | "/protected/${cognito-identity.amazonaws.com:sub}/*" 453 | ] 454 | ] 455 | } 456 | ] 457 | } 458 | ] 459 | } 460 | } 461 | }, 462 | "S3AuthPrivatePolicy": { 463 | "DependsOn": [ 464 | "S3Bucket" 465 | ], 466 | "Condition": "CreateAuthPrivate", 467 | "Type": "AWS::IAM::Policy", 468 | "Properties": { 469 | "PolicyName": { 470 | "Ref": "s3PrivatePolicy" 471 | }, 472 | "Roles": [ 473 | { 474 | "Ref": "authRoleName" 475 | } 476 | ], 477 | "PolicyDocument": { 478 | "Version": "2012-10-17", 479 | "Statement": [ 480 | { 481 | "Effect": "Allow", 482 | "Action": { 483 | "Fn::Split" : [ "," , { 484 | "Ref": "s3PermissionsAuthenticatedPrivate" 485 | } ] 486 | }, 487 | "Resource": [ 488 | { 489 | "Fn::Join": [ 490 | "", 491 | [ 492 | "arn:aws:s3:::", 493 | { 494 | "Ref": "S3Bucket" 495 | }, 496 | "/private/${cognito-identity.amazonaws.com:sub}/*" 497 | ] 498 | ] 499 | } 500 | ] 501 | } 502 | ] 503 | } 504 | } 505 | }, 506 | "S3AuthUploadPolicy": { 507 | "DependsOn": [ 508 | "S3Bucket" 509 | ], 510 | "Condition": "CreateAuthUploads", 511 | "Type": "AWS::IAM::Policy", 512 | "Properties": { 513 | "PolicyName": { 514 | "Ref": "s3UploadsPolicy" 515 | }, 516 | "Roles": [ 517 | { 518 | "Ref": "authRoleName" 519 | } 520 | ], 521 | "PolicyDocument": { 522 | "Version": "2012-10-17", 523 | "Statement": [ 524 | { 525 | "Effect": "Allow", 526 | "Action": { 527 | "Fn::Split" : [ "," , { 528 | "Ref": "s3PermissionsAuthenticatedUploads" 529 | } ] 530 | }, 531 | "Resource": [ 532 | { 533 | "Fn::Join": [ 534 | "", 535 | [ 536 | "arn:aws:s3:::", 537 | { 538 | "Ref": "S3Bucket" 539 | }, 540 | "/uploads/*" 541 | ] 542 | ] 543 | } 544 | ] 545 | } 546 | ] 547 | } 548 | } 549 | }, 550 | "S3AuthReadPolicy": { 551 | "DependsOn": [ 552 | "S3Bucket" 553 | ], 554 | "Condition": "AuthReadAndList", 555 | "Type": "AWS::IAM::Policy", 556 | "Properties": { 557 | "PolicyName": { 558 | "Ref": "s3ReadPolicy" 559 | }, 560 | "Roles": [ 561 | { 562 | "Ref": "authRoleName" 563 | } 564 | ], 565 | "PolicyDocument": { 566 | "Version": "2012-10-17", 567 | "Statement": [ 568 | { 569 | "Effect": "Allow", 570 | "Action": [ 571 | "s3:GetObject" 572 | ], 573 | "Resource": [ 574 | { 575 | "Fn::Join": [ 576 | "", 577 | [ 578 | "arn:aws:s3:::", 579 | { 580 | "Ref": "S3Bucket" 581 | }, 582 | "/protected/*" 583 | ] 584 | ] 585 | } 586 | ] 587 | }, 588 | { 589 | "Effect": "Allow", 590 | "Action": [ 591 | "s3:ListBucket" 592 | ], 593 | "Resource": [ 594 | { 595 | "Fn::Join": [ 596 | "", 597 | [ 598 | "arn:aws:s3:::", 599 | { 600 | "Ref": "S3Bucket" 601 | } 602 | ] 603 | ] 604 | } 605 | ], 606 | "Condition": { 607 | "StringLike": { 608 | "s3:prefix": [ 609 | "public/", 610 | "public/*", 611 | "protected/", 612 | "protected/*", 613 | "private/${cognito-identity.amazonaws.com:sub}/", 614 | "private/${cognito-identity.amazonaws.com:sub}/*" 615 | ] 616 | } 617 | } 618 | } 619 | ] 620 | } 621 | } 622 | }, 623 | "S3GuestPublicPolicy": { 624 | "DependsOn": [ 625 | "S3Bucket" 626 | ], 627 | "Condition": "CreateGuestPublic", 628 | "Type": "AWS::IAM::Policy", 629 | "Properties": { 630 | "PolicyName": { 631 | "Ref": "s3PublicPolicy" 632 | }, 633 | "Roles": [ 634 | { 635 | "Ref": "unauthRoleName" 636 | } 637 | ], 638 | "PolicyDocument": { 639 | "Version": "2012-10-17", 640 | "Statement": [ 641 | { 642 | "Effect": "Allow", 643 | "Action": { 644 | "Fn::Split" : [ "," , { 645 | "Ref": "s3PermissionsGuestPublic" 646 | } ] 647 | }, 648 | "Resource": [ 649 | { 650 | "Fn::Join": [ 651 | "", 652 | [ 653 | "arn:aws:s3:::", 654 | { 655 | "Ref": "S3Bucket" 656 | }, 657 | "/public/*" 658 | ] 659 | ] 660 | } 661 | ] 662 | } 663 | ] 664 | } 665 | } 666 | }, 667 | "S3GuestUploadPolicy": { 668 | "DependsOn": [ 669 | "S3Bucket" 670 | ], 671 | "Condition": "CreateGuestUploads", 672 | "Type": "AWS::IAM::Policy", 673 | "Properties": { 674 | "PolicyName": { 675 | "Ref": "s3UploadsPolicy" 676 | }, 677 | "Roles": [ 678 | { 679 | "Ref": "unauthRoleName" 680 | } 681 | ], 682 | "PolicyDocument": { 683 | "Version": "2012-10-17", 684 | "Statement": [ 685 | { 686 | "Effect": "Allow", 687 | "Action": { 688 | "Fn::Split" : [ "," , { 689 | "Ref": "s3PermissionsGuestUploads" 690 | } ] 691 | }, 692 | "Resource": [ 693 | { 694 | "Fn::Join": [ 695 | "", 696 | [ 697 | "arn:aws:s3:::", 698 | { 699 | "Ref": "S3Bucket" 700 | }, 701 | "/uploads/*" 702 | ] 703 | ] 704 | } 705 | ] 706 | } 707 | ] 708 | } 709 | } 710 | }, 711 | "S3GuestReadPolicy": { 712 | "DependsOn": [ 713 | "S3Bucket" 714 | ], 715 | "Condition": "GuestReadAndList", 716 | "Type": "AWS::IAM::Policy", 717 | "Properties": { 718 | "PolicyName": { 719 | "Ref": "s3ReadPolicy" 720 | }, 721 | "Roles": [ 722 | { 723 | "Ref": "unauthRoleName" 724 | } 725 | ], 726 | "PolicyDocument": { 727 | "Version": "2012-10-17", 728 | "Statement": [ 729 | { 730 | "Effect": "Allow", 731 | "Action": [ 732 | "s3:GetObject" 733 | ], 734 | "Resource": [ 735 | { 736 | "Fn::Join": [ 737 | "", 738 | [ 739 | "arn:aws:s3:::", 740 | { 741 | "Ref": "S3Bucket" 742 | }, 743 | "/protected/*" 744 | ] 745 | ] 746 | } 747 | ] 748 | }, 749 | { 750 | "Effect": "Allow", 751 | "Action": [ 752 | "s3:ListBucket" 753 | ], 754 | "Resource": [ 755 | { 756 | "Fn::Join": [ 757 | "", 758 | [ 759 | "arn:aws:s3:::", 760 | { 761 | "Ref": "S3Bucket" 762 | } 763 | ] 764 | ] 765 | } 766 | ], 767 | "Condition": { 768 | "StringLike": { 769 | "s3:prefix": [ 770 | "public/", 771 | "public/*", 772 | "protected/", 773 | "protected/*" 774 | ] 775 | } 776 | } 777 | } 778 | ] 779 | } 780 | } 781 | } 782 | }, 783 | "Outputs": { 784 | "BucketName": { 785 | "Value": { 786 | "Ref": "S3Bucket" 787 | }, 788 | "Description": "Bucket name for the S3 bucket" 789 | }, 790 | "Region": { 791 | "Value": { 792 | "Ref": "AWS::Region" 793 | } 794 | } 795 | } 796 | } 797 | -------------------------------------------------------------------------------- /amplify/backend/storage/photoalbumsstorage/storage-params.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-photoalbums-dev-193536-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::534592782540:role/amplify-photoalbums-dev-193536-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::534592782540:role/amplify-photoalbums-dev-193536-authRole", 7 | "Region": "us-east-1", 8 | "DeploymentBucketName": "amplify-photoalbums-dev-193536-deployment", 9 | "UnauthRoleName": "amplify-photoalbums-dev-193536-unauthRole", 10 | "StackName": "amplify-photoalbums-dev-193536", 11 | "StackId": "arn:aws:cloudformation:us-east-1:534592782540:stack/amplify-photoalbums-dev-193536/fe351d70-2285-11ea-ab9d-0ed5acf75591", 12 | "AmplifyAppId": "d3ea1382tw73ek" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "photoalbums9240b8c0": {} 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "photoalbums", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/api": "^2.1.2", 7 | "@aws-amplify/core": "^2.2.1", 8 | "@aws-amplify/datastore": "^1.0.3", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.4.0", 11 | "@testing-library/user-event": "^7.2.1", 12 | "aws-amplify": "^2.2.1", 13 | "aws-amplify-react": "^3.1.2", 14 | "lodash": "^4.17.15", 15 | "react": "^16.12.0", 16 | "react-dom": "^16.12.0", 17 | "react-router-dom": "^5.1.2", 18 | "react-scripts": "3.3.0", 19 | "uuid": "^3.3.3" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakannimer/amplify-react-workshop/17fdf010d86ee1b86b4aaaab2d13d3e13b535d0b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 28 | Photo Albums 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakannimer/amplify-react-workshop/17fdf010d86ee1b86b4aaaab2d13d3e13b535d0b/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakannimer/amplify-react-workshop/17fdf010d86ee1b86b4aaaab2d13d3e13b535d0b/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Grid, 4 | Header, 5 | Input, 6 | List, 7 | Segment, 8 | Form, 9 | Divider 10 | } from "semantic-ui-react"; 11 | import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"; 12 | import sortBy from "lodash/sortBy"; 13 | import { withAuthenticator, S3Image } from "aws-amplify-react"; 14 | import { createAlbum, createPhoto } from "./graphql/mutations"; 15 | import { onCreateAlbum, onCreatePhoto } from "./graphql/subscriptions"; 16 | import { listAlbums, getAlbum, convertImageToText } from "./graphql/queries"; 17 | import API, { graphqlOperation } from "@aws-amplify/api"; 18 | import Amplify, { Auth, Storage } from "aws-amplify"; 19 | import { v4 as uuid } from "uuid"; 20 | 21 | import awsconfig from "./aws-exports"; 22 | 23 | Amplify.Logger.LOG_LEVEL = "DEBUG"; 24 | 25 | Amplify.configure(awsconfig); 26 | 27 | const uploadFile = async (event, albumId, username) => { 28 | const { 29 | target: { value, files } 30 | } = event; 31 | const fileForUpload = files[0]; 32 | const file = fileForUpload || value; 33 | const extension = file.name.split(".")[1]; 34 | const { type: mimeType } = file; 35 | const key = `images/${uuid()}${albumId}.${extension}`; 36 | let s3Obj; 37 | try { 38 | s3Obj = await Storage.put(key, file, { 39 | contentType: mimeType, 40 | metadata: { 41 | owner: username, 42 | albumId 43 | } 44 | }); 45 | console.log("successfully uploaded image!"); 46 | } catch (err) { 47 | console.log("error: ", err); 48 | return; 49 | } 50 | const s3ImageKey = s3Obj.key; 51 | const predictionResult = await API.graphql( 52 | graphqlOperation(convertImageToText, { 53 | input: { 54 | identifyLabels: { 55 | key: s3ImageKey 56 | } 57 | } 58 | }) 59 | ); 60 | const imageLabels = predictionResult.data.convertImageToText; 61 | console.warn({ imageLabels }); 62 | 63 | await API.graphql( 64 | graphqlOperation(createPhoto, { 65 | input: { 66 | bucket: awsconfig.aws_user_files_s3_bucket, 67 | name: key, 68 | createdAt: `${Date.now()}`, 69 | photoAlbumId: albumId, 70 | labels: imageLabels 71 | } 72 | }) 73 | ); 74 | }; 75 | 76 | const S3ImageUpload = ({ albumId }) => { 77 | const { username } = React.useContext(UserContext); 78 | const [isUploading, setIsUploading] = React.useState(false); 79 | const onChange = async event => { 80 | setIsUploading(true); 81 | 82 | let files = []; 83 | for (var i = 0; i < event.target.files.length; i++) { 84 | files.push(event.target.files.item(i)); 85 | } 86 | await Promise.all(files.map(f => uploadFile(event, albumId, username))); 87 | 88 | setIsUploading(false); 89 | }; 90 | return ( 91 |
92 | document.getElementById("add-image-file-input").click()} 94 | disabled={isUploading} 95 | icon="file image outline" 96 | content={isUploading ? "Uploading..." : "Add Images"} 97 | /> 98 | 106 |
107 | ); 108 | }; 109 | 110 | const NewAlbum = () => { 111 | const [albumName, setAlbumName] = React.useState(""); 112 | const handleSubmit = async () => { 113 | console.log(`Creating album ${albumName} `); 114 | await API.graphql( 115 | graphqlOperation(createAlbum, { 116 | input: { 117 | name: albumName, 118 | createdAt: `${Date.now()}` 119 | } 120 | }) 121 | ); 122 | setAlbumName(""); 123 | }; 124 | const handleChange = event => { 125 | setAlbumName(event.target.value); 126 | }; 127 | return ( 128 | 129 |
Add a new album
130 | 140 |
141 | ); 142 | }; 143 | 144 | const AlbumsList = ({ albums = [] }) => { 145 | return ( 146 | 147 |
My Albums
148 | 149 | {sortBy(albums, ["createdAt"]).map(album => ( 150 | 151 | {album.name} 152 | 153 | ))} 154 | 155 |
156 | ); 157 | }; 158 | const AlbumDetailsLoader = ({ id }) => { 159 | const { username } = React.useContext(UserContext); 160 | const [isLoading, setIsLoading] = React.useState(false); 161 | const [album, setAlbum] = React.useState({}); 162 | 163 | React.useEffect(() => { 164 | let isMounted = true; 165 | setIsLoading(true); 166 | API.graphql(graphqlOperation(getAlbum, { id })).then(albumDetails => { 167 | if (!isMounted) return; 168 | setIsLoading(false); 169 | setAlbum(albumDetails.data.getAlbum); 170 | }); 171 | API.graphql(graphqlOperation(onCreatePhoto, { owner: username })).subscribe( 172 | photo => { 173 | const newPhoto = photo.value.data.onCreatePhoto; 174 | setAlbum(alb => { 175 | return { ...alb, photos: { items: [newPhoto, ...alb.photos.items] } }; 176 | }); 177 | } 178 | ); 179 | return () => { 180 | isMounted = false; 181 | }; 182 | }, [id, username]); 183 | 184 | if (isLoading) { 185 | return
Loading...
; 186 | } 187 | return ; 188 | }; 189 | 190 | const AlbumDetails = ({ album }) => { 191 | return ( 192 | 193 |
{album.name}
194 | 195 | 196 |
197 | ); 198 | }; 199 | 200 | const AlbumsListLoader = () => { 201 | const [isLoading, setIsLoading] = React.useState(true); 202 | const [albums, setAlbums] = React.useState([]); 203 | const { username } = React.useContext(UserContext); 204 | React.useEffect(() => { 205 | let isMounted = true; 206 | if (!username) return; 207 | setIsLoading(true); 208 | API.graphql(graphqlOperation(listAlbums)).then(albs => { 209 | if (!isMounted) return; 210 | setAlbums(albs.data.listAlbums.items); 211 | setIsLoading(false); 212 | }); 213 | API.graphql(graphqlOperation(onCreateAlbum, { owner: username })).subscribe( 214 | newAlbum => { 215 | const albumRecord = newAlbum.value.data.onCreateAlbum; 216 | setAlbums(albs => [...albs, albumRecord]); 217 | } 218 | ); 219 | return () => { 220 | isMounted = false; 221 | }; 222 | }, [username]); 223 | if (isLoading) return null; 224 | return ; 225 | }; 226 | 227 | const PhotosList = ({ photos }) => { 228 | return ( 229 |
230 |
249 | ); 250 | }; 251 | 252 | const UserContext = React.createContext({ username: null }); 253 | 254 | const App = () => { 255 | const [user, setUser] = React.useState({ username: null }); 256 | React.useEffect(() => { 257 | Auth.currentAuthenticatedUser().then(user => { 258 | setUser(user); 259 | }); 260 | }, []); 261 | return ( 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ( 272 |
273 | Back to Albums list 274 |
275 | )} 276 | /> 277 | ( 280 | 281 | )} 282 | /> 283 |
284 |
285 |
286 |
287 | ); 288 | }; 289 | 290 | export default withAuthenticator(App, { includeGreetings: true }); 291 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const createAlbum = `mutation CreateAlbum( 5 | $input: CreateAlbumInput! 6 | $condition: ModelAlbumConditionInput 7 | ) { 8 | createAlbum(input: $input, condition: $condition) { 9 | id 10 | name 11 | createdAt 12 | photos { 13 | items { 14 | id 15 | bucket 16 | name 17 | createdAt 18 | labels 19 | owner 20 | } 21 | nextToken 22 | } 23 | owner 24 | } 25 | } 26 | `; 27 | export const updateAlbum = `mutation UpdateAlbum( 28 | $input: UpdateAlbumInput! 29 | $condition: ModelAlbumConditionInput 30 | ) { 31 | updateAlbum(input: $input, condition: $condition) { 32 | id 33 | name 34 | createdAt 35 | photos { 36 | items { 37 | id 38 | bucket 39 | name 40 | createdAt 41 | labels 42 | owner 43 | } 44 | nextToken 45 | } 46 | owner 47 | } 48 | } 49 | `; 50 | export const deleteAlbum = `mutation DeleteAlbum( 51 | $input: DeleteAlbumInput! 52 | $condition: ModelAlbumConditionInput 53 | ) { 54 | deleteAlbum(input: $input, condition: $condition) { 55 | id 56 | name 57 | createdAt 58 | photos { 59 | items { 60 | id 61 | bucket 62 | name 63 | createdAt 64 | labels 65 | owner 66 | } 67 | nextToken 68 | } 69 | owner 70 | } 71 | } 72 | `; 73 | export const createPhoto = `mutation CreatePhoto( 74 | $input: CreatePhotoInput! 75 | $condition: ModelPhotoConditionInput 76 | ) { 77 | createPhoto(input: $input, condition: $condition) { 78 | id 79 | album { 80 | id 81 | name 82 | createdAt 83 | photos { 84 | nextToken 85 | } 86 | owner 87 | } 88 | bucket 89 | name 90 | createdAt 91 | labels 92 | owner 93 | } 94 | } 95 | `; 96 | export const updatePhoto = `mutation UpdatePhoto( 97 | $input: UpdatePhotoInput! 98 | $condition: ModelPhotoConditionInput 99 | ) { 100 | updatePhoto(input: $input, condition: $condition) { 101 | id 102 | album { 103 | id 104 | name 105 | createdAt 106 | photos { 107 | nextToken 108 | } 109 | owner 110 | } 111 | bucket 112 | name 113 | createdAt 114 | labels 115 | owner 116 | } 117 | } 118 | `; 119 | export const deletePhoto = `mutation DeletePhoto( 120 | $input: DeletePhotoInput! 121 | $condition: ModelPhotoConditionInput 122 | ) { 123 | deletePhoto(input: $input, condition: $condition) { 124 | id 125 | album { 126 | id 127 | name 128 | createdAt 129 | photos { 130 | nextToken 131 | } 132 | owner 133 | } 134 | bucket 135 | name 136 | createdAt 137 | labels 138 | owner 139 | } 140 | } 141 | `; 142 | -------------------------------------------------------------------------------- /src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const getAlbum = `query GetAlbum($id: ID!) { 5 | getAlbum(id: $id) { 6 | id 7 | name 8 | createdAt 9 | photos { 10 | items { 11 | id 12 | bucket 13 | name 14 | createdAt 15 | labels 16 | owner 17 | } 18 | nextToken 19 | } 20 | owner 21 | } 22 | } 23 | `; 24 | export const listAlbums = `query ListAlbums( 25 | $filter: ModelAlbumFilterInput 26 | $limit: Int 27 | $nextToken: String 28 | ) { 29 | listAlbums(filter: $filter, limit: $limit, nextToken: $nextToken) { 30 | items { 31 | id 32 | name 33 | createdAt 34 | photos { 35 | nextToken 36 | } 37 | owner 38 | } 39 | nextToken 40 | } 41 | } 42 | `; 43 | export const getPhoto = `query GetPhoto($id: ID!) { 44 | getPhoto(id: $id) { 45 | id 46 | album { 47 | id 48 | name 49 | createdAt 50 | photos { 51 | nextToken 52 | } 53 | owner 54 | } 55 | bucket 56 | name 57 | createdAt 58 | labels 59 | owner 60 | } 61 | } 62 | `; 63 | export const listPhotos = `query ListPhotos( 64 | $filter: ModelPhotoFilterInput 65 | $limit: Int 66 | $nextToken: String 67 | ) { 68 | listPhotos(filter: $filter, limit: $limit, nextToken: $nextToken) { 69 | items { 70 | id 71 | album { 72 | id 73 | name 74 | createdAt 75 | owner 76 | } 77 | bucket 78 | name 79 | createdAt 80 | labels 81 | owner 82 | } 83 | nextToken 84 | } 85 | } 86 | `; 87 | export const convertImageToText = `query ConvertImageToText($input: ConvertImageToTextInput!) { 88 | convertImageToText(input: $input) 89 | } 90 | `; 91 | -------------------------------------------------------------------------------- /src/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" : "getAlbum", 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" : "Album", 37 | "ofType" : null 38 | }, 39 | "isDeprecated" : false, 40 | "deprecationReason" : null 41 | }, { 42 | "name" : "listAlbums", 43 | "description" : null, 44 | "args" : [ { 45 | "name" : "filter", 46 | "description" : null, 47 | "type" : { 48 | "kind" : "INPUT_OBJECT", 49 | "name" : "ModelAlbumFilterInput", 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" : "ModelAlbumConnection", 75 | "ofType" : null 76 | }, 77 | "isDeprecated" : false, 78 | "deprecationReason" : null 79 | }, { 80 | "name" : "getPhoto", 81 | "description" : null, 82 | "args" : [ { 83 | "name" : "id", 84 | "description" : null, 85 | "type" : { 86 | "kind" : "NON_NULL", 87 | "name" : null, 88 | "ofType" : { 89 | "kind" : "SCALAR", 90 | "name" : "ID", 91 | "ofType" : null 92 | } 93 | }, 94 | "defaultValue" : null 95 | } ], 96 | "type" : { 97 | "kind" : "OBJECT", 98 | "name" : "Photo", 99 | "ofType" : null 100 | }, 101 | "isDeprecated" : false, 102 | "deprecationReason" : null 103 | }, { 104 | "name" : "listPhotos", 105 | "description" : null, 106 | "args" : [ { 107 | "name" : "filter", 108 | "description" : null, 109 | "type" : { 110 | "kind" : "INPUT_OBJECT", 111 | "name" : "ModelPhotoFilterInput", 112 | "ofType" : null 113 | }, 114 | "defaultValue" : null 115 | }, { 116 | "name" : "limit", 117 | "description" : null, 118 | "type" : { 119 | "kind" : "SCALAR", 120 | "name" : "Int", 121 | "ofType" : null 122 | }, 123 | "defaultValue" : null 124 | }, { 125 | "name" : "nextToken", 126 | "description" : null, 127 | "type" : { 128 | "kind" : "SCALAR", 129 | "name" : "String", 130 | "ofType" : null 131 | }, 132 | "defaultValue" : null 133 | } ], 134 | "type" : { 135 | "kind" : "OBJECT", 136 | "name" : "ModelPhotoConnection", 137 | "ofType" : null 138 | }, 139 | "isDeprecated" : false, 140 | "deprecationReason" : null 141 | }, { 142 | "name" : "convertImageToText", 143 | "description" : null, 144 | "args" : [ { 145 | "name" : "input", 146 | "description" : null, 147 | "type" : { 148 | "kind" : "NON_NULL", 149 | "name" : null, 150 | "ofType" : { 151 | "kind" : "INPUT_OBJECT", 152 | "name" : "ConvertImageToTextInput", 153 | "ofType" : null 154 | } 155 | }, 156 | "defaultValue" : null 157 | } ], 158 | "type" : { 159 | "kind" : "LIST", 160 | "name" : null, 161 | "ofType" : { 162 | "kind" : "SCALAR", 163 | "name" : "String", 164 | "ofType" : null 165 | } 166 | }, 167 | "isDeprecated" : false, 168 | "deprecationReason" : null 169 | } ], 170 | "inputFields" : null, 171 | "interfaces" : [ ], 172 | "enumValues" : null, 173 | "possibleTypes" : null 174 | }, { 175 | "kind" : "OBJECT", 176 | "name" : "Album", 177 | "description" : null, 178 | "fields" : [ { 179 | "name" : "id", 180 | "description" : null, 181 | "args" : [ ], 182 | "type" : { 183 | "kind" : "NON_NULL", 184 | "name" : null, 185 | "ofType" : { 186 | "kind" : "SCALAR", 187 | "name" : "ID", 188 | "ofType" : null 189 | } 190 | }, 191 | "isDeprecated" : false, 192 | "deprecationReason" : null 193 | }, { 194 | "name" : "name", 195 | "description" : null, 196 | "args" : [ ], 197 | "type" : { 198 | "kind" : "NON_NULL", 199 | "name" : null, 200 | "ofType" : { 201 | "kind" : "SCALAR", 202 | "name" : "String", 203 | "ofType" : null 204 | } 205 | }, 206 | "isDeprecated" : false, 207 | "deprecationReason" : null 208 | }, { 209 | "name" : "createdAt", 210 | "description" : null, 211 | "args" : [ ], 212 | "type" : { 213 | "kind" : "NON_NULL", 214 | "name" : null, 215 | "ofType" : { 216 | "kind" : "SCALAR", 217 | "name" : "String", 218 | "ofType" : null 219 | } 220 | }, 221 | "isDeprecated" : false, 222 | "deprecationReason" : null 223 | }, { 224 | "name" : "photos", 225 | "description" : null, 226 | "args" : [ { 227 | "name" : "filter", 228 | "description" : null, 229 | "type" : { 230 | "kind" : "INPUT_OBJECT", 231 | "name" : "ModelPhotoFilterInput", 232 | "ofType" : null 233 | }, 234 | "defaultValue" : null 235 | }, { 236 | "name" : "sortDirection", 237 | "description" : null, 238 | "type" : { 239 | "kind" : "ENUM", 240 | "name" : "ModelSortDirection", 241 | "ofType" : null 242 | }, 243 | "defaultValue" : null 244 | }, { 245 | "name" : "limit", 246 | "description" : null, 247 | "type" : { 248 | "kind" : "SCALAR", 249 | "name" : "Int", 250 | "ofType" : null 251 | }, 252 | "defaultValue" : null 253 | }, { 254 | "name" : "nextToken", 255 | "description" : null, 256 | "type" : { 257 | "kind" : "SCALAR", 258 | "name" : "String", 259 | "ofType" : null 260 | }, 261 | "defaultValue" : null 262 | } ], 263 | "type" : { 264 | "kind" : "OBJECT", 265 | "name" : "ModelPhotoConnection", 266 | "ofType" : null 267 | }, 268 | "isDeprecated" : false, 269 | "deprecationReason" : null 270 | }, { 271 | "name" : "owner", 272 | "description" : null, 273 | "args" : [ ], 274 | "type" : { 275 | "kind" : "SCALAR", 276 | "name" : "String", 277 | "ofType" : null 278 | }, 279 | "isDeprecated" : false, 280 | "deprecationReason" : null 281 | } ], 282 | "inputFields" : null, 283 | "interfaces" : [ ], 284 | "enumValues" : null, 285 | "possibleTypes" : null 286 | }, { 287 | "kind" : "SCALAR", 288 | "name" : "ID", 289 | "description" : "Built-in ID", 290 | "fields" : null, 291 | "inputFields" : null, 292 | "interfaces" : null, 293 | "enumValues" : null, 294 | "possibleTypes" : null 295 | }, { 296 | "kind" : "SCALAR", 297 | "name" : "String", 298 | "description" : "Built-in String", 299 | "fields" : null, 300 | "inputFields" : null, 301 | "interfaces" : null, 302 | "enumValues" : null, 303 | "possibleTypes" : null 304 | }, { 305 | "kind" : "OBJECT", 306 | "name" : "ModelPhotoConnection", 307 | "description" : null, 308 | "fields" : [ { 309 | "name" : "items", 310 | "description" : null, 311 | "args" : [ ], 312 | "type" : { 313 | "kind" : "LIST", 314 | "name" : null, 315 | "ofType" : { 316 | "kind" : "OBJECT", 317 | "name" : "Photo", 318 | "ofType" : null 319 | } 320 | }, 321 | "isDeprecated" : false, 322 | "deprecationReason" : null 323 | }, { 324 | "name" : "nextToken", 325 | "description" : null, 326 | "args" : [ ], 327 | "type" : { 328 | "kind" : "SCALAR", 329 | "name" : "String", 330 | "ofType" : null 331 | }, 332 | "isDeprecated" : false, 333 | "deprecationReason" : null 334 | } ], 335 | "inputFields" : null, 336 | "interfaces" : [ ], 337 | "enumValues" : null, 338 | "possibleTypes" : null 339 | }, { 340 | "kind" : "OBJECT", 341 | "name" : "Photo", 342 | "description" : null, 343 | "fields" : [ { 344 | "name" : "id", 345 | "description" : null, 346 | "args" : [ ], 347 | "type" : { 348 | "kind" : "NON_NULL", 349 | "name" : null, 350 | "ofType" : { 351 | "kind" : "SCALAR", 352 | "name" : "ID", 353 | "ofType" : null 354 | } 355 | }, 356 | "isDeprecated" : false, 357 | "deprecationReason" : null 358 | }, { 359 | "name" : "album", 360 | "description" : null, 361 | "args" : [ ], 362 | "type" : { 363 | "kind" : "OBJECT", 364 | "name" : "Album", 365 | "ofType" : null 366 | }, 367 | "isDeprecated" : false, 368 | "deprecationReason" : null 369 | }, { 370 | "name" : "bucket", 371 | "description" : null, 372 | "args" : [ ], 373 | "type" : { 374 | "kind" : "NON_NULL", 375 | "name" : null, 376 | "ofType" : { 377 | "kind" : "SCALAR", 378 | "name" : "String", 379 | "ofType" : null 380 | } 381 | }, 382 | "isDeprecated" : false, 383 | "deprecationReason" : null 384 | }, { 385 | "name" : "name", 386 | "description" : null, 387 | "args" : [ ], 388 | "type" : { 389 | "kind" : "NON_NULL", 390 | "name" : null, 391 | "ofType" : { 392 | "kind" : "SCALAR", 393 | "name" : "String", 394 | "ofType" : null 395 | } 396 | }, 397 | "isDeprecated" : false, 398 | "deprecationReason" : null 399 | }, { 400 | "name" : "createdAt", 401 | "description" : null, 402 | "args" : [ ], 403 | "type" : { 404 | "kind" : "NON_NULL", 405 | "name" : null, 406 | "ofType" : { 407 | "kind" : "SCALAR", 408 | "name" : "String", 409 | "ofType" : null 410 | } 411 | }, 412 | "isDeprecated" : false, 413 | "deprecationReason" : null 414 | }, { 415 | "name" : "labels", 416 | "description" : null, 417 | "args" : [ ], 418 | "type" : { 419 | "kind" : "LIST", 420 | "name" : null, 421 | "ofType" : { 422 | "kind" : "SCALAR", 423 | "name" : "String", 424 | "ofType" : null 425 | } 426 | }, 427 | "isDeprecated" : false, 428 | "deprecationReason" : null 429 | }, { 430 | "name" : "owner", 431 | "description" : null, 432 | "args" : [ ], 433 | "type" : { 434 | "kind" : "SCALAR", 435 | "name" : "String", 436 | "ofType" : null 437 | }, 438 | "isDeprecated" : false, 439 | "deprecationReason" : null 440 | } ], 441 | "inputFields" : null, 442 | "interfaces" : [ ], 443 | "enumValues" : null, 444 | "possibleTypes" : null 445 | }, { 446 | "kind" : "INPUT_OBJECT", 447 | "name" : "ModelPhotoFilterInput", 448 | "description" : null, 449 | "fields" : null, 450 | "inputFields" : [ { 451 | "name" : "id", 452 | "description" : null, 453 | "type" : { 454 | "kind" : "INPUT_OBJECT", 455 | "name" : "ModelIDInput", 456 | "ofType" : null 457 | }, 458 | "defaultValue" : null 459 | }, { 460 | "name" : "bucket", 461 | "description" : null, 462 | "type" : { 463 | "kind" : "INPUT_OBJECT", 464 | "name" : "ModelStringInput", 465 | "ofType" : null 466 | }, 467 | "defaultValue" : null 468 | }, { 469 | "name" : "name", 470 | "description" : null, 471 | "type" : { 472 | "kind" : "INPUT_OBJECT", 473 | "name" : "ModelStringInput", 474 | "ofType" : null 475 | }, 476 | "defaultValue" : null 477 | }, { 478 | "name" : "createdAt", 479 | "description" : null, 480 | "type" : { 481 | "kind" : "INPUT_OBJECT", 482 | "name" : "ModelStringInput", 483 | "ofType" : null 484 | }, 485 | "defaultValue" : null 486 | }, { 487 | "name" : "labels", 488 | "description" : null, 489 | "type" : { 490 | "kind" : "INPUT_OBJECT", 491 | "name" : "ModelStringInput", 492 | "ofType" : null 493 | }, 494 | "defaultValue" : null 495 | }, { 496 | "name" : "and", 497 | "description" : null, 498 | "type" : { 499 | "kind" : "LIST", 500 | "name" : null, 501 | "ofType" : { 502 | "kind" : "INPUT_OBJECT", 503 | "name" : "ModelPhotoFilterInput", 504 | "ofType" : null 505 | } 506 | }, 507 | "defaultValue" : null 508 | }, { 509 | "name" : "or", 510 | "description" : null, 511 | "type" : { 512 | "kind" : "LIST", 513 | "name" : null, 514 | "ofType" : { 515 | "kind" : "INPUT_OBJECT", 516 | "name" : "ModelPhotoFilterInput", 517 | "ofType" : null 518 | } 519 | }, 520 | "defaultValue" : null 521 | }, { 522 | "name" : "not", 523 | "description" : null, 524 | "type" : { 525 | "kind" : "INPUT_OBJECT", 526 | "name" : "ModelPhotoFilterInput", 527 | "ofType" : null 528 | }, 529 | "defaultValue" : null 530 | } ], 531 | "interfaces" : null, 532 | "enumValues" : null, 533 | "possibleTypes" : null 534 | }, { 535 | "kind" : "INPUT_OBJECT", 536 | "name" : "ModelIDInput", 537 | "description" : null, 538 | "fields" : null, 539 | "inputFields" : [ { 540 | "name" : "ne", 541 | "description" : null, 542 | "type" : { 543 | "kind" : "SCALAR", 544 | "name" : "ID", 545 | "ofType" : null 546 | }, 547 | "defaultValue" : null 548 | }, { 549 | "name" : "eq", 550 | "description" : null, 551 | "type" : { 552 | "kind" : "SCALAR", 553 | "name" : "ID", 554 | "ofType" : null 555 | }, 556 | "defaultValue" : null 557 | }, { 558 | "name" : "le", 559 | "description" : null, 560 | "type" : { 561 | "kind" : "SCALAR", 562 | "name" : "ID", 563 | "ofType" : null 564 | }, 565 | "defaultValue" : null 566 | }, { 567 | "name" : "lt", 568 | "description" : null, 569 | "type" : { 570 | "kind" : "SCALAR", 571 | "name" : "ID", 572 | "ofType" : null 573 | }, 574 | "defaultValue" : null 575 | }, { 576 | "name" : "ge", 577 | "description" : null, 578 | "type" : { 579 | "kind" : "SCALAR", 580 | "name" : "ID", 581 | "ofType" : null 582 | }, 583 | "defaultValue" : null 584 | }, { 585 | "name" : "gt", 586 | "description" : null, 587 | "type" : { 588 | "kind" : "SCALAR", 589 | "name" : "ID", 590 | "ofType" : null 591 | }, 592 | "defaultValue" : null 593 | }, { 594 | "name" : "contains", 595 | "description" : null, 596 | "type" : { 597 | "kind" : "SCALAR", 598 | "name" : "ID", 599 | "ofType" : null 600 | }, 601 | "defaultValue" : null 602 | }, { 603 | "name" : "notContains", 604 | "description" : null, 605 | "type" : { 606 | "kind" : "SCALAR", 607 | "name" : "ID", 608 | "ofType" : null 609 | }, 610 | "defaultValue" : null 611 | }, { 612 | "name" : "between", 613 | "description" : null, 614 | "type" : { 615 | "kind" : "LIST", 616 | "name" : null, 617 | "ofType" : { 618 | "kind" : "SCALAR", 619 | "name" : "ID", 620 | "ofType" : null 621 | } 622 | }, 623 | "defaultValue" : null 624 | }, { 625 | "name" : "beginsWith", 626 | "description" : null, 627 | "type" : { 628 | "kind" : "SCALAR", 629 | "name" : "ID", 630 | "ofType" : null 631 | }, 632 | "defaultValue" : null 633 | }, { 634 | "name" : "attributeExists", 635 | "description" : null, 636 | "type" : { 637 | "kind" : "SCALAR", 638 | "name" : "Boolean", 639 | "ofType" : null 640 | }, 641 | "defaultValue" : null 642 | }, { 643 | "name" : "attributeType", 644 | "description" : null, 645 | "type" : { 646 | "kind" : "ENUM", 647 | "name" : "ModelAttributeTypes", 648 | "ofType" : null 649 | }, 650 | "defaultValue" : null 651 | }, { 652 | "name" : "size", 653 | "description" : null, 654 | "type" : { 655 | "kind" : "INPUT_OBJECT", 656 | "name" : "ModelSizeInput", 657 | "ofType" : null 658 | }, 659 | "defaultValue" : null 660 | } ], 661 | "interfaces" : null, 662 | "enumValues" : null, 663 | "possibleTypes" : null 664 | }, { 665 | "kind" : "SCALAR", 666 | "name" : "Boolean", 667 | "description" : "Built-in Boolean", 668 | "fields" : null, 669 | "inputFields" : null, 670 | "interfaces" : null, 671 | "enumValues" : null, 672 | "possibleTypes" : null 673 | }, { 674 | "kind" : "ENUM", 675 | "name" : "ModelAttributeTypes", 676 | "description" : null, 677 | "fields" : null, 678 | "inputFields" : null, 679 | "interfaces" : null, 680 | "enumValues" : [ { 681 | "name" : "binary", 682 | "description" : null, 683 | "isDeprecated" : false, 684 | "deprecationReason" : null 685 | }, { 686 | "name" : "binarySet", 687 | "description" : null, 688 | "isDeprecated" : false, 689 | "deprecationReason" : null 690 | }, { 691 | "name" : "bool", 692 | "description" : null, 693 | "isDeprecated" : false, 694 | "deprecationReason" : null 695 | }, { 696 | "name" : "list", 697 | "description" : null, 698 | "isDeprecated" : false, 699 | "deprecationReason" : null 700 | }, { 701 | "name" : "map", 702 | "description" : null, 703 | "isDeprecated" : false, 704 | "deprecationReason" : null 705 | }, { 706 | "name" : "number", 707 | "description" : null, 708 | "isDeprecated" : false, 709 | "deprecationReason" : null 710 | }, { 711 | "name" : "numberSet", 712 | "description" : null, 713 | "isDeprecated" : false, 714 | "deprecationReason" : null 715 | }, { 716 | "name" : "string", 717 | "description" : null, 718 | "isDeprecated" : false, 719 | "deprecationReason" : null 720 | }, { 721 | "name" : "stringSet", 722 | "description" : null, 723 | "isDeprecated" : false, 724 | "deprecationReason" : null 725 | }, { 726 | "name" : "_null", 727 | "description" : null, 728 | "isDeprecated" : false, 729 | "deprecationReason" : null 730 | } ], 731 | "possibleTypes" : null 732 | }, { 733 | "kind" : "INPUT_OBJECT", 734 | "name" : "ModelSizeInput", 735 | "description" : null, 736 | "fields" : null, 737 | "inputFields" : [ { 738 | "name" : "ne", 739 | "description" : null, 740 | "type" : { 741 | "kind" : "SCALAR", 742 | "name" : "Int", 743 | "ofType" : null 744 | }, 745 | "defaultValue" : null 746 | }, { 747 | "name" : "eq", 748 | "description" : null, 749 | "type" : { 750 | "kind" : "SCALAR", 751 | "name" : "Int", 752 | "ofType" : null 753 | }, 754 | "defaultValue" : null 755 | }, { 756 | "name" : "le", 757 | "description" : null, 758 | "type" : { 759 | "kind" : "SCALAR", 760 | "name" : "Int", 761 | "ofType" : null 762 | }, 763 | "defaultValue" : null 764 | }, { 765 | "name" : "lt", 766 | "description" : null, 767 | "type" : { 768 | "kind" : "SCALAR", 769 | "name" : "Int", 770 | "ofType" : null 771 | }, 772 | "defaultValue" : null 773 | }, { 774 | "name" : "ge", 775 | "description" : null, 776 | "type" : { 777 | "kind" : "SCALAR", 778 | "name" : "Int", 779 | "ofType" : null 780 | }, 781 | "defaultValue" : null 782 | }, { 783 | "name" : "gt", 784 | "description" : null, 785 | "type" : { 786 | "kind" : "SCALAR", 787 | "name" : "Int", 788 | "ofType" : null 789 | }, 790 | "defaultValue" : null 791 | }, { 792 | "name" : "between", 793 | "description" : null, 794 | "type" : { 795 | "kind" : "LIST", 796 | "name" : null, 797 | "ofType" : { 798 | "kind" : "SCALAR", 799 | "name" : "Int", 800 | "ofType" : null 801 | } 802 | }, 803 | "defaultValue" : null 804 | } ], 805 | "interfaces" : null, 806 | "enumValues" : null, 807 | "possibleTypes" : null 808 | }, { 809 | "kind" : "SCALAR", 810 | "name" : "Int", 811 | "description" : "Built-in Int", 812 | "fields" : null, 813 | "inputFields" : null, 814 | "interfaces" : null, 815 | "enumValues" : null, 816 | "possibleTypes" : null 817 | }, { 818 | "kind" : "INPUT_OBJECT", 819 | "name" : "ModelStringInput", 820 | "description" : null, 821 | "fields" : null, 822 | "inputFields" : [ { 823 | "name" : "ne", 824 | "description" : null, 825 | "type" : { 826 | "kind" : "SCALAR", 827 | "name" : "String", 828 | "ofType" : null 829 | }, 830 | "defaultValue" : null 831 | }, { 832 | "name" : "eq", 833 | "description" : null, 834 | "type" : { 835 | "kind" : "SCALAR", 836 | "name" : "String", 837 | "ofType" : null 838 | }, 839 | "defaultValue" : null 840 | }, { 841 | "name" : "le", 842 | "description" : null, 843 | "type" : { 844 | "kind" : "SCALAR", 845 | "name" : "String", 846 | "ofType" : null 847 | }, 848 | "defaultValue" : null 849 | }, { 850 | "name" : "lt", 851 | "description" : null, 852 | "type" : { 853 | "kind" : "SCALAR", 854 | "name" : "String", 855 | "ofType" : null 856 | }, 857 | "defaultValue" : null 858 | }, { 859 | "name" : "ge", 860 | "description" : null, 861 | "type" : { 862 | "kind" : "SCALAR", 863 | "name" : "String", 864 | "ofType" : null 865 | }, 866 | "defaultValue" : null 867 | }, { 868 | "name" : "gt", 869 | "description" : null, 870 | "type" : { 871 | "kind" : "SCALAR", 872 | "name" : "String", 873 | "ofType" : null 874 | }, 875 | "defaultValue" : null 876 | }, { 877 | "name" : "contains", 878 | "description" : null, 879 | "type" : { 880 | "kind" : "SCALAR", 881 | "name" : "String", 882 | "ofType" : null 883 | }, 884 | "defaultValue" : null 885 | }, { 886 | "name" : "notContains", 887 | "description" : null, 888 | "type" : { 889 | "kind" : "SCALAR", 890 | "name" : "String", 891 | "ofType" : null 892 | }, 893 | "defaultValue" : null 894 | }, { 895 | "name" : "between", 896 | "description" : null, 897 | "type" : { 898 | "kind" : "LIST", 899 | "name" : null, 900 | "ofType" : { 901 | "kind" : "SCALAR", 902 | "name" : "String", 903 | "ofType" : null 904 | } 905 | }, 906 | "defaultValue" : null 907 | }, { 908 | "name" : "beginsWith", 909 | "description" : null, 910 | "type" : { 911 | "kind" : "SCALAR", 912 | "name" : "String", 913 | "ofType" : null 914 | }, 915 | "defaultValue" : null 916 | }, { 917 | "name" : "attributeExists", 918 | "description" : null, 919 | "type" : { 920 | "kind" : "SCALAR", 921 | "name" : "Boolean", 922 | "ofType" : null 923 | }, 924 | "defaultValue" : null 925 | }, { 926 | "name" : "attributeType", 927 | "description" : null, 928 | "type" : { 929 | "kind" : "ENUM", 930 | "name" : "ModelAttributeTypes", 931 | "ofType" : null 932 | }, 933 | "defaultValue" : null 934 | }, { 935 | "name" : "size", 936 | "description" : null, 937 | "type" : { 938 | "kind" : "INPUT_OBJECT", 939 | "name" : "ModelSizeInput", 940 | "ofType" : null 941 | }, 942 | "defaultValue" : null 943 | } ], 944 | "interfaces" : null, 945 | "enumValues" : null, 946 | "possibleTypes" : null 947 | }, { 948 | "kind" : "ENUM", 949 | "name" : "ModelSortDirection", 950 | "description" : null, 951 | "fields" : null, 952 | "inputFields" : null, 953 | "interfaces" : null, 954 | "enumValues" : [ { 955 | "name" : "ASC", 956 | "description" : null, 957 | "isDeprecated" : false, 958 | "deprecationReason" : null 959 | }, { 960 | "name" : "DESC", 961 | "description" : null, 962 | "isDeprecated" : false, 963 | "deprecationReason" : null 964 | } ], 965 | "possibleTypes" : null 966 | }, { 967 | "kind" : "OBJECT", 968 | "name" : "ModelAlbumConnection", 969 | "description" : null, 970 | "fields" : [ { 971 | "name" : "items", 972 | "description" : null, 973 | "args" : [ ], 974 | "type" : { 975 | "kind" : "LIST", 976 | "name" : null, 977 | "ofType" : { 978 | "kind" : "OBJECT", 979 | "name" : "Album", 980 | "ofType" : null 981 | } 982 | }, 983 | "isDeprecated" : false, 984 | "deprecationReason" : null 985 | }, { 986 | "name" : "nextToken", 987 | "description" : null, 988 | "args" : [ ], 989 | "type" : { 990 | "kind" : "SCALAR", 991 | "name" : "String", 992 | "ofType" : null 993 | }, 994 | "isDeprecated" : false, 995 | "deprecationReason" : null 996 | } ], 997 | "inputFields" : null, 998 | "interfaces" : [ ], 999 | "enumValues" : null, 1000 | "possibleTypes" : null 1001 | }, { 1002 | "kind" : "INPUT_OBJECT", 1003 | "name" : "ModelAlbumFilterInput", 1004 | "description" : null, 1005 | "fields" : null, 1006 | "inputFields" : [ { 1007 | "name" : "id", 1008 | "description" : null, 1009 | "type" : { 1010 | "kind" : "INPUT_OBJECT", 1011 | "name" : "ModelIDInput", 1012 | "ofType" : null 1013 | }, 1014 | "defaultValue" : null 1015 | }, { 1016 | "name" : "name", 1017 | "description" : null, 1018 | "type" : { 1019 | "kind" : "INPUT_OBJECT", 1020 | "name" : "ModelStringInput", 1021 | "ofType" : null 1022 | }, 1023 | "defaultValue" : null 1024 | }, { 1025 | "name" : "createdAt", 1026 | "description" : null, 1027 | "type" : { 1028 | "kind" : "INPUT_OBJECT", 1029 | "name" : "ModelStringInput", 1030 | "ofType" : null 1031 | }, 1032 | "defaultValue" : null 1033 | }, { 1034 | "name" : "and", 1035 | "description" : null, 1036 | "type" : { 1037 | "kind" : "LIST", 1038 | "name" : null, 1039 | "ofType" : { 1040 | "kind" : "INPUT_OBJECT", 1041 | "name" : "ModelAlbumFilterInput", 1042 | "ofType" : null 1043 | } 1044 | }, 1045 | "defaultValue" : null 1046 | }, { 1047 | "name" : "or", 1048 | "description" : null, 1049 | "type" : { 1050 | "kind" : "LIST", 1051 | "name" : null, 1052 | "ofType" : { 1053 | "kind" : "INPUT_OBJECT", 1054 | "name" : "ModelAlbumFilterInput", 1055 | "ofType" : null 1056 | } 1057 | }, 1058 | "defaultValue" : null 1059 | }, { 1060 | "name" : "not", 1061 | "description" : null, 1062 | "type" : { 1063 | "kind" : "INPUT_OBJECT", 1064 | "name" : "ModelAlbumFilterInput", 1065 | "ofType" : null 1066 | }, 1067 | "defaultValue" : null 1068 | } ], 1069 | "interfaces" : null, 1070 | "enumValues" : null, 1071 | "possibleTypes" : null 1072 | }, { 1073 | "kind" : "INPUT_OBJECT", 1074 | "name" : "ConvertImageToTextInput", 1075 | "description" : null, 1076 | "fields" : null, 1077 | "inputFields" : [ { 1078 | "name" : "identifyLabels", 1079 | "description" : null, 1080 | "type" : { 1081 | "kind" : "NON_NULL", 1082 | "name" : null, 1083 | "ofType" : { 1084 | "kind" : "INPUT_OBJECT", 1085 | "name" : "ConvertImageToTextIdentifyLabelsInput", 1086 | "ofType" : null 1087 | } 1088 | }, 1089 | "defaultValue" : null 1090 | } ], 1091 | "interfaces" : null, 1092 | "enumValues" : null, 1093 | "possibleTypes" : null 1094 | }, { 1095 | "kind" : "INPUT_OBJECT", 1096 | "name" : "ConvertImageToTextIdentifyLabelsInput", 1097 | "description" : null, 1098 | "fields" : null, 1099 | "inputFields" : [ { 1100 | "name" : "key", 1101 | "description" : null, 1102 | "type" : { 1103 | "kind" : "NON_NULL", 1104 | "name" : null, 1105 | "ofType" : { 1106 | "kind" : "SCALAR", 1107 | "name" : "String", 1108 | "ofType" : null 1109 | } 1110 | }, 1111 | "defaultValue" : null 1112 | } ], 1113 | "interfaces" : null, 1114 | "enumValues" : null, 1115 | "possibleTypes" : null 1116 | }, { 1117 | "kind" : "OBJECT", 1118 | "name" : "Mutation", 1119 | "description" : null, 1120 | "fields" : [ { 1121 | "name" : "createAlbum", 1122 | "description" : null, 1123 | "args" : [ { 1124 | "name" : "input", 1125 | "description" : null, 1126 | "type" : { 1127 | "kind" : "NON_NULL", 1128 | "name" : null, 1129 | "ofType" : { 1130 | "kind" : "INPUT_OBJECT", 1131 | "name" : "CreateAlbumInput", 1132 | "ofType" : null 1133 | } 1134 | }, 1135 | "defaultValue" : null 1136 | }, { 1137 | "name" : "condition", 1138 | "description" : null, 1139 | "type" : { 1140 | "kind" : "INPUT_OBJECT", 1141 | "name" : "ModelAlbumConditionInput", 1142 | "ofType" : null 1143 | }, 1144 | "defaultValue" : null 1145 | } ], 1146 | "type" : { 1147 | "kind" : "OBJECT", 1148 | "name" : "Album", 1149 | "ofType" : null 1150 | }, 1151 | "isDeprecated" : false, 1152 | "deprecationReason" : null 1153 | }, { 1154 | "name" : "updateAlbum", 1155 | "description" : null, 1156 | "args" : [ { 1157 | "name" : "input", 1158 | "description" : null, 1159 | "type" : { 1160 | "kind" : "NON_NULL", 1161 | "name" : null, 1162 | "ofType" : { 1163 | "kind" : "INPUT_OBJECT", 1164 | "name" : "UpdateAlbumInput", 1165 | "ofType" : null 1166 | } 1167 | }, 1168 | "defaultValue" : null 1169 | }, { 1170 | "name" : "condition", 1171 | "description" : null, 1172 | "type" : { 1173 | "kind" : "INPUT_OBJECT", 1174 | "name" : "ModelAlbumConditionInput", 1175 | "ofType" : null 1176 | }, 1177 | "defaultValue" : null 1178 | } ], 1179 | "type" : { 1180 | "kind" : "OBJECT", 1181 | "name" : "Album", 1182 | "ofType" : null 1183 | }, 1184 | "isDeprecated" : false, 1185 | "deprecationReason" : null 1186 | }, { 1187 | "name" : "deleteAlbum", 1188 | "description" : null, 1189 | "args" : [ { 1190 | "name" : "input", 1191 | "description" : null, 1192 | "type" : { 1193 | "kind" : "NON_NULL", 1194 | "name" : null, 1195 | "ofType" : { 1196 | "kind" : "INPUT_OBJECT", 1197 | "name" : "DeleteAlbumInput", 1198 | "ofType" : null 1199 | } 1200 | }, 1201 | "defaultValue" : null 1202 | }, { 1203 | "name" : "condition", 1204 | "description" : null, 1205 | "type" : { 1206 | "kind" : "INPUT_OBJECT", 1207 | "name" : "ModelAlbumConditionInput", 1208 | "ofType" : null 1209 | }, 1210 | "defaultValue" : null 1211 | } ], 1212 | "type" : { 1213 | "kind" : "OBJECT", 1214 | "name" : "Album", 1215 | "ofType" : null 1216 | }, 1217 | "isDeprecated" : false, 1218 | "deprecationReason" : null 1219 | }, { 1220 | "name" : "createPhoto", 1221 | "description" : null, 1222 | "args" : [ { 1223 | "name" : "input", 1224 | "description" : null, 1225 | "type" : { 1226 | "kind" : "NON_NULL", 1227 | "name" : null, 1228 | "ofType" : { 1229 | "kind" : "INPUT_OBJECT", 1230 | "name" : "CreatePhotoInput", 1231 | "ofType" : null 1232 | } 1233 | }, 1234 | "defaultValue" : null 1235 | }, { 1236 | "name" : "condition", 1237 | "description" : null, 1238 | "type" : { 1239 | "kind" : "INPUT_OBJECT", 1240 | "name" : "ModelPhotoConditionInput", 1241 | "ofType" : null 1242 | }, 1243 | "defaultValue" : null 1244 | } ], 1245 | "type" : { 1246 | "kind" : "OBJECT", 1247 | "name" : "Photo", 1248 | "ofType" : null 1249 | }, 1250 | "isDeprecated" : false, 1251 | "deprecationReason" : null 1252 | }, { 1253 | "name" : "updatePhoto", 1254 | "description" : null, 1255 | "args" : [ { 1256 | "name" : "input", 1257 | "description" : null, 1258 | "type" : { 1259 | "kind" : "NON_NULL", 1260 | "name" : null, 1261 | "ofType" : { 1262 | "kind" : "INPUT_OBJECT", 1263 | "name" : "UpdatePhotoInput", 1264 | "ofType" : null 1265 | } 1266 | }, 1267 | "defaultValue" : null 1268 | }, { 1269 | "name" : "condition", 1270 | "description" : null, 1271 | "type" : { 1272 | "kind" : "INPUT_OBJECT", 1273 | "name" : "ModelPhotoConditionInput", 1274 | "ofType" : null 1275 | }, 1276 | "defaultValue" : null 1277 | } ], 1278 | "type" : { 1279 | "kind" : "OBJECT", 1280 | "name" : "Photo", 1281 | "ofType" : null 1282 | }, 1283 | "isDeprecated" : false, 1284 | "deprecationReason" : null 1285 | }, { 1286 | "name" : "deletePhoto", 1287 | "description" : null, 1288 | "args" : [ { 1289 | "name" : "input", 1290 | "description" : null, 1291 | "type" : { 1292 | "kind" : "NON_NULL", 1293 | "name" : null, 1294 | "ofType" : { 1295 | "kind" : "INPUT_OBJECT", 1296 | "name" : "DeletePhotoInput", 1297 | "ofType" : null 1298 | } 1299 | }, 1300 | "defaultValue" : null 1301 | }, { 1302 | "name" : "condition", 1303 | "description" : null, 1304 | "type" : { 1305 | "kind" : "INPUT_OBJECT", 1306 | "name" : "ModelPhotoConditionInput", 1307 | "ofType" : null 1308 | }, 1309 | "defaultValue" : null 1310 | } ], 1311 | "type" : { 1312 | "kind" : "OBJECT", 1313 | "name" : "Photo", 1314 | "ofType" : null 1315 | }, 1316 | "isDeprecated" : false, 1317 | "deprecationReason" : null 1318 | } ], 1319 | "inputFields" : null, 1320 | "interfaces" : [ ], 1321 | "enumValues" : null, 1322 | "possibleTypes" : null 1323 | }, { 1324 | "kind" : "INPUT_OBJECT", 1325 | "name" : "CreateAlbumInput", 1326 | "description" : null, 1327 | "fields" : null, 1328 | "inputFields" : [ { 1329 | "name" : "id", 1330 | "description" : null, 1331 | "type" : { 1332 | "kind" : "SCALAR", 1333 | "name" : "ID", 1334 | "ofType" : null 1335 | }, 1336 | "defaultValue" : null 1337 | }, { 1338 | "name" : "name", 1339 | "description" : null, 1340 | "type" : { 1341 | "kind" : "NON_NULL", 1342 | "name" : null, 1343 | "ofType" : { 1344 | "kind" : "SCALAR", 1345 | "name" : "String", 1346 | "ofType" : null 1347 | } 1348 | }, 1349 | "defaultValue" : null 1350 | }, { 1351 | "name" : "createdAt", 1352 | "description" : null, 1353 | "type" : { 1354 | "kind" : "NON_NULL", 1355 | "name" : null, 1356 | "ofType" : { 1357 | "kind" : "SCALAR", 1358 | "name" : "String", 1359 | "ofType" : null 1360 | } 1361 | }, 1362 | "defaultValue" : null 1363 | } ], 1364 | "interfaces" : null, 1365 | "enumValues" : null, 1366 | "possibleTypes" : null 1367 | }, { 1368 | "kind" : "INPUT_OBJECT", 1369 | "name" : "ModelAlbumConditionInput", 1370 | "description" : null, 1371 | "fields" : null, 1372 | "inputFields" : [ { 1373 | "name" : "name", 1374 | "description" : null, 1375 | "type" : { 1376 | "kind" : "INPUT_OBJECT", 1377 | "name" : "ModelStringInput", 1378 | "ofType" : null 1379 | }, 1380 | "defaultValue" : null 1381 | }, { 1382 | "name" : "createdAt", 1383 | "description" : null, 1384 | "type" : { 1385 | "kind" : "INPUT_OBJECT", 1386 | "name" : "ModelStringInput", 1387 | "ofType" : null 1388 | }, 1389 | "defaultValue" : null 1390 | }, { 1391 | "name" : "and", 1392 | "description" : null, 1393 | "type" : { 1394 | "kind" : "LIST", 1395 | "name" : null, 1396 | "ofType" : { 1397 | "kind" : "INPUT_OBJECT", 1398 | "name" : "ModelAlbumConditionInput", 1399 | "ofType" : null 1400 | } 1401 | }, 1402 | "defaultValue" : null 1403 | }, { 1404 | "name" : "or", 1405 | "description" : null, 1406 | "type" : { 1407 | "kind" : "LIST", 1408 | "name" : null, 1409 | "ofType" : { 1410 | "kind" : "INPUT_OBJECT", 1411 | "name" : "ModelAlbumConditionInput", 1412 | "ofType" : null 1413 | } 1414 | }, 1415 | "defaultValue" : null 1416 | }, { 1417 | "name" : "not", 1418 | "description" : null, 1419 | "type" : { 1420 | "kind" : "INPUT_OBJECT", 1421 | "name" : "ModelAlbumConditionInput", 1422 | "ofType" : null 1423 | }, 1424 | "defaultValue" : null 1425 | } ], 1426 | "interfaces" : null, 1427 | "enumValues" : null, 1428 | "possibleTypes" : null 1429 | }, { 1430 | "kind" : "INPUT_OBJECT", 1431 | "name" : "UpdateAlbumInput", 1432 | "description" : null, 1433 | "fields" : null, 1434 | "inputFields" : [ { 1435 | "name" : "id", 1436 | "description" : null, 1437 | "type" : { 1438 | "kind" : "NON_NULL", 1439 | "name" : null, 1440 | "ofType" : { 1441 | "kind" : "SCALAR", 1442 | "name" : "ID", 1443 | "ofType" : null 1444 | } 1445 | }, 1446 | "defaultValue" : null 1447 | }, { 1448 | "name" : "name", 1449 | "description" : null, 1450 | "type" : { 1451 | "kind" : "SCALAR", 1452 | "name" : "String", 1453 | "ofType" : null 1454 | }, 1455 | "defaultValue" : null 1456 | }, { 1457 | "name" : "createdAt", 1458 | "description" : null, 1459 | "type" : { 1460 | "kind" : "SCALAR", 1461 | "name" : "String", 1462 | "ofType" : null 1463 | }, 1464 | "defaultValue" : null 1465 | } ], 1466 | "interfaces" : null, 1467 | "enumValues" : null, 1468 | "possibleTypes" : null 1469 | }, { 1470 | "kind" : "INPUT_OBJECT", 1471 | "name" : "DeleteAlbumInput", 1472 | "description" : null, 1473 | "fields" : null, 1474 | "inputFields" : [ { 1475 | "name" : "id", 1476 | "description" : null, 1477 | "type" : { 1478 | "kind" : "SCALAR", 1479 | "name" : "ID", 1480 | "ofType" : null 1481 | }, 1482 | "defaultValue" : null 1483 | } ], 1484 | "interfaces" : null, 1485 | "enumValues" : null, 1486 | "possibleTypes" : null 1487 | }, { 1488 | "kind" : "INPUT_OBJECT", 1489 | "name" : "CreatePhotoInput", 1490 | "description" : null, 1491 | "fields" : null, 1492 | "inputFields" : [ { 1493 | "name" : "id", 1494 | "description" : null, 1495 | "type" : { 1496 | "kind" : "SCALAR", 1497 | "name" : "ID", 1498 | "ofType" : null 1499 | }, 1500 | "defaultValue" : null 1501 | }, { 1502 | "name" : "bucket", 1503 | "description" : null, 1504 | "type" : { 1505 | "kind" : "NON_NULL", 1506 | "name" : null, 1507 | "ofType" : { 1508 | "kind" : "SCALAR", 1509 | "name" : "String", 1510 | "ofType" : null 1511 | } 1512 | }, 1513 | "defaultValue" : null 1514 | }, { 1515 | "name" : "name", 1516 | "description" : null, 1517 | "type" : { 1518 | "kind" : "NON_NULL", 1519 | "name" : null, 1520 | "ofType" : { 1521 | "kind" : "SCALAR", 1522 | "name" : "String", 1523 | "ofType" : null 1524 | } 1525 | }, 1526 | "defaultValue" : null 1527 | }, { 1528 | "name" : "createdAt", 1529 | "description" : null, 1530 | "type" : { 1531 | "kind" : "NON_NULL", 1532 | "name" : null, 1533 | "ofType" : { 1534 | "kind" : "SCALAR", 1535 | "name" : "String", 1536 | "ofType" : null 1537 | } 1538 | }, 1539 | "defaultValue" : null 1540 | }, { 1541 | "name" : "labels", 1542 | "description" : null, 1543 | "type" : { 1544 | "kind" : "LIST", 1545 | "name" : null, 1546 | "ofType" : { 1547 | "kind" : "SCALAR", 1548 | "name" : "String", 1549 | "ofType" : null 1550 | } 1551 | }, 1552 | "defaultValue" : null 1553 | }, { 1554 | "name" : "photoAlbumId", 1555 | "description" : null, 1556 | "type" : { 1557 | "kind" : "SCALAR", 1558 | "name" : "ID", 1559 | "ofType" : null 1560 | }, 1561 | "defaultValue" : null 1562 | } ], 1563 | "interfaces" : null, 1564 | "enumValues" : null, 1565 | "possibleTypes" : null 1566 | }, { 1567 | "kind" : "INPUT_OBJECT", 1568 | "name" : "ModelPhotoConditionInput", 1569 | "description" : null, 1570 | "fields" : null, 1571 | "inputFields" : [ { 1572 | "name" : "bucket", 1573 | "description" : null, 1574 | "type" : { 1575 | "kind" : "INPUT_OBJECT", 1576 | "name" : "ModelStringInput", 1577 | "ofType" : null 1578 | }, 1579 | "defaultValue" : null 1580 | }, { 1581 | "name" : "name", 1582 | "description" : null, 1583 | "type" : { 1584 | "kind" : "INPUT_OBJECT", 1585 | "name" : "ModelStringInput", 1586 | "ofType" : null 1587 | }, 1588 | "defaultValue" : null 1589 | }, { 1590 | "name" : "createdAt", 1591 | "description" : null, 1592 | "type" : { 1593 | "kind" : "INPUT_OBJECT", 1594 | "name" : "ModelStringInput", 1595 | "ofType" : null 1596 | }, 1597 | "defaultValue" : null 1598 | }, { 1599 | "name" : "labels", 1600 | "description" : null, 1601 | "type" : { 1602 | "kind" : "INPUT_OBJECT", 1603 | "name" : "ModelStringInput", 1604 | "ofType" : null 1605 | }, 1606 | "defaultValue" : null 1607 | }, { 1608 | "name" : "and", 1609 | "description" : null, 1610 | "type" : { 1611 | "kind" : "LIST", 1612 | "name" : null, 1613 | "ofType" : { 1614 | "kind" : "INPUT_OBJECT", 1615 | "name" : "ModelPhotoConditionInput", 1616 | "ofType" : null 1617 | } 1618 | }, 1619 | "defaultValue" : null 1620 | }, { 1621 | "name" : "or", 1622 | "description" : null, 1623 | "type" : { 1624 | "kind" : "LIST", 1625 | "name" : null, 1626 | "ofType" : { 1627 | "kind" : "INPUT_OBJECT", 1628 | "name" : "ModelPhotoConditionInput", 1629 | "ofType" : null 1630 | } 1631 | }, 1632 | "defaultValue" : null 1633 | }, { 1634 | "name" : "not", 1635 | "description" : null, 1636 | "type" : { 1637 | "kind" : "INPUT_OBJECT", 1638 | "name" : "ModelPhotoConditionInput", 1639 | "ofType" : null 1640 | }, 1641 | "defaultValue" : null 1642 | } ], 1643 | "interfaces" : null, 1644 | "enumValues" : null, 1645 | "possibleTypes" : null 1646 | }, { 1647 | "kind" : "INPUT_OBJECT", 1648 | "name" : "UpdatePhotoInput", 1649 | "description" : null, 1650 | "fields" : null, 1651 | "inputFields" : [ { 1652 | "name" : "id", 1653 | "description" : null, 1654 | "type" : { 1655 | "kind" : "NON_NULL", 1656 | "name" : null, 1657 | "ofType" : { 1658 | "kind" : "SCALAR", 1659 | "name" : "ID", 1660 | "ofType" : null 1661 | } 1662 | }, 1663 | "defaultValue" : null 1664 | }, { 1665 | "name" : "bucket", 1666 | "description" : null, 1667 | "type" : { 1668 | "kind" : "SCALAR", 1669 | "name" : "String", 1670 | "ofType" : null 1671 | }, 1672 | "defaultValue" : null 1673 | }, { 1674 | "name" : "name", 1675 | "description" : null, 1676 | "type" : { 1677 | "kind" : "SCALAR", 1678 | "name" : "String", 1679 | "ofType" : null 1680 | }, 1681 | "defaultValue" : null 1682 | }, { 1683 | "name" : "createdAt", 1684 | "description" : null, 1685 | "type" : { 1686 | "kind" : "SCALAR", 1687 | "name" : "String", 1688 | "ofType" : null 1689 | }, 1690 | "defaultValue" : null 1691 | }, { 1692 | "name" : "labels", 1693 | "description" : null, 1694 | "type" : { 1695 | "kind" : "LIST", 1696 | "name" : null, 1697 | "ofType" : { 1698 | "kind" : "SCALAR", 1699 | "name" : "String", 1700 | "ofType" : null 1701 | } 1702 | }, 1703 | "defaultValue" : null 1704 | }, { 1705 | "name" : "photoAlbumId", 1706 | "description" : null, 1707 | "type" : { 1708 | "kind" : "SCALAR", 1709 | "name" : "ID", 1710 | "ofType" : null 1711 | }, 1712 | "defaultValue" : null 1713 | } ], 1714 | "interfaces" : null, 1715 | "enumValues" : null, 1716 | "possibleTypes" : null 1717 | }, { 1718 | "kind" : "INPUT_OBJECT", 1719 | "name" : "DeletePhotoInput", 1720 | "description" : null, 1721 | "fields" : null, 1722 | "inputFields" : [ { 1723 | "name" : "id", 1724 | "description" : null, 1725 | "type" : { 1726 | "kind" : "SCALAR", 1727 | "name" : "ID", 1728 | "ofType" : null 1729 | }, 1730 | "defaultValue" : null 1731 | } ], 1732 | "interfaces" : null, 1733 | "enumValues" : null, 1734 | "possibleTypes" : null 1735 | }, { 1736 | "kind" : "OBJECT", 1737 | "name" : "Subscription", 1738 | "description" : null, 1739 | "fields" : [ { 1740 | "name" : "onCreateAlbum", 1741 | "description" : null, 1742 | "args" : [ { 1743 | "name" : "owner", 1744 | "description" : null, 1745 | "type" : { 1746 | "kind" : "NON_NULL", 1747 | "name" : null, 1748 | "ofType" : { 1749 | "kind" : "SCALAR", 1750 | "name" : "String", 1751 | "ofType" : null 1752 | } 1753 | }, 1754 | "defaultValue" : null 1755 | } ], 1756 | "type" : { 1757 | "kind" : "OBJECT", 1758 | "name" : "Album", 1759 | "ofType" : null 1760 | }, 1761 | "isDeprecated" : false, 1762 | "deprecationReason" : null 1763 | }, { 1764 | "name" : "onUpdateAlbum", 1765 | "description" : null, 1766 | "args" : [ { 1767 | "name" : "owner", 1768 | "description" : null, 1769 | "type" : { 1770 | "kind" : "NON_NULL", 1771 | "name" : null, 1772 | "ofType" : { 1773 | "kind" : "SCALAR", 1774 | "name" : "String", 1775 | "ofType" : null 1776 | } 1777 | }, 1778 | "defaultValue" : null 1779 | } ], 1780 | "type" : { 1781 | "kind" : "OBJECT", 1782 | "name" : "Album", 1783 | "ofType" : null 1784 | }, 1785 | "isDeprecated" : false, 1786 | "deprecationReason" : null 1787 | }, { 1788 | "name" : "onDeleteAlbum", 1789 | "description" : null, 1790 | "args" : [ { 1791 | "name" : "owner", 1792 | "description" : null, 1793 | "type" : { 1794 | "kind" : "NON_NULL", 1795 | "name" : null, 1796 | "ofType" : { 1797 | "kind" : "SCALAR", 1798 | "name" : "String", 1799 | "ofType" : null 1800 | } 1801 | }, 1802 | "defaultValue" : null 1803 | } ], 1804 | "type" : { 1805 | "kind" : "OBJECT", 1806 | "name" : "Album", 1807 | "ofType" : null 1808 | }, 1809 | "isDeprecated" : false, 1810 | "deprecationReason" : null 1811 | }, { 1812 | "name" : "onCreatePhoto", 1813 | "description" : null, 1814 | "args" : [ { 1815 | "name" : "owner", 1816 | "description" : null, 1817 | "type" : { 1818 | "kind" : "NON_NULL", 1819 | "name" : null, 1820 | "ofType" : { 1821 | "kind" : "SCALAR", 1822 | "name" : "String", 1823 | "ofType" : null 1824 | } 1825 | }, 1826 | "defaultValue" : null 1827 | } ], 1828 | "type" : { 1829 | "kind" : "OBJECT", 1830 | "name" : "Photo", 1831 | "ofType" : null 1832 | }, 1833 | "isDeprecated" : false, 1834 | "deprecationReason" : null 1835 | }, { 1836 | "name" : "onUpdatePhoto", 1837 | "description" : null, 1838 | "args" : [ { 1839 | "name" : "owner", 1840 | "description" : null, 1841 | "type" : { 1842 | "kind" : "NON_NULL", 1843 | "name" : null, 1844 | "ofType" : { 1845 | "kind" : "SCALAR", 1846 | "name" : "String", 1847 | "ofType" : null 1848 | } 1849 | }, 1850 | "defaultValue" : null 1851 | } ], 1852 | "type" : { 1853 | "kind" : "OBJECT", 1854 | "name" : "Photo", 1855 | "ofType" : null 1856 | }, 1857 | "isDeprecated" : false, 1858 | "deprecationReason" : null 1859 | }, { 1860 | "name" : "onDeletePhoto", 1861 | "description" : null, 1862 | "args" : [ { 1863 | "name" : "owner", 1864 | "description" : null, 1865 | "type" : { 1866 | "kind" : "NON_NULL", 1867 | "name" : null, 1868 | "ofType" : { 1869 | "kind" : "SCALAR", 1870 | "name" : "String", 1871 | "ofType" : null 1872 | } 1873 | }, 1874 | "defaultValue" : null 1875 | } ], 1876 | "type" : { 1877 | "kind" : "OBJECT", 1878 | "name" : "Photo", 1879 | "ofType" : null 1880 | }, 1881 | "isDeprecated" : false, 1882 | "deprecationReason" : null 1883 | } ], 1884 | "inputFields" : null, 1885 | "interfaces" : [ ], 1886 | "enumValues" : null, 1887 | "possibleTypes" : null 1888 | }, { 1889 | "kind" : "INPUT_OBJECT", 1890 | "name" : "ModelBooleanInput", 1891 | "description" : null, 1892 | "fields" : null, 1893 | "inputFields" : [ { 1894 | "name" : "ne", 1895 | "description" : null, 1896 | "type" : { 1897 | "kind" : "SCALAR", 1898 | "name" : "Boolean", 1899 | "ofType" : null 1900 | }, 1901 | "defaultValue" : null 1902 | }, { 1903 | "name" : "eq", 1904 | "description" : null, 1905 | "type" : { 1906 | "kind" : "SCALAR", 1907 | "name" : "Boolean", 1908 | "ofType" : null 1909 | }, 1910 | "defaultValue" : null 1911 | }, { 1912 | "name" : "attributeExists", 1913 | "description" : null, 1914 | "type" : { 1915 | "kind" : "SCALAR", 1916 | "name" : "Boolean", 1917 | "ofType" : null 1918 | }, 1919 | "defaultValue" : null 1920 | }, { 1921 | "name" : "attributeType", 1922 | "description" : null, 1923 | "type" : { 1924 | "kind" : "ENUM", 1925 | "name" : "ModelAttributeTypes", 1926 | "ofType" : null 1927 | }, 1928 | "defaultValue" : null 1929 | } ], 1930 | "interfaces" : null, 1931 | "enumValues" : null, 1932 | "possibleTypes" : null 1933 | }, { 1934 | "kind" : "INPUT_OBJECT", 1935 | "name" : "ModelFloatInput", 1936 | "description" : null, 1937 | "fields" : null, 1938 | "inputFields" : [ { 1939 | "name" : "ne", 1940 | "description" : null, 1941 | "type" : { 1942 | "kind" : "SCALAR", 1943 | "name" : "Float", 1944 | "ofType" : null 1945 | }, 1946 | "defaultValue" : null 1947 | }, { 1948 | "name" : "eq", 1949 | "description" : null, 1950 | "type" : { 1951 | "kind" : "SCALAR", 1952 | "name" : "Float", 1953 | "ofType" : null 1954 | }, 1955 | "defaultValue" : null 1956 | }, { 1957 | "name" : "le", 1958 | "description" : null, 1959 | "type" : { 1960 | "kind" : "SCALAR", 1961 | "name" : "Float", 1962 | "ofType" : null 1963 | }, 1964 | "defaultValue" : null 1965 | }, { 1966 | "name" : "lt", 1967 | "description" : null, 1968 | "type" : { 1969 | "kind" : "SCALAR", 1970 | "name" : "Float", 1971 | "ofType" : null 1972 | }, 1973 | "defaultValue" : null 1974 | }, { 1975 | "name" : "ge", 1976 | "description" : null, 1977 | "type" : { 1978 | "kind" : "SCALAR", 1979 | "name" : "Float", 1980 | "ofType" : null 1981 | }, 1982 | "defaultValue" : null 1983 | }, { 1984 | "name" : "gt", 1985 | "description" : null, 1986 | "type" : { 1987 | "kind" : "SCALAR", 1988 | "name" : "Float", 1989 | "ofType" : null 1990 | }, 1991 | "defaultValue" : null 1992 | }, { 1993 | "name" : "between", 1994 | "description" : null, 1995 | "type" : { 1996 | "kind" : "LIST", 1997 | "name" : null, 1998 | "ofType" : { 1999 | "kind" : "SCALAR", 2000 | "name" : "Float", 2001 | "ofType" : null 2002 | } 2003 | }, 2004 | "defaultValue" : null 2005 | }, { 2006 | "name" : "attributeExists", 2007 | "description" : null, 2008 | "type" : { 2009 | "kind" : "SCALAR", 2010 | "name" : "Boolean", 2011 | "ofType" : null 2012 | }, 2013 | "defaultValue" : null 2014 | }, { 2015 | "name" : "attributeType", 2016 | "description" : null, 2017 | "type" : { 2018 | "kind" : "ENUM", 2019 | "name" : "ModelAttributeTypes", 2020 | "ofType" : null 2021 | }, 2022 | "defaultValue" : null 2023 | } ], 2024 | "interfaces" : null, 2025 | "enumValues" : null, 2026 | "possibleTypes" : null 2027 | }, { 2028 | "kind" : "SCALAR", 2029 | "name" : "Float", 2030 | "description" : "Built-in Float", 2031 | "fields" : null, 2032 | "inputFields" : null, 2033 | "interfaces" : null, 2034 | "enumValues" : null, 2035 | "possibleTypes" : null 2036 | }, { 2037 | "kind" : "INPUT_OBJECT", 2038 | "name" : "ModelIntInput", 2039 | "description" : null, 2040 | "fields" : null, 2041 | "inputFields" : [ { 2042 | "name" : "ne", 2043 | "description" : null, 2044 | "type" : { 2045 | "kind" : "SCALAR", 2046 | "name" : "Int", 2047 | "ofType" : null 2048 | }, 2049 | "defaultValue" : null 2050 | }, { 2051 | "name" : "eq", 2052 | "description" : null, 2053 | "type" : { 2054 | "kind" : "SCALAR", 2055 | "name" : "Int", 2056 | "ofType" : null 2057 | }, 2058 | "defaultValue" : null 2059 | }, { 2060 | "name" : "le", 2061 | "description" : null, 2062 | "type" : { 2063 | "kind" : "SCALAR", 2064 | "name" : "Int", 2065 | "ofType" : null 2066 | }, 2067 | "defaultValue" : null 2068 | }, { 2069 | "name" : "lt", 2070 | "description" : null, 2071 | "type" : { 2072 | "kind" : "SCALAR", 2073 | "name" : "Int", 2074 | "ofType" : null 2075 | }, 2076 | "defaultValue" : null 2077 | }, { 2078 | "name" : "ge", 2079 | "description" : null, 2080 | "type" : { 2081 | "kind" : "SCALAR", 2082 | "name" : "Int", 2083 | "ofType" : null 2084 | }, 2085 | "defaultValue" : null 2086 | }, { 2087 | "name" : "gt", 2088 | "description" : null, 2089 | "type" : { 2090 | "kind" : "SCALAR", 2091 | "name" : "Int", 2092 | "ofType" : null 2093 | }, 2094 | "defaultValue" : null 2095 | }, { 2096 | "name" : "between", 2097 | "description" : null, 2098 | "type" : { 2099 | "kind" : "LIST", 2100 | "name" : null, 2101 | "ofType" : { 2102 | "kind" : "SCALAR", 2103 | "name" : "Int", 2104 | "ofType" : null 2105 | } 2106 | }, 2107 | "defaultValue" : null 2108 | }, { 2109 | "name" : "attributeExists", 2110 | "description" : null, 2111 | "type" : { 2112 | "kind" : "SCALAR", 2113 | "name" : "Boolean", 2114 | "ofType" : null 2115 | }, 2116 | "defaultValue" : null 2117 | }, { 2118 | "name" : "attributeType", 2119 | "description" : null, 2120 | "type" : { 2121 | "kind" : "ENUM", 2122 | "name" : "ModelAttributeTypes", 2123 | "ofType" : null 2124 | }, 2125 | "defaultValue" : null 2126 | } ], 2127 | "interfaces" : null, 2128 | "enumValues" : null, 2129 | "possibleTypes" : null 2130 | }, { 2131 | "kind" : "OBJECT", 2132 | "name" : "__Schema", 2133 | "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.", 2134 | "fields" : [ { 2135 | "name" : "types", 2136 | "description" : "A list of all types supported by this server.", 2137 | "args" : [ ], 2138 | "type" : { 2139 | "kind" : "NON_NULL", 2140 | "name" : null, 2141 | "ofType" : { 2142 | "kind" : "LIST", 2143 | "name" : null, 2144 | "ofType" : { 2145 | "kind" : "NON_NULL", 2146 | "name" : null, 2147 | "ofType" : { 2148 | "kind" : "OBJECT", 2149 | "name" : "__Type", 2150 | "ofType" : null 2151 | } 2152 | } 2153 | } 2154 | }, 2155 | "isDeprecated" : false, 2156 | "deprecationReason" : null 2157 | }, { 2158 | "name" : "queryType", 2159 | "description" : "The type that query operations will be rooted at.", 2160 | "args" : [ ], 2161 | "type" : { 2162 | "kind" : "NON_NULL", 2163 | "name" : null, 2164 | "ofType" : { 2165 | "kind" : "OBJECT", 2166 | "name" : "__Type", 2167 | "ofType" : null 2168 | } 2169 | }, 2170 | "isDeprecated" : false, 2171 | "deprecationReason" : null 2172 | }, { 2173 | "name" : "mutationType", 2174 | "description" : "If this server supports mutation, the type that mutation operations will be rooted at.", 2175 | "args" : [ ], 2176 | "type" : { 2177 | "kind" : "OBJECT", 2178 | "name" : "__Type", 2179 | "ofType" : null 2180 | }, 2181 | "isDeprecated" : false, 2182 | "deprecationReason" : null 2183 | }, { 2184 | "name" : "directives", 2185 | "description" : "'A list of all directives supported by this server.", 2186 | "args" : [ ], 2187 | "type" : { 2188 | "kind" : "NON_NULL", 2189 | "name" : null, 2190 | "ofType" : { 2191 | "kind" : "LIST", 2192 | "name" : null, 2193 | "ofType" : { 2194 | "kind" : "NON_NULL", 2195 | "name" : null, 2196 | "ofType" : { 2197 | "kind" : "OBJECT", 2198 | "name" : "__Directive", 2199 | "ofType" : null 2200 | } 2201 | } 2202 | } 2203 | }, 2204 | "isDeprecated" : false, 2205 | "deprecationReason" : null 2206 | }, { 2207 | "name" : "subscriptionType", 2208 | "description" : "'If this server support subscription, the type that subscription operations will be rooted at.", 2209 | "args" : [ ], 2210 | "type" : { 2211 | "kind" : "OBJECT", 2212 | "name" : "__Type", 2213 | "ofType" : null 2214 | }, 2215 | "isDeprecated" : false, 2216 | "deprecationReason" : null 2217 | } ], 2218 | "inputFields" : null, 2219 | "interfaces" : [ ], 2220 | "enumValues" : null, 2221 | "possibleTypes" : null 2222 | }, { 2223 | "kind" : "OBJECT", 2224 | "name" : "__Type", 2225 | "description" : null, 2226 | "fields" : [ { 2227 | "name" : "kind", 2228 | "description" : null, 2229 | "args" : [ ], 2230 | "type" : { 2231 | "kind" : "NON_NULL", 2232 | "name" : null, 2233 | "ofType" : { 2234 | "kind" : "ENUM", 2235 | "name" : "__TypeKind", 2236 | "ofType" : null 2237 | } 2238 | }, 2239 | "isDeprecated" : false, 2240 | "deprecationReason" : null 2241 | }, { 2242 | "name" : "name", 2243 | "description" : null, 2244 | "args" : [ ], 2245 | "type" : { 2246 | "kind" : "SCALAR", 2247 | "name" : "String", 2248 | "ofType" : null 2249 | }, 2250 | "isDeprecated" : false, 2251 | "deprecationReason" : null 2252 | }, { 2253 | "name" : "description", 2254 | "description" : null, 2255 | "args" : [ ], 2256 | "type" : { 2257 | "kind" : "SCALAR", 2258 | "name" : "String", 2259 | "ofType" : null 2260 | }, 2261 | "isDeprecated" : false, 2262 | "deprecationReason" : null 2263 | }, { 2264 | "name" : "fields", 2265 | "description" : null, 2266 | "args" : [ { 2267 | "name" : "includeDeprecated", 2268 | "description" : null, 2269 | "type" : { 2270 | "kind" : "SCALAR", 2271 | "name" : "Boolean", 2272 | "ofType" : null 2273 | }, 2274 | "defaultValue" : "false" 2275 | } ], 2276 | "type" : { 2277 | "kind" : "LIST", 2278 | "name" : null, 2279 | "ofType" : { 2280 | "kind" : "NON_NULL", 2281 | "name" : null, 2282 | "ofType" : { 2283 | "kind" : "OBJECT", 2284 | "name" : "__Field", 2285 | "ofType" : null 2286 | } 2287 | } 2288 | }, 2289 | "isDeprecated" : false, 2290 | "deprecationReason" : null 2291 | }, { 2292 | "name" : "interfaces", 2293 | "description" : null, 2294 | "args" : [ ], 2295 | "type" : { 2296 | "kind" : "LIST", 2297 | "name" : null, 2298 | "ofType" : { 2299 | "kind" : "NON_NULL", 2300 | "name" : null, 2301 | "ofType" : { 2302 | "kind" : "OBJECT", 2303 | "name" : "__Type", 2304 | "ofType" : null 2305 | } 2306 | } 2307 | }, 2308 | "isDeprecated" : false, 2309 | "deprecationReason" : null 2310 | }, { 2311 | "name" : "possibleTypes", 2312 | "description" : null, 2313 | "args" : [ ], 2314 | "type" : { 2315 | "kind" : "LIST", 2316 | "name" : null, 2317 | "ofType" : { 2318 | "kind" : "NON_NULL", 2319 | "name" : null, 2320 | "ofType" : { 2321 | "kind" : "OBJECT", 2322 | "name" : "__Type", 2323 | "ofType" : null 2324 | } 2325 | } 2326 | }, 2327 | "isDeprecated" : false, 2328 | "deprecationReason" : null 2329 | }, { 2330 | "name" : "enumValues", 2331 | "description" : null, 2332 | "args" : [ { 2333 | "name" : "includeDeprecated", 2334 | "description" : null, 2335 | "type" : { 2336 | "kind" : "SCALAR", 2337 | "name" : "Boolean", 2338 | "ofType" : null 2339 | }, 2340 | "defaultValue" : "false" 2341 | } ], 2342 | "type" : { 2343 | "kind" : "LIST", 2344 | "name" : null, 2345 | "ofType" : { 2346 | "kind" : "NON_NULL", 2347 | "name" : null, 2348 | "ofType" : { 2349 | "kind" : "OBJECT", 2350 | "name" : "__EnumValue", 2351 | "ofType" : null 2352 | } 2353 | } 2354 | }, 2355 | "isDeprecated" : false, 2356 | "deprecationReason" : null 2357 | }, { 2358 | "name" : "inputFields", 2359 | "description" : null, 2360 | "args" : [ ], 2361 | "type" : { 2362 | "kind" : "LIST", 2363 | "name" : null, 2364 | "ofType" : { 2365 | "kind" : "NON_NULL", 2366 | "name" : null, 2367 | "ofType" : { 2368 | "kind" : "OBJECT", 2369 | "name" : "__InputValue", 2370 | "ofType" : null 2371 | } 2372 | } 2373 | }, 2374 | "isDeprecated" : false, 2375 | "deprecationReason" : null 2376 | }, { 2377 | "name" : "ofType", 2378 | "description" : null, 2379 | "args" : [ ], 2380 | "type" : { 2381 | "kind" : "OBJECT", 2382 | "name" : "__Type", 2383 | "ofType" : null 2384 | }, 2385 | "isDeprecated" : false, 2386 | "deprecationReason" : null 2387 | } ], 2388 | "inputFields" : null, 2389 | "interfaces" : [ ], 2390 | "enumValues" : null, 2391 | "possibleTypes" : null 2392 | }, { 2393 | "kind" : "ENUM", 2394 | "name" : "__TypeKind", 2395 | "description" : "An enum describing what kind of type a given __Type is", 2396 | "fields" : null, 2397 | "inputFields" : null, 2398 | "interfaces" : null, 2399 | "enumValues" : [ { 2400 | "name" : "SCALAR", 2401 | "description" : "Indicates this type is a scalar.", 2402 | "isDeprecated" : false, 2403 | "deprecationReason" : null 2404 | }, { 2405 | "name" : "OBJECT", 2406 | "description" : "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 2407 | "isDeprecated" : false, 2408 | "deprecationReason" : null 2409 | }, { 2410 | "name" : "INTERFACE", 2411 | "description" : "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 2412 | "isDeprecated" : false, 2413 | "deprecationReason" : null 2414 | }, { 2415 | "name" : "UNION", 2416 | "description" : "Indicates this type is a union. `possibleTypes` is a valid field.", 2417 | "isDeprecated" : false, 2418 | "deprecationReason" : null 2419 | }, { 2420 | "name" : "ENUM", 2421 | "description" : "Indicates this type is an enum. `enumValues` is a valid field.", 2422 | "isDeprecated" : false, 2423 | "deprecationReason" : null 2424 | }, { 2425 | "name" : "INPUT_OBJECT", 2426 | "description" : "Indicates this type is an input object. `inputFields` is a valid field.", 2427 | "isDeprecated" : false, 2428 | "deprecationReason" : null 2429 | }, { 2430 | "name" : "LIST", 2431 | "description" : "Indicates this type is a list. `ofType` is a valid field.", 2432 | "isDeprecated" : false, 2433 | "deprecationReason" : null 2434 | }, { 2435 | "name" : "NON_NULL", 2436 | "description" : "Indicates this type is a non-null. `ofType` is a valid field.", 2437 | "isDeprecated" : false, 2438 | "deprecationReason" : null 2439 | } ], 2440 | "possibleTypes" : null 2441 | }, { 2442 | "kind" : "OBJECT", 2443 | "name" : "__Field", 2444 | "description" : null, 2445 | "fields" : [ { 2446 | "name" : "name", 2447 | "description" : null, 2448 | "args" : [ ], 2449 | "type" : { 2450 | "kind" : "NON_NULL", 2451 | "name" : null, 2452 | "ofType" : { 2453 | "kind" : "SCALAR", 2454 | "name" : "String", 2455 | "ofType" : null 2456 | } 2457 | }, 2458 | "isDeprecated" : false, 2459 | "deprecationReason" : null 2460 | }, { 2461 | "name" : "description", 2462 | "description" : null, 2463 | "args" : [ ], 2464 | "type" : { 2465 | "kind" : "SCALAR", 2466 | "name" : "String", 2467 | "ofType" : null 2468 | }, 2469 | "isDeprecated" : false, 2470 | "deprecationReason" : null 2471 | }, { 2472 | "name" : "args", 2473 | "description" : null, 2474 | "args" : [ ], 2475 | "type" : { 2476 | "kind" : "NON_NULL", 2477 | "name" : null, 2478 | "ofType" : { 2479 | "kind" : "LIST", 2480 | "name" : null, 2481 | "ofType" : { 2482 | "kind" : "NON_NULL", 2483 | "name" : null, 2484 | "ofType" : { 2485 | "kind" : "OBJECT", 2486 | "name" : "__InputValue", 2487 | "ofType" : null 2488 | } 2489 | } 2490 | } 2491 | }, 2492 | "isDeprecated" : false, 2493 | "deprecationReason" : null 2494 | }, { 2495 | "name" : "type", 2496 | "description" : null, 2497 | "args" : [ ], 2498 | "type" : { 2499 | "kind" : "NON_NULL", 2500 | "name" : null, 2501 | "ofType" : { 2502 | "kind" : "OBJECT", 2503 | "name" : "__Type", 2504 | "ofType" : null 2505 | } 2506 | }, 2507 | "isDeprecated" : false, 2508 | "deprecationReason" : null 2509 | }, { 2510 | "name" : "isDeprecated", 2511 | "description" : null, 2512 | "args" : [ ], 2513 | "type" : { 2514 | "kind" : "NON_NULL", 2515 | "name" : null, 2516 | "ofType" : { 2517 | "kind" : "SCALAR", 2518 | "name" : "Boolean", 2519 | "ofType" : null 2520 | } 2521 | }, 2522 | "isDeprecated" : false, 2523 | "deprecationReason" : null 2524 | }, { 2525 | "name" : "deprecationReason", 2526 | "description" : null, 2527 | "args" : [ ], 2528 | "type" : { 2529 | "kind" : "SCALAR", 2530 | "name" : "String", 2531 | "ofType" : null 2532 | }, 2533 | "isDeprecated" : false, 2534 | "deprecationReason" : null 2535 | } ], 2536 | "inputFields" : null, 2537 | "interfaces" : [ ], 2538 | "enumValues" : null, 2539 | "possibleTypes" : null 2540 | }, { 2541 | "kind" : "OBJECT", 2542 | "name" : "__InputValue", 2543 | "description" : null, 2544 | "fields" : [ { 2545 | "name" : "name", 2546 | "description" : null, 2547 | "args" : [ ], 2548 | "type" : { 2549 | "kind" : "NON_NULL", 2550 | "name" : null, 2551 | "ofType" : { 2552 | "kind" : "SCALAR", 2553 | "name" : "String", 2554 | "ofType" : null 2555 | } 2556 | }, 2557 | "isDeprecated" : false, 2558 | "deprecationReason" : null 2559 | }, { 2560 | "name" : "description", 2561 | "description" : null, 2562 | "args" : [ ], 2563 | "type" : { 2564 | "kind" : "SCALAR", 2565 | "name" : "String", 2566 | "ofType" : null 2567 | }, 2568 | "isDeprecated" : false, 2569 | "deprecationReason" : null 2570 | }, { 2571 | "name" : "type", 2572 | "description" : null, 2573 | "args" : [ ], 2574 | "type" : { 2575 | "kind" : "NON_NULL", 2576 | "name" : null, 2577 | "ofType" : { 2578 | "kind" : "OBJECT", 2579 | "name" : "__Type", 2580 | "ofType" : null 2581 | } 2582 | }, 2583 | "isDeprecated" : false, 2584 | "deprecationReason" : null 2585 | }, { 2586 | "name" : "defaultValue", 2587 | "description" : null, 2588 | "args" : [ ], 2589 | "type" : { 2590 | "kind" : "SCALAR", 2591 | "name" : "String", 2592 | "ofType" : null 2593 | }, 2594 | "isDeprecated" : false, 2595 | "deprecationReason" : null 2596 | } ], 2597 | "inputFields" : null, 2598 | "interfaces" : [ ], 2599 | "enumValues" : null, 2600 | "possibleTypes" : null 2601 | }, { 2602 | "kind" : "OBJECT", 2603 | "name" : "__EnumValue", 2604 | "description" : null, 2605 | "fields" : [ { 2606 | "name" : "name", 2607 | "description" : null, 2608 | "args" : [ ], 2609 | "type" : { 2610 | "kind" : "NON_NULL", 2611 | "name" : null, 2612 | "ofType" : { 2613 | "kind" : "SCALAR", 2614 | "name" : "String", 2615 | "ofType" : null 2616 | } 2617 | }, 2618 | "isDeprecated" : false, 2619 | "deprecationReason" : null 2620 | }, { 2621 | "name" : "description", 2622 | "description" : null, 2623 | "args" : [ ], 2624 | "type" : { 2625 | "kind" : "SCALAR", 2626 | "name" : "String", 2627 | "ofType" : null 2628 | }, 2629 | "isDeprecated" : false, 2630 | "deprecationReason" : null 2631 | }, { 2632 | "name" : "isDeprecated", 2633 | "description" : null, 2634 | "args" : [ ], 2635 | "type" : { 2636 | "kind" : "NON_NULL", 2637 | "name" : null, 2638 | "ofType" : { 2639 | "kind" : "SCALAR", 2640 | "name" : "Boolean", 2641 | "ofType" : null 2642 | } 2643 | }, 2644 | "isDeprecated" : false, 2645 | "deprecationReason" : null 2646 | }, { 2647 | "name" : "deprecationReason", 2648 | "description" : null, 2649 | "args" : [ ], 2650 | "type" : { 2651 | "kind" : "SCALAR", 2652 | "name" : "String", 2653 | "ofType" : null 2654 | }, 2655 | "isDeprecated" : false, 2656 | "deprecationReason" : null 2657 | } ], 2658 | "inputFields" : null, 2659 | "interfaces" : [ ], 2660 | "enumValues" : null, 2661 | "possibleTypes" : null 2662 | }, { 2663 | "kind" : "OBJECT", 2664 | "name" : "__Directive", 2665 | "description" : null, 2666 | "fields" : [ { 2667 | "name" : "name", 2668 | "description" : null, 2669 | "args" : [ ], 2670 | "type" : { 2671 | "kind" : "SCALAR", 2672 | "name" : "String", 2673 | "ofType" : null 2674 | }, 2675 | "isDeprecated" : false, 2676 | "deprecationReason" : null 2677 | }, { 2678 | "name" : "description", 2679 | "description" : null, 2680 | "args" : [ ], 2681 | "type" : { 2682 | "kind" : "SCALAR", 2683 | "name" : "String", 2684 | "ofType" : null 2685 | }, 2686 | "isDeprecated" : false, 2687 | "deprecationReason" : null 2688 | }, { 2689 | "name" : "locations", 2690 | "description" : null, 2691 | "args" : [ ], 2692 | "type" : { 2693 | "kind" : "LIST", 2694 | "name" : null, 2695 | "ofType" : { 2696 | "kind" : "NON_NULL", 2697 | "name" : null, 2698 | "ofType" : { 2699 | "kind" : "ENUM", 2700 | "name" : "__DirectiveLocation", 2701 | "ofType" : null 2702 | } 2703 | } 2704 | }, 2705 | "isDeprecated" : false, 2706 | "deprecationReason" : null 2707 | }, { 2708 | "name" : "args", 2709 | "description" : null, 2710 | "args" : [ ], 2711 | "type" : { 2712 | "kind" : "NON_NULL", 2713 | "name" : null, 2714 | "ofType" : { 2715 | "kind" : "LIST", 2716 | "name" : null, 2717 | "ofType" : { 2718 | "kind" : "NON_NULL", 2719 | "name" : null, 2720 | "ofType" : { 2721 | "kind" : "OBJECT", 2722 | "name" : "__InputValue", 2723 | "ofType" : null 2724 | } 2725 | } 2726 | } 2727 | }, 2728 | "isDeprecated" : false, 2729 | "deprecationReason" : null 2730 | }, { 2731 | "name" : "onOperation", 2732 | "description" : null, 2733 | "args" : [ ], 2734 | "type" : { 2735 | "kind" : "SCALAR", 2736 | "name" : "Boolean", 2737 | "ofType" : null 2738 | }, 2739 | "isDeprecated" : true, 2740 | "deprecationReason" : "Use `locations`." 2741 | }, { 2742 | "name" : "onFragment", 2743 | "description" : null, 2744 | "args" : [ ], 2745 | "type" : { 2746 | "kind" : "SCALAR", 2747 | "name" : "Boolean", 2748 | "ofType" : null 2749 | }, 2750 | "isDeprecated" : true, 2751 | "deprecationReason" : "Use `locations`." 2752 | }, { 2753 | "name" : "onField", 2754 | "description" : null, 2755 | "args" : [ ], 2756 | "type" : { 2757 | "kind" : "SCALAR", 2758 | "name" : "Boolean", 2759 | "ofType" : null 2760 | }, 2761 | "isDeprecated" : true, 2762 | "deprecationReason" : "Use `locations`." 2763 | } ], 2764 | "inputFields" : null, 2765 | "interfaces" : [ ], 2766 | "enumValues" : null, 2767 | "possibleTypes" : null 2768 | }, { 2769 | "kind" : "ENUM", 2770 | "name" : "__DirectiveLocation", 2771 | "description" : "An enum describing valid locations where a directive can be placed", 2772 | "fields" : null, 2773 | "inputFields" : null, 2774 | "interfaces" : null, 2775 | "enumValues" : [ { 2776 | "name" : "QUERY", 2777 | "description" : "Indicates the directive is valid on queries.", 2778 | "isDeprecated" : false, 2779 | "deprecationReason" : null 2780 | }, { 2781 | "name" : "MUTATION", 2782 | "description" : "Indicates the directive is valid on mutations.", 2783 | "isDeprecated" : false, 2784 | "deprecationReason" : null 2785 | }, { 2786 | "name" : "FIELD", 2787 | "description" : "Indicates the directive is valid on fields.", 2788 | "isDeprecated" : false, 2789 | "deprecationReason" : null 2790 | }, { 2791 | "name" : "FRAGMENT_DEFINITION", 2792 | "description" : "Indicates the directive is valid on fragment definitions.", 2793 | "isDeprecated" : false, 2794 | "deprecationReason" : null 2795 | }, { 2796 | "name" : "FRAGMENT_SPREAD", 2797 | "description" : "Indicates the directive is valid on fragment spreads.", 2798 | "isDeprecated" : false, 2799 | "deprecationReason" : null 2800 | }, { 2801 | "name" : "INLINE_FRAGMENT", 2802 | "description" : "Indicates the directive is valid on inline fragments.", 2803 | "isDeprecated" : false, 2804 | "deprecationReason" : null 2805 | }, { 2806 | "name" : "SCHEMA", 2807 | "description" : "Indicates the directive is valid on a schema SDL definition.", 2808 | "isDeprecated" : false, 2809 | "deprecationReason" : null 2810 | }, { 2811 | "name" : "SCALAR", 2812 | "description" : "Indicates the directive is valid on a scalar SDL definition.", 2813 | "isDeprecated" : false, 2814 | "deprecationReason" : null 2815 | }, { 2816 | "name" : "OBJECT", 2817 | "description" : "Indicates the directive is valid on an object SDL definition.", 2818 | "isDeprecated" : false, 2819 | "deprecationReason" : null 2820 | }, { 2821 | "name" : "FIELD_DEFINITION", 2822 | "description" : "Indicates the directive is valid on a field SDL definition.", 2823 | "isDeprecated" : false, 2824 | "deprecationReason" : null 2825 | }, { 2826 | "name" : "ARGUMENT_DEFINITION", 2827 | "description" : "Indicates the directive is valid on a field argument SDL definition.", 2828 | "isDeprecated" : false, 2829 | "deprecationReason" : null 2830 | }, { 2831 | "name" : "INTERFACE", 2832 | "description" : "Indicates the directive is valid on an interface SDL definition.", 2833 | "isDeprecated" : false, 2834 | "deprecationReason" : null 2835 | }, { 2836 | "name" : "UNION", 2837 | "description" : "Indicates the directive is valid on an union SDL definition.", 2838 | "isDeprecated" : false, 2839 | "deprecationReason" : null 2840 | }, { 2841 | "name" : "ENUM", 2842 | "description" : "Indicates the directive is valid on an enum SDL definition.", 2843 | "isDeprecated" : false, 2844 | "deprecationReason" : null 2845 | }, { 2846 | "name" : "ENUM_VALUE", 2847 | "description" : "Indicates the directive is valid on an enum value SDL definition.", 2848 | "isDeprecated" : false, 2849 | "deprecationReason" : null 2850 | }, { 2851 | "name" : "INPUT_OBJECT", 2852 | "description" : "Indicates the directive is valid on an input object SDL definition.", 2853 | "isDeprecated" : false, 2854 | "deprecationReason" : null 2855 | }, { 2856 | "name" : "INPUT_FIELD_DEFINITION", 2857 | "description" : "Indicates the directive is valid on an input object field SDL definition.", 2858 | "isDeprecated" : false, 2859 | "deprecationReason" : null 2860 | } ], 2861 | "possibleTypes" : null 2862 | } ], 2863 | "directives" : [ { 2864 | "name" : "include", 2865 | "description" : "Directs the executor to include this field or fragment only when the `if` argument is true", 2866 | "locations" : [ "FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT" ], 2867 | "args" : [ { 2868 | "name" : "if", 2869 | "description" : "Included when true.", 2870 | "type" : { 2871 | "kind" : "NON_NULL", 2872 | "name" : null, 2873 | "ofType" : { 2874 | "kind" : "SCALAR", 2875 | "name" : "Boolean", 2876 | "ofType" : null 2877 | } 2878 | }, 2879 | "defaultValue" : null 2880 | } ], 2881 | "onOperation" : false, 2882 | "onFragment" : true, 2883 | "onField" : true 2884 | }, { 2885 | "name" : "skip", 2886 | "description" : "Directs the executor to skip this field or fragment when the `if`'argument is true.", 2887 | "locations" : [ "FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT" ], 2888 | "args" : [ { 2889 | "name" : "if", 2890 | "description" : "Skipped when true.", 2891 | "type" : { 2892 | "kind" : "NON_NULL", 2893 | "name" : null, 2894 | "ofType" : { 2895 | "kind" : "SCALAR", 2896 | "name" : "Boolean", 2897 | "ofType" : null 2898 | } 2899 | }, 2900 | "defaultValue" : null 2901 | } ], 2902 | "onOperation" : false, 2903 | "onFragment" : true, 2904 | "onField" : true 2905 | }, { 2906 | "name" : "defer", 2907 | "description" : "This directive allows results to be deferred during execution", 2908 | "locations" : [ "FIELD" ], 2909 | "args" : [ ], 2910 | "onOperation" : false, 2911 | "onFragment" : false, 2912 | "onField" : true 2913 | }, { 2914 | "name" : "aws_cognito_user_pools", 2915 | "description" : "Tells the service this field/object has access authorized by a Cognito User Pools token.", 2916 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2917 | "args" : [ { 2918 | "name" : "cognito_groups", 2919 | "description" : "List of cognito user pool groups which have access on this field", 2920 | "type" : { 2921 | "kind" : "LIST", 2922 | "name" : null, 2923 | "ofType" : { 2924 | "kind" : "SCALAR", 2925 | "name" : "String", 2926 | "ofType" : null 2927 | } 2928 | }, 2929 | "defaultValue" : null 2930 | } ], 2931 | "onOperation" : false, 2932 | "onFragment" : false, 2933 | "onField" : false 2934 | }, { 2935 | "name" : "aws_oidc", 2936 | "description" : "Tells the service this field/object has access authorized by an OIDC token.", 2937 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2938 | "args" : [ ], 2939 | "onOperation" : false, 2940 | "onFragment" : false, 2941 | "onField" : false 2942 | }, { 2943 | "name" : "aws_api_key", 2944 | "description" : "Tells the service this field/object has access authorized by an API key.", 2945 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2946 | "args" : [ ], 2947 | "onOperation" : false, 2948 | "onFragment" : false, 2949 | "onField" : false 2950 | }, { 2951 | "name" : "aws_auth", 2952 | "description" : "Directs the schema to enforce authorization on a field", 2953 | "locations" : [ "FIELD_DEFINITION" ], 2954 | "args" : [ { 2955 | "name" : "cognito_groups", 2956 | "description" : "List of cognito user pool groups which have access on this field", 2957 | "type" : { 2958 | "kind" : "LIST", 2959 | "name" : null, 2960 | "ofType" : { 2961 | "kind" : "SCALAR", 2962 | "name" : "String", 2963 | "ofType" : null 2964 | } 2965 | }, 2966 | "defaultValue" : null 2967 | } ], 2968 | "onOperation" : false, 2969 | "onFragment" : false, 2970 | "onField" : false 2971 | }, { 2972 | "name" : "aws_publish", 2973 | "description" : "Tells the service which subscriptions will be published to when this mutation is called. This directive is deprecated use @aws_susbscribe directive instead.", 2974 | "locations" : [ "FIELD_DEFINITION" ], 2975 | "args" : [ { 2976 | "name" : "subscriptions", 2977 | "description" : "List of subscriptions which will be published to when this mutation is called.", 2978 | "type" : { 2979 | "kind" : "LIST", 2980 | "name" : null, 2981 | "ofType" : { 2982 | "kind" : "SCALAR", 2983 | "name" : "String", 2984 | "ofType" : null 2985 | } 2986 | }, 2987 | "defaultValue" : null 2988 | } ], 2989 | "onOperation" : false, 2990 | "onFragment" : false, 2991 | "onField" : false 2992 | }, { 2993 | "name" : "aws_iam", 2994 | "description" : "Tells the service this field/object has access authorized by sigv4 signing.", 2995 | "locations" : [ "OBJECT", "FIELD_DEFINITION" ], 2996 | "args" : [ ], 2997 | "onOperation" : false, 2998 | "onFragment" : false, 2999 | "onField" : false 3000 | }, { 3001 | "name" : "aws_subscribe", 3002 | "description" : "Tells the service which mutation triggers this subscription.", 3003 | "locations" : [ "FIELD_DEFINITION" ], 3004 | "args" : [ { 3005 | "name" : "mutations", 3006 | "description" : "List of mutations which will trigger this subscription when they are called.", 3007 | "type" : { 3008 | "kind" : "LIST", 3009 | "name" : null, 3010 | "ofType" : { 3011 | "kind" : "SCALAR", 3012 | "name" : "String", 3013 | "ofType" : null 3014 | } 3015 | }, 3016 | "defaultValue" : null 3017 | } ], 3018 | "onOperation" : false, 3019 | "onFragment" : false, 3020 | "onField" : false 3021 | }, { 3022 | "name" : "deprecated", 3023 | "description" : null, 3024 | "locations" : [ "FIELD_DEFINITION", "ENUM_VALUE" ], 3025 | "args" : [ { 3026 | "name" : "reason", 3027 | "description" : null, 3028 | "type" : { 3029 | "kind" : "SCALAR", 3030 | "name" : "String", 3031 | "ofType" : null 3032 | }, 3033 | "defaultValue" : "\"No longer supported\"" 3034 | } ], 3035 | "onOperation" : false, 3036 | "onFragment" : false, 3037 | "onField" : false 3038 | } ] 3039 | } 3040 | } 3041 | } -------------------------------------------------------------------------------- /src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const onCreateAlbum = `subscription OnCreateAlbum($owner: String!) { 5 | onCreateAlbum(owner: $owner) { 6 | id 7 | name 8 | createdAt 9 | photos { 10 | items { 11 | id 12 | bucket 13 | name 14 | createdAt 15 | labels 16 | owner 17 | } 18 | nextToken 19 | } 20 | owner 21 | } 22 | } 23 | `; 24 | export const onUpdateAlbum = `subscription OnUpdateAlbum($owner: String!) { 25 | onUpdateAlbum(owner: $owner) { 26 | id 27 | name 28 | createdAt 29 | photos { 30 | items { 31 | id 32 | bucket 33 | name 34 | createdAt 35 | labels 36 | owner 37 | } 38 | nextToken 39 | } 40 | owner 41 | } 42 | } 43 | `; 44 | export const onDeleteAlbum = `subscription OnDeleteAlbum($owner: String!) { 45 | onDeleteAlbum(owner: $owner) { 46 | id 47 | name 48 | createdAt 49 | photos { 50 | items { 51 | id 52 | bucket 53 | name 54 | createdAt 55 | labels 56 | owner 57 | } 58 | nextToken 59 | } 60 | owner 61 | } 62 | } 63 | `; 64 | export const onCreatePhoto = `subscription OnCreatePhoto($owner: String!) { 65 | onCreatePhoto(owner: $owner) { 66 | id 67 | album { 68 | id 69 | name 70 | createdAt 71 | photos { 72 | nextToken 73 | } 74 | owner 75 | } 76 | bucket 77 | name 78 | createdAt 79 | labels 80 | owner 81 | } 82 | } 83 | `; 84 | export const onUpdatePhoto = `subscription OnUpdatePhoto($owner: String!) { 85 | onUpdatePhoto(owner: $owner) { 86 | id 87 | album { 88 | id 89 | name 90 | createdAt 91 | photos { 92 | nextToken 93 | } 94 | owner 95 | } 96 | bucket 97 | name 98 | createdAt 99 | labels 100 | owner 101 | } 102 | } 103 | `; 104 | export const onDeletePhoto = `subscription OnDeletePhoto($owner: String!) { 105 | onDeletePhoto(owner: $owner) { 106 | id 107 | album { 108 | id 109 | name 110 | createdAt 111 | photos { 112 | nextToken 113 | } 114 | owner 115 | } 116 | bucket 117 | name 118 | createdAt 119 | labels 120 | owner 121 | } 122 | } 123 | `; 124 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | --------------------------------------------------------------------------------