Update with-aws-amplify-typescript example (#24292)

- Update the Readme to use the latest Amplify CLI versions' prompts
- Update the example to use the [SSR features of AWS Amplify ](https://aws.amazon.com/blogs/mobile/ssr-support-for-aws-amplify-javascript-libraries/)
- Update to use Next version `10` instead of `9`
- Correctly use the types produced by the Amplify CLI in `API.ts`
- Add `amplify auth`  and auth rules to the GraphQL model as suggested by the Amplify CLI.
This commit is contained in:
Jarrod Watts 2021-04-22 20:01:27 +10:00 committed by GitHub
parent 1caa7f4971
commit c481147b86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 445 additions and 269 deletions

View file

@ -36,9 +36,17 @@ yarn-error.log*
# Amplify
amplify/\#current-cloud-backend
amplify/.config/local-*
amplify/logs
amplify/mock-data
amplify/backend/amplify-meta.json
amplify/backend/awscloudformation
build/
amplify/backend/.temp
dist/
aws-exports.js
awsconfiguration.json
amplifyconfiguration.json
amplifyconfiguration.dart
amplify-build-config.json
amplify-gradle-config.json
amplifytools.xcconfig
.secret-*

View file

@ -6,9 +6,9 @@ This example shows how to build a server rendered web application with NextJS an
Two routes are implemented :
- `/` : A static route that uses getStaticProps to load data from AppSync and renders it on the server (Code in [pages/index.tsx](pages/index.tsx))
- `/` : A server-rendered route that uses `getServersideProps` to load data from AppSync and renders it on the server (Code in [pages/index.tsx](src/pages/index.tsx))
- `/todo/[id]` : A dynamic route that uses `getStaticProps` and the id from the provided context to load a single todo from AppSync and render it on the server. (Code in [pages/todo/[id].tsx](pages/todo/[id].tsx))
- `/todo/[id]` : A dynamic route that uses `getStaticPaths`, `getStaticProps` and the id from the provided context to load a single todo from AppSync and render it on the server. (Code in [pages/todo/[id].tsx](src/pages/todo/[id].tsx))
## How to use
@ -59,15 +59,17 @@ $ amplify init
javascript
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path: out
? Build Command: (npm run-script build)
? Start Command: (npm run-script start)
? Distribution Directory Path: build
? Build Command: npm run build
? Start Command: npm run start
? Do you want to use an AWS profile? Y
? Select the authentication method you want to use: AWS Profile
? Please choose the profile you want to use: <Your profile
# </Interactive>
```
#### Add the API
#### Add the API and the Auth
```sh
$ amplify add api
@ -76,14 +78,66 @@ $ amplify add api
GraphQL
REST
? Provide API name: <API_NAME>
? Choose an authorization type for the API (Use arrow keys)
? Choose the default authorization type for the API (Use arrow keys)
API key
Amazon Cognito User Pool
? Do you have an annotated GraphQL schema? (y/N) y
? Provide your schema file path: ./schema.graphql
IAM
OpenID Connect
? Enter a description for the API key: <API_DESCRIPTION>
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API:
No, I am done.
Yes, I want to make some additional changes.
? Configure additional auth types? y
? Choose the additional authorization types you want to configure for the API
(*) Amazon Cognito User Pool
( ) IAM
( ) OpenID Connect
Do you want to use the default authentication and security configuration? (Use arrow keys)
Default configuration
Default configuration with Social Provider (Federation)
Manual configuration
I want to learn more.
How do you want users to be able to sign in? (Use arrow keys)
Username
Email
Phone Number
Email or Phone Number
I want to learn more.
Do you want to configure advanced settings? (Use arrow keys)
No, I am done.
Yes, I want to make some additional changes.
? Enable conflict detection? N
? Do you have an annotated GraphQL schema? N
? Choose a schema template: (Use arrow keys)
Single object with fields (e.g., “Todo” with ID, name, description)
One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
Objects with fine-grained access control (e.g., a project management app with owner-based authorization)
? Do you want to edit the schema now? Y
# </Interactive>
```
#### Edit GraphQL Schema
Open [`amplify/backend/api/nextjswithamplifyts/schema.graphql`](amplify/backend/api/nextjswithamplifyts/schema.graphql) and change it to the following:
```
type Todo
@model
@auth(
rules: [
{ allow: owner } # Allow the creator of a todo to perform Create, Update, Delete operations.
{ allow: public, operations: [read] } # Allow public (guest users without an account) to Read todos.
{ allow: private, operations: [read] } # Allow private (other signed in users) to Read todos.
]
) {
id: ID!
name: String!
description: String
}
```
#### Deploy infrastructure
```sh
@ -95,10 +149,10 @@ $ amplify push
javascript
typescript
flow
? Enter the file name pattern of graphql queries, mutations and subscriptions (src/graphql/**/*.js)
? Enter the file name pattern of graphql queries, mutations and subscriptions (src/graphql/**/*.ts)
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions (Y/n) Y
? Enter maximum statement depth [increase from default if your schema is deeply nested] (2)
? Enter the file name for the generated code: src\API.ts
# </Interactive>
```
@ -111,9 +165,3 @@ npm run dev
yarn
yarn dev
```
### Edit GraphQL Schema
1. Open [`amplify/backend/api/nextjswithamplifyts/schema.graphql`](amplify/backend/api/nextjswithamplifyts/schema.graphql) and change what you need to.
2. Run `amplify push`
3. 👍

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "",
"scripts": {
"dev": "next",
"dev": "next dev",
"build": "next build",
"start": "next start"
},
@ -11,17 +11,16 @@
"author": "",
"license": "MIT",
"dependencies": {
"aws-amplify": "2.1.0",
"immer": "3.1.3",
"nanoid": "2.0.3",
"next": "latest",
"react": "16.13.1",
"react-dom": "16.13.1"
"@aws-amplify/ui-react": "^1.0.7",
"aws-amplify": "^3.3.27",
"next": "10.1.3",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@types/node": "12.6.8",
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"typescript": "4.0"
"@types/node": "^14.14.41",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"typescript": "^4.2.4"
}
}

View file

@ -1,177 +0,0 @@
import { Reducer, useReducer, Dispatch } from 'react'
import { API, graphqlOperation } from 'aws-amplify'
import nanoid from 'nanoid'
import produce from 'immer'
import { ListTodosQuery, GetTodoListQuery } from '../src/API'
import config from '../src/aws-exports'
import {
createTodo,
deleteTodo,
createTodoList,
} from '../src/graphql/mutations'
import { getTodoList } from '../src/graphql/queries'
const MY_ID = nanoid()
API.configure(config)
type Todo = Omit<
ListTodosQuery['listTodos']['items'][0],
'__typename' | 'todoList'
>
type Props = {
todos: Todo[]
}
type State = {
todos: Todo[]
currentName: string
}
type Action =
| {
type: 'add-todo'
payload: Todo
}
| {
type: 'delete-todo'
payload: string
}
| {
type: 'reset-current'
}
| { type: 'set-current'; payload: string }
const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
case 'add-todo': {
return produce(state, (draft) => {
draft.todos.push(action.payload)
})
}
case 'delete-todo': {
const index = state.todos.findIndex(({ id }) => action.payload === id)
if (index === -1) return state
return produce(state, (draft) => {
draft.todos.splice(index, 1)
})
}
case 'reset-current': {
return produce(state, (draft) => {
draft.currentName = ''
})
}
case 'set-current': {
return produce(state, (draft) => {
draft.currentName = action.payload
})
}
default: {
return state
}
}
}
const createToDo = async (dispatch: Dispatch<Action>, currentToDo) => {
const todo = {
id: nanoid(),
name: currentToDo,
createdAt: `${Date.now()}`,
completed: false,
todoTodoListId: 'global',
userId: MY_ID,
}
dispatch({ type: 'add-todo', payload: todo })
dispatch({ type: 'reset-current' })
try {
await API.graphql(graphqlOperation(createTodo, { input: todo }))
} catch (err) {
dispatch({ type: 'set-current', payload: todo.name })
console.warn('Error adding to do ', err)
}
}
const deleteToDo = async (dispatch: Dispatch<Action>, id: string) => {
dispatch({ type: 'delete-todo', payload: id })
try {
await API.graphql({
...graphqlOperation(deleteTodo),
variables: { input: { id } },
})
} catch (err) {
console.warn('Error deleting to do ', err)
}
}
const App = (props: Props) => {
const [state, dispatch] = useReducer(reducer, {
todos: props.todos,
currentName: '',
})
return (
<div>
<h3>Add a Todo</h3>
<form
onSubmit={(ev) => {
ev.preventDefault()
createToDo(dispatch, state.currentName)
}}
>
<input
value={state.currentName}
onChange={(e) => {
dispatch({ type: 'set-current', payload: e.target.value })
}}
/>
<button type="submit">Create Todo</button>
</form>
<h3>Todos List</h3>
{state.todos.map((todo, index) => (
<p key={index}>
<a href={`/todo/${todo.id}`}>{todo.name}</a>
<button
onClick={() => {
deleteToDo(dispatch, todo.id)
}}
>
delete
</button>
</p>
))}
</div>
)
}
export const getStaticProps = async () => {
let result = (await API.graphql(
graphqlOperation(getTodoList, { id: 'global' })
)) as { data: GetTodoListQuery; errors: any[] }
if (result.errors) {
console.error('Failed to fetch todolist.', result.errors)
throw new Error(result.errors[0].message)
}
if (result.data.getTodoList !== null) {
return {
props: {
todos: result.data.getTodoList.todos.items,
},
}
}
await API.graphql(
graphqlOperation(createTodoList, {
input: {
id: 'global',
createdAt: `${Date.now()}`,
},
})
)
return {
props: {
todos: [],
},
}
}
export default App

View file

@ -1,48 +0,0 @@
import { API, graphqlOperation } from 'aws-amplify'
import { GetTodoQuery, GetTodoListQuery } from '../../src/API'
import { getTodo, getTodoList } from '../../src/graphql/queries'
import config from '../../src/aws-exports'
API.configure(config)
const TodoPage = (props: { todo: GetTodoQuery['getTodo'] }) => {
return (
<div>
<h2>Individual Todo {props.todo.id}</h2>
<pre>{JSON.stringify(props.todo, null, 2)}</pre>
</div>
)
}
export const getStaticPaths = async () => {
let result = (await API.graphql(
graphqlOperation(getTodoList, { id: 'global' })
)) as { data: GetTodoListQuery; errors: any[] }
if (result.errors) {
console.error('Failed to fetch todos paths.', result.errors)
throw new Error(result.errors[0].message)
}
const paths = result.data.getTodoList.todos.items.map(({ id }) => ({
params: { id },
}))
return { paths, fallback: false }
}
export const getStaticProps = async ({ params: { id } }) => {
const todo = (await API.graphql({
...graphqlOperation(getTodo),
variables: { id },
})) as { data: GetTodoQuery; errors: any[] }
if (todo.errors) {
console.error('Failed to fetch todo.', todo.errors)
throw new Error(todo.errors[0].message)
}
return {
props: {
todo: todo.data.getTodo,
},
}
}
export default TodoPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,15 +0,0 @@
type Todo @model {
# ! means non-null GraphQL fields are allowed to be null by default
id: ID!
name: String!
createdAt: String!
completed: Boolean!
todoList: TodoList! @connection(name: "SortedList")
userId: String!
}
type TodoList @model {
id: ID!
createdAt: String!
todos: [Todo] @connection(name: "SortedList", sortField: "createdAt")
}

View file

@ -0,0 +1,118 @@
import { AmplifyAuthenticator } from '@aws-amplify/ui-react'
import { Amplify, API, Auth, withSSRContext } from 'aws-amplify'
import Head from 'next/head'
import awsExports from '../aws-exports'
import { createTodo } from '../graphql/mutations'
import { listTodos } from '../graphql/queries'
import {
CreateTodoInput,
CreateTodoMutation,
ListTodosQuery,
Todo,
} from '../API'
import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api'
import { useRouter } from 'next/router'
import { GetServerSideProps } from 'next'
import styles from '../styles/Home.module.css'
Amplify.configure({ ...awsExports, ssr: true })
export default function Home({ todos = [] }: { todos: Todo[] }) {
const router = useRouter()
async function handleCreateTodo(event) {
event.preventDefault()
const form = new FormData(event.target)
try {
const createInput: CreateTodoInput = {
name: form.get('title').toString(),
description: form.get('content').toString(),
}
const request = (await API.graphql({
authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
query: createTodo,
variables: {
input: createInput,
},
})) as { data: CreateTodoMutation; errors: any[] }
router.push(`/todo/${request.data.createTodo.id}`)
} catch ({ errors }) {
console.error(...errors)
throw new Error(errors[0].message)
}
}
return (
<div className={styles.container}>
<Head>
<title>Amplify + Next.js</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>Amplify + Next.js</h1>
<p className={styles.description}>
<code className={styles.code}>{todos.length}</code>
Todos
</p>
<div className={styles.grid}>
{todos.map((todo) => (
<a href={`/todo/${todo.id}`} key={todo.id}>
<h3>{todo.name}</h3>
<p>{todo.description}</p>
</a>
))}
<div className={styles.card}>
<h3 className={styles.title}>New Todo</h3>
<AmplifyAuthenticator>
<form onSubmit={handleCreateTodo}>
<fieldset>
<legend>Title</legend>
<input
defaultValue={`Today, ${new Date().toLocaleTimeString()}`}
name="title"
/>
</fieldset>
<fieldset>
<legend>Content</legend>
<textarea
defaultValue="I built an Amplify app with Next.js!"
name="content"
/>
</fieldset>
<button>Create Todo</button>
<button type="button" onClick={() => Auth.signOut()}>
Sign out
</button>
</form>
</AmplifyAuthenticator>
</div>
</div>
</main>
</div>
)
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const SSR = withSSRContext({ req })
const response = (await SSR.API.graphql({ query: listTodos })) as {
data: ListTodosQuery
}
return {
props: {
todos: response.data.listTodos.items,
},
}
}

View file

@ -0,0 +1,100 @@
import { Amplify, API, withSSRContext } from 'aws-amplify'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { DeleteTodoInput, GetTodoQuery, Todo, ListTodosQuery } from '../../API'
import awsExports from '../../aws-exports'
import { deleteTodo } from '../../graphql/mutations'
import { getTodo, listTodos } from '../../graphql/queries'
import { GetStaticProps, GetStaticPaths } from 'next'
import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api'
import styles from '../../styles/Home.module.css'
Amplify.configure({ ...awsExports, ssr: true })
export default function TodoPage({ todo }: { todo: Todo }) {
const router = useRouter()
if (router.isFallback) {
return (
<div className={styles.container}>
<h1 className={styles.title}>Loading&hellip;</h1>
</div>
)
}
async function handleDelete(): Promise<void> {
try {
const deleteInput: DeleteTodoInput = {
id: todo.id,
}
await API.graphql({
authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
query: deleteTodo,
variables: {
input: deleteInput,
},
})
router.push(`/`)
} catch ({ errors }) {
console.error(...errors)
throw new Error(errors[0].message)
}
}
return (
<div className={styles.container}>
<Head>
<title>{todo.name} Amplify + Next.js</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>{todo.name}</h1>
<p className={styles.description}>{todo.description}</p>
</main>
<footer>
<button className={styles.footer} onClick={handleDelete}>
💥 Delete todo
</button>
</footer>
</div>
)
}
export const getStaticPaths: GetStaticPaths = async () => {
const SSR = withSSRContext()
const todosQuery = (await SSR.API.graphql({
query: listTodos,
authMode: GRAPHQL_AUTH_MODE.API_KEY,
})) as { data: ListTodosQuery; errors: any[] }
const paths = todosQuery.data.listTodos.items.map((todo: Todo) => ({
params: { id: todo.id },
}))
return {
fallback: true,
paths,
}
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const SSR = withSSRContext()
const response = (await SSR.API.graphql({
query: getTodo,
variables: {
id: params.id,
},
})) as { data: GetTodoQuery }
return {
props: {
todo: response.data.getTodo,
},
}
}

View file

@ -0,0 +1,123 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
direction: column;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View file

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}