Amplify(Gen1)でAppSync+DynamoDBテーブルを作成しながらschema.graphqlを読み解く
AmplifyでAppSync+DynamoDBテーブルを作成する
チュートリアルに沿ってAmplifyを作成するところまで実施します。
(ここまで: https://docs.amplify.aws/gen1/react/start/getting-started/data-model/)
本記事は schema.graphql
に焦点を当てる記事のため、Amplifyのチュートリアル自体には深く触れません。
amplify add api
でAPIを追加しamplify push
を実行すると、このようなファイルが自動生成されます。
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
type Blog @model {
id: ID!
name: String!
posts: [Post] @hasMany
}
type Post @model {
id: ID!
title: String!
blog: Blog @belongsTo
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
post: Post @belongsTo
content: String!
}
ここではGraphQLのスキーマを定義していますが、同時にDynamoDBの定義も行っています。DynamoDBを作成するには @model
をつける必要があります。
この @
から始まる @xxx
はディレクティブと呼ばれます。
Amplifyには多くのディレクティブがあります。
Transformer v1 と v2 で大きくディレクティブが変更になっているのですが、そちらは後述します。
Directive | Description |
---|---|
@model | スキーマ定義に沿って自動でDynamoDBテーブルを作成し、idというフィールドを自動でPrimaryKeyとして追加する(変更可能) またAppSyncのスキーマ定義に createAt とupdateAt フィールドも自動で追加される(この2つの値のデフォルトはReadOnly) |
@primaryKey |
@model で追加されるidフィールドではなく、独自のフィールドをprimaryKeyとしたい場合に使用する |
@index | グローバルセカンダリインデックスを作成したい場合に対象のフィールドに付与する |
@hasOne | 1方向の1対1リレーションシップを設定する |
@hasMany | 1方向の1対多リレーションシップを設定する |
@belogsTo | @hasOneまたは@hasManyのフィールドに双方向の1対1リレーションシップを設定する これにより逆引きができるようになる |
@manyToMany | 2つのテーブルの結合テーブルを設定する |
@auth | データに対する認可を設定する |
@function | Lambda関数リゾルバーをフィールドに設定する |
@http | HTTPリゾルバーをフィールドに設定する |
@predictions | Amazon Rekognition、Amazon Translate、Amazon PollyなどのAI/MLサービスのオーケストレーションにクエリする |
@searchable | Amazon OpenSearchへデータを作成する(ストリーミング) |
@mapsTo | 商用のデータ移行などで一時的にテーブルを使いたいときに使う |
GraphQL Transformer v1 と v2
schema.graphqlの記法にはVersion1とVersion2があります。
記法がv1とv2で大きく異なるので、使用しているTransformerのバージョン確認は必須です。
{
"features": {
"graphqltransformer": {
"addmissingownerfields": true,
"improvepluralization": false,
"validatetypenamereservedwords": true,
"useexperimentalpipelinedtransformer": true,
"enableiterativegsiupdates": true,
"secondarykeyasgsi": true,
"skipoverridemutationinputtypes": true,
"transformerversion": 2, //ここにある
"suppressschemamigrationprompt": true,
.....
}
v1とv2では使用するディレクティブが異なります。
例えばGSIを貼る場合、v1では@key
を使用していましたが、v2では@index
を使用します。
またディレクティブを付与する位置もv1ではtype
の定義と同じ行でしたが、v2からはフィールドに記述するようになりました。
以下の画像ではv1とv2の違いについて示されています。
@index
に加えて@primaryKey
の定義方法も変更されました。
@auth
ディレクティブについても、v1では@auth
をつけない場合にはデフォルトでAPIキー認証が設定されましたが、v2からはデフォルトでクライアントからのアクセスを拒否する設定になります。
操作許可の部分ではv1とv2で真逆の意味になります。
v1ではoperations
に記載した操作が拒否され、書いていない操作は許可。
v2ではoperations
に記載した操作は許可され、書いていない操作は拒否されます。
v1で使用されていた@connection
ディレクティブは、v2から廃止され@hasOne、@hasMany、@belongsTo、@manyToMany
に変わりました。
type Post @model {
id: ID!
title: String!
author: User @connection(fields: ["authorID"])
authorID: ID
}
type Post @model {
id: ID!
title: String!
author: User @hasOne(fields: ["authorID"])
authorID: ID
}
引用: https://docs.amplify.aws/gen1/react/tools/cli/migration/transformer-migration/
このようにTransformerのバージョンにより大きく記法が異なります。
そのためAmplifyのドキュメントでは、ドキュメントの上部に以下のような注意書きが出ています。
v1なのかv2なのか確認してからドキュメントを参照するようにしましょう。
リレーションシップに関連するディレクティブを深掘りする
リレーションシップに関連するディレクティブを使用することで、Amplify側でよしなに様々な設定を行なってくれます。
ただ細かく設定したい場合は落とし穴があるため、その点にも触れながらリレーションシップに関連するディレクティブを紹介していきます。
- @hasOne
- @hasMany
- @belongsTo
- @manyToMany
@hasOne
hasOneにすると、対象テーブルのID(Primary Key)がスキーマに追加されることになります。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
}
@hasOne
を定義したので、Teamテーブルを参考にProjectテーブルで必要なフィールドを定義します。
mutation CreateProject {
createProject(input: { projectTeamId: "team-id", name: "Some Name" }) {
team {
name
id
}
name
id
}
}
仮にTeamテーブルに2つ以外にteamCode
のようなフィールドがスキーマに定義されていても、Projectテーブルで使用しないのであれば上記と同じMutationで動作します。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
teamCode: String //任意でもう一つフィールドを定義
}
mutation CreateProject {
createProject(input: { projectTeamId: "team-id", name: "Some Name" }) {
team {
name
id
}
name
id
}
}
@hasOneにフィールドを追加することで、該当のteamIdと紐づけることができます。
以下の例ではProjectテーブルのteamIdフィールドとTeamテーブルのIdを紐づけています。
type Project @model {
id: ID!
name: String
teamID: ID
team: Team @hasOne(fields: ["teamID"])
}
type Team @model {
id: ID!
name: String!
}
mutation CreateProject {
createProject(input: { name: "New Project", teamID: "a-team-id" }) {
id
name
team {
id
name
}
}
}
@hasMany
@hasMany
ディレクティブを付与するには以下のようにします。
このディレクティブを付与すると、リレーションシップを設定した先のテーブル(ここではComment)に対してGSIが自動で作成されます。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
content: String!
}
mutation CreatePost {
createPost(input: { title: "Hello World!!" }) {
title
id
comments {
items {
id
content
}
}
}
}
デフォルトではGSIに対して gsi-{@hasManyディレクティブをつけたオブジェクト型}.{@hasManyディレクティブをつけたフィールド名}
という名前が付与されます。
Post
のcomments
に対してComment
とのリレーションシップを設定したので、Commentテーブルにgsi-Post.comments
という名前のGSIが作成されています。(ややこしい)
ついでに見ると、デフォルトで作成されるGSIのパーティションキーは
{Postテーブル}{Postテーブルのcommentsフィールド}{Commentテーブルのプライマリーキー}
になってます。
デフォルトではなく独自でインデックスを付与したい場合はディレクティブの後ろに定義することでカスタマイズできます。
また同時にソートキーも設定可能です。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
}
type Comment @model {
id: ID!
postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
content: String!
}
ここでは以下の構成になっています。
-
Post
のid
は、Comment
のpostID
に紐づけられている -
Comment
側では、postID
フィールドにbyPost
というインデックスが設定されている
この設定により、Post
の id
をキーとして、Comment
テーブルのセカンダリインデックス byPost
をクエリすることで、関連する Comment
オブジェクトを取得できる仕組みになっています。
実際のコメントテーブルのGSIはこのようになっています。
@authについて
@authディレクティブを使用して、各データに対して細かく認可の設定が可能です。
public, owner, private, groups, custom の選択肢があり、操作よって細かく設定できます。
type Todo
@model
@auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) {
content: String
}
認証方法の種類
Strategy | Provider | 説明 |
---|---|---|
public | apiKey | パブリックデータアクセス。APIキーを使用すれば全てのユーザーにアクセス権が付与される。 |
public | iam(or identityPool) | 商用環境のパブリックアクセスで推奨される方法。認証されていないユーザーにアクセス許可が付与される。 |
owner | userPools/oidc | レコードの所有者(owner)のみデータアクセス可能 |
private | userPools/oidc/iam | サインインしている全てのユーザーにアクセス権が付与される |
groups | userPools/oidc | 特定のユーザーグループにアクセス権が付与される |
custom | function | Lambda関数内で独自のカスタム認証ルールを定義し、それを使用する |
privateでproviderを付与しない場合は、Cognitoユーザープールから有効なJWTトークンを持つ全てのユーザーがアクセスできる状態になります。
type Todo @model @auth(rules: [{ allow: private }]) {
content: String
}
これをオーバーライドして、Cognito IDプールの認証済みロールに設定することもできます。
type Post @model @auth(rules: [{ allow: private, provider: iam }]) {
id: ID!
title: String!
}
ちなみにCognito ユーザープールとIDプールの違いは以下のイメージです。
- Cognito ユーザープール: JWTトークンを使用する
- Cognito IDプール: AWSのクレデンシャルを使用する
適用してデプロイしてみる
以下のschema.graphql
ファイルの内容でデプロイしてみます。
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
type Blog @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
name: String!
posts: [Post] @hasMany
}
type Post @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
title: String!
blog: Blog @belongsTo
comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
createdBy: User @hasOne
}
type Comment @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
content: String!
}
type Team @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
name: String!
}
type User @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
name: String!
tema: Team @hasOne
}
% amplify push
✔ Successfully pulled backend environment xxxx from the cloud.
Current Environment: xxxx
┌──────────┬────────────────────┬───────────┬───────────────────┐
│ Category │ Resource name │ Operation │ Provider plugin │
├──────────┼────────────────────┼───────────┼───────────────────┤
│ Api │ xxxx │ No Change │ awscloudformation │
└──────────┴────────────────────┴───────────┴───────────────────┘
No changes detected
warningが出ました。このバージョンのAmplify CLIではiam
ではなく identityPool
を使うように、ということです。
WARNING: Schema is using an @auth directive with deprecated provider 'iam'. Replace 'iam' provider with 'identityPool' provider.
AWSコンソール上でAppSyncのクエリを実際に叩きたい場合は以下の方法で確認できます。
- Amplifyで設定したIAMベースの
@auth
ルールは、Amplifyで作成されたIAMロールでのみ許可されているため、このままでは403エラーになります。
AWSコンソール上でAPIの動作テストをするためには、custom-role.jsonというファイルに設定を記載する必要があります。
{
"adminRoleNames": ["<YOUR_IAM_USER_OR_ROLE_NAME>"]
}
- AppSyncのコンソールから作成されたAPIを選択
- クエリを選択します
- 鍵のアイコンを選択します
- デフォルトはIAM認証になっているので、もし別の認証方法を選択した場合はここで設定してください。
- 設定できたので、試しに
listTeams
を実行してみます
- 結果が表示されます。今回はまだ何もDynamoDBに登録されていないので、何も帰ってきません。
もし認証されていない場合は、以下のようなエラーが返却されます。
@functionについて
@model
ディレクティブを付けることで、create、read、update、delete、subscription の各クエリやミューテーションが自動で作成されます。
@function
は自動作成されるクエリとは別に、独自のロジックを持つクエリを手動で定義したい場合に使用します。
使用方法
- カスタムクエリやミューテーションを定義する
type Query {
myCustomQuery(args: String): String
}
type Mutation {
myCustomMutation(args: String): String
}
- リゾルバーオプションを設定する
リゾルバーオプションを設定することで、データ取得や処理の際の挙動を制御できます。
- Lambda関数リゾルバー: Lambda関数を使用してクエリやミューテーションを処理します
- HTTPリゾルバー: クエリやミューテーション時にHTTPエンドポイントを呼び出します
- AppSync JavaScript または VTLリゾルバー: JavaScriptリゾルバーかVTLマッピングテンプレートを使用してロジックをカスタマイズします
Lambda関数リゾルバーの場合
まず、Lambda関数を作成します。
exports.handler = async function (event, context) {
return event.arguments.msg;
};
schema.graphql
のスキーマに@function
を追加します。
type Query {
echo(msg: String): String @function(name: "echofunction")
}
これで設定は完了です。
@function の動作
Amplifyは@function
が定義されたGraphQLの各フィールドに対し、内部的に以下のリソースを作成します。
- AWS IAMロール
- Lambda関数を呼びだす権限を持つ
- AWS AppSyncに対する信頼ポリシー
- AWS AppSyncデータソース
- AWS AppSync パイプライン関数
- AWS AppSync リゾルバー
認可ルール(@auth)の追加
カスタムリゾルバーの定義にも、@auth
をつけることで認可ルールを追加することができます。
※VTLリゾルバーを使用している場合には併用不可
type Mutation {
myCustomMutation(args: String): String
@auth(rules: [{ allow: private, provider: iam }])
}
おわり
Amplifyは公式ドキュメントを確認しながら進めることで、効率的にバックエンド構築が可能です。しかし、リレーションシップや認可設定など細かい部分では注意点も多く、実際に試行錯誤することが重要です。
よく読めばドキュメントにあったり、なかったりするので検証しながら進めるのが吉だと感じました。
参考にさせていただいた記事
Discussion