概要 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!' };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' , targetIdentityId?: 'XXXXXXX' , validateObjectExistence?: false , expiresIn?: 20 useAccelerateEndpoint?: true ; }, }); 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について勉強し、一般的な考え方ややり方を理解していくことにします。