AWS Amplify のチュートリアルをやってみた時に詰まったところのメモ

概要

DynamoDBの中身を扱うツールが欲しくなったので、
今更ですがAmplifyを使ってみようと思い、公式のチュートリアルをやってみました。

ちなみにReactも触ったことがなく、今回初めて使っているので勘違いしている部分があるかもしれませんが、ご了承ください。

チュートリアルが作成された当時に比べてAmplifyのバージョンが上がっているせいか、所々でエラーが出ていたので、調べた記録を残しておくことにしました。

実施したチュートリアルとエラーの概要

今回試したのは以下のチュートリアルです。

エラーの概要としては、「モジュール 4: GraphQL API とデータベースを追加する」で、以下のようにimportする部分があるのですが、amplifyのライブラリがバージョンアップして使い方が変わっているようで、このままコピペするとエラーが出ました。

1
import { API } from "aws-amplify";

Amplify Dev Centerのドキュメントを参考にして修正したところ正常に動作したので、
おそらくこれで合っているのだろうと思っています。

1
import { generateClient } from 'aws-amplify/api';

詳細は続きを見てください。

遭遇したエラーたち

「モジュール 4: GraphQL API とデータベースを追加する」で遭遇したエラー

「API と相互にやり取りするためのフロントエンドコードを記述する」のチュートリアル
に記載されている src/App.js をそのままコピペして npm start を実行すると、
以下のようなエラーが出ました。

1
2
3
4
5
6
7
8
9
10
11
12
13
Failed to compile.

Attempted import error: 'API' is not exported from 'aws-amplify' (imported as 'API').
ERROR in ./src/App.js 22:26-37
export 'API' (imported as 'API') was not found in 'aws-amplify' (possible exports: Amplify)

ERROR in ./src/App.js 35:10-21
export 'API' (imported as 'API') was not found in 'aws-amplify' (possible exports: Amplify)

ERROR in ./src/App.js 49:10-21
export 'API' (imported as 'API') was not found in 'aws-amplify' (possible exports: Amplify)

webpack compiled with 3 errors

冒頭にも述べましたが、以下のドキュメントによると、

1
import { API } from "aws-amplify";

とするのではなく、以下のように import したうえで const で client を宣言して、
graphqlのメソッドを呼び出すように書いてありました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { generateClient } from 'aws-amplify/api';
import { createTodo, updateTodo, deleteTodo } from './graphql/mutations';

const client = generateClient();

const todo = { name: 'My first todo', description: 'Hello world!' };

/* create a todo */
await client.graphql({
query: createTodo,
variables: {
input: todo
}
});

(以下略)

そこで、以下のように変更したところ、エラーが解消されて正常に動作しました。

よくよく見ると query: createNoteMutation あたりも変更が必要そうな気がしたのですが、とりあえず動いたので今回は考えないことにしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import React, { useState, useEffect } from "react";
import "./App.css";
import "@aws-amplify/ui-react/styles.css";
- import { API } from "aws-amplify";
+ import { generateClient } from "aws-amplify/api";
import {
Button,
Flex,
Heading,
Text,
TextField,
View,
withAuthenticator,
} from "@aws-amplify/ui-react";
import { listNotes } from "./graphql/queries";
import {
createNote as createNoteMutation,
deleteNote as deleteNoteMutation,
} from "./graphql/mutations";

const App = ({ signOut }) => {
const [notes, setNotes] = useState([]);
+ const client = generateClient();

useEffect(() => {
fetchNotes();
}, []);

async function fetchNotes() {
- const apiData = await API.graphql({ query: listNotes });
+ const apiData = await client.graphql({ query: listNotes });
const notesFromAPI = apiData.data.listNotes.items;
setNotes(notesFromAPI);
}

async function createNote(event) {
event.preventDefault();
const form = new FormData(event.target);
const data = {
name: form.get("name"),
description: form.get("description"),
};
- await API.graphql({
+ await client.graphql({
query: createNoteMutation,
variables: { input: data },
});
fetchNotes();
event.target.reset();
}

async function deleteNote({ id }) {
const newNotes = notes.filter((note) => note.id !== id);
setNotes(newNotes);
- await API.graphql({
+ await client.graphql({
query: deleteNoteMutation,
variables: { input: { id } },
});
}

return (
<View className="App">
<Heading level={1}>My Notes App</Heading>
<View as="form" margin="3rem 0" onSubmit={createNote}>
<Flex direction="row" justifyContent="center">
<TextField
name="name"
placeholder="Note Name"
label="Note Name"
labelHidden
variation="quiet"
required
/>
<TextField
name="description"
placeholder="Note Description"
label="Note Description"
labelHidden
variation="quiet"
required
/>
<Button type="submit" variation="primary">
Create Note
</Button>
</Flex>
</View>
<Heading level={2}>Current Notes</Heading>
<View margin="3rem 0">
{notes.map((note) => (
<Flex
key={note.id || note.name}
direction="row"
justifyContent="center"
alignItems="center"
>
<Text as="strong" fontWeight={700}>
{note.name}
</Text>
<Text as="span">{note.description}</Text>
<Button variation="link" onClick={() => deleteNote(note)}>
Delete note
</Button>
</Flex>
))}
</View>
<Button onClick={signOut}>Sign Out</Button>
</View>
);
};

export default withAuthenticator(App);

「モジュール 5: ストレージを追加する」で遭遇したエラー

モジュール4とほぼ同じですが、
ここでも「React アプリケーションを更新する」のチュートリアル
に記載されている src/App.js をそのままコピペして npm start を実行したところ、
以下のようなエラーが出ました。

1
2
3
4
5
6
7
8
9
10
11
12
13
Failed to compile.

Attempted import error: 'Storage' is not exported from 'aws-amplify' (imported as 'Storage').
ERROR in ./src/App.js 30:26-37
export 'Storage' (imported as 'Storage') was not found in 'aws-amplify' (possible exports: Amplify)

ERROR in ./src/App.js 46:28-39
export 'Storage' (imported as 'Storage') was not found in 'aws-amplify' (possible exports: Amplify)

ERROR in ./src/App.js 62:10-24
export 'Storage' (imported as 'Storage') was not found in 'aws-amplify' (possible exports: Amplify)

webpack compiled with 3 errors

今回は、ドキュメントのストレージの説明を参照します。

チュートリアルのコードは以下のような呼び出し方になっていますが、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Storage } from "aws-amplify";

(中略)

const url = await Storage.get(note.name);

(中略)

if (!!data.image) await Storage.put(data.name, image);

(中略)

await Storage.remove(name);

(以下略)

ここもドキュメントを参照すると変更があったようで、以下のように import して使うように書かれています。

下記は getUrl だけを抜粋してドキュメントから転記しましたが、 uploadData, remove も同様です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { getUrl } from 'aws-amplify/storage';

const getUrlResult = await getUrl({
key: filename,
options: {
accessLevel?: 'guest' , // can be 'private', 'protected', or 'guest' but defaults to `guest`
targetIdentityId?: 'XXXXXXX', // id of another user, if `accessLevel` is `guest`
validateObjectExistence?: false, // defaults to false
expiresIn?: 20 // validity of the URL, in seconds. defaults to 900 (15 minutes) and maxes at 3600 (1 hour)
useAccelerateEndpoint?: true; // Whether to use accelerate endpoint.
},
});
console.log('signed URL: ', getUrlResult.url);
console.log('URL expires at: ', getUrlResult.expiresAt);

ドキュメントに倣って、以下のように変更したところエラーが解消されて正常に動作しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import React, { useState, useEffect } from "react";
import "./App.css";
import "@aws-amplify/ui-react/styles.css";
import { generateClient } from "aws-amplify/api";
- import { Storage } from "aws-amplify";
+ import { uploadData, getUrl, remove } from "aws-amplify/storage";
import {
Button,
Flex,
Heading,
Image,
Text,
TextField,
View,
withAuthenticator,
} from "@aws-amplify/ui-react";
import { listNotes } from "./graphql/queries";
import {
createNote as createNoteMutation,
deleteNote as deleteNoteMutation,
} from "./graphql/mutations";

const App = ({ signOut }) => {
const client = generateClient();
const [notes, setNotes] = useState([]);

useEffect(() => {
fetchNotes();
}, []);

async function fetchNotes() {
const apiData = await client.graphql({ query: listNotes });
const notesFromAPI = apiData.data.listNotes.items;
await Promise.all(
notesFromAPI.map(async (note) => {
if (note.image) {
- const url = await Storage.get(note.name);
- note.image = url;
+ const getUrlResult = await getUrl({ key: note.name });
+ note.image = getUrlResult.url;
}
return note;
})
);
setNotes(notesFromAPI);
}

async function createNote(event) {
event.preventDefault();
const form = new FormData(event.target);
const image = form.get("image");
const data = {
name: form.get("name"),
description: form.get("description"),
image: image.name,
};
- if (!!data.image) await Storage.put(data.name, image);
+ if (!!data.image) {
+ try {
+ const result = await uploadData({
+ key: data.name,
+ data: image
+ }).result;
+ console.log('Succeeded: ', result);
+ } catch (error) {
+ console.log('Error : ', error);
+ }
+ }
await client.graphql({
query: createNoteMutation,
variables: { input: data },
});
fetchNotes();
event.target.reset();
}

async function deleteNote({ id, name }) {
const newNotes = notes.filter((note) => note.id !== id);
setNotes(newNotes);
- await Storage.remove(name);
+ try {
+ await remove({ key: name });
+ } catch (error) {
+ console.log('Error ', error);
+ }
await client.graphql({
query: deleteNoteMutation,
variables: { input: { id } },
});
}

return (
<View className="App">
<Heading level={1}>My Notes App</Heading>
<View as="form" margin="3rem 0" onSubmit={createNote}>
<Flex direction="row" justifyContent="center">
<TextField
name="name"
placeholder="Note Name"
label="Note Name"
labelHidden
variation="quiet"
required
/>
<TextField
name="description"
placeholder="Note Description"
label="Note Description"
labelHidden
variation="quiet"
required
/>
<View
name="image"
as="input"
type="file"
style={{ alignSelf: "end" }}
/>
<Button type="submit" variation="primary">
Create Note
</Button>
</Flex>
</View>
<Heading level={2}>Current Notes</Heading>
<View margin="3rem 0">
{notes.map((note) => (
<Flex
key={note.id || note.name}
direction="row"
justifyContent="center"
alignItems="center"
>
<Text as="strong" fontWeight={700}>
{note.name}
</Text>
<Text as="span">{note.description}</Text>
{note.image && (
<Image
src={note.image}
alt={`visual aid for ${notes.name}`}
style={{ width: 400 }}
/>
)}
<Button variation="link" onClick={() => deleteNote(note)}>
Delete note
</Button>
</Flex>
))}
</View>
<Button onClick={signOut}>Sign Out</Button>
</View>
);
};

export default withAuthenticator(App);

感想

AmplifyとReactを初めて触ったのと、javascriptもあまり触ったことが無い状態から始めたものの、
特に困難な部分もなくチュートリアルを完了できたので、初心者に優しい良いチュートリアルだと思いました。

特に、認証機能の追加が非常に簡単に行えたのは便利ですね、実際に今後作るツールでも活用したいと感じました。

気になったのは、将来的に開発環境を作り直したとき、バージョン差分がどう影響してくるのか(自分が)理解できてない点です。

Reactやnpmについて勉強し、一般的な考え方ややり方を理解していくことにします。