🪡

Amplify(Gen1)でAppSync+DynamoDBテーブルを作成しながらschema.graphqlを読み解く

2025/01/25に公開

AmplifyでAppSync+DynamoDBテーブルを作成する

チュートリアルに沿ってAmplifyを作成するところまで実施します。
(ここまで: https://docs.amplify.aws/gen1/react/start/getting-started/data-model/)
本記事は schema.graphql に焦点を当てる記事のため、Amplifyのチュートリアル自体には深く触れません。
https://docs.amplify.aws/gen1/react/start/getting-started/data-model/

amplify add api でAPIを追加しamplify push を実行すると、このようなファイルが自動生成されます。

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 {
  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のスキーマ定義にcreateAtupdateAtフィールドも自動で追加される(この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 商用のデータ移行などで一時的にテーブルを使いたいときに使う

https://docs.amplify.aws/gen1/react/tools/cli/graphqlapi/directives-reference/

GraphQL Transformer v1 と v2

schema.graphqlの記法にはVersion1とVersion2があります。
記法がv1とv2で大きく異なるので、使用しているTransformerのバージョン確認は必須です。

amplify-dev/amplify/cli.json
{
  "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に変わりました。

v1
type Post @model {
  id: ID!
  title: String!
  author: User @connection(fields: ["authorID"])
  authorID: ID
}
v2
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)がスキーマに追加されることになります。

schema.graphql
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で動作します。

schema.graphql
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を紐づけています。

schema.graphql
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が自動で作成されます。

schema.graphql
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ディレクティブをつけたフィールド名} という名前が付与されます。

Postcommentsに対してCommentとのリレーションシップを設定したので、Commentテーブルにgsi-Post.commentsという名前のGSIが作成されています。(ややこしい)

ついでに見ると、デフォルトで作成されるGSIのパーティションキーは
{Postテーブル}{Postテーブルのcommentsフィールド}{Commentテーブルのプライマリーキー}
になってます。

デフォルトではなく独自でインデックスを付与したい場合はディレクティブの後ろに定義することでカスタマイズできます。
また同時にソートキーも設定可能です。

schema.graphql
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!
}

ここでは以下の構成になっています。

  • Postid は、CommentpostID に紐づけられている
  • Comment 側では、postID フィールドに byPost というインデックスが設定されている

この設定により、Postid をキーとして、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!
}

https://docs.amplify.aws/gen1/react/build-a-backend/graphqlapi/customize-authorization-rules/#signed-in-user-data-access

ちなみにCognito ユーザープールとIDプールの違いは以下のイメージです。

  • Cognito ユーザープール: JWTトークンを使用する
  • Cognito IDプール: AWSのクレデンシャルを使用する
    alt text

https://docs.amplify.aws/gen1/javascript/prev/build-a-backend/auth/under-the-hood/#accessing-aws-services

適用してデプロイしてみる

以下のschema.graphqlファイルの内容でデプロイしてみます。

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のクエリを実際に叩きたい場合は以下の方法で確認できます。

  1. Amplifyで設定したIAMベースの@authルールは、Amplifyで作成されたIAMロールでのみ許可されているため、このままでは403エラーになります。
    AWSコンソール上でAPIの動作テストをするためには、custom-role.jsonというファイルに設定を記載する必要があります。
amplify/backend/api/<your-api-name>/custom-roles.json
{
  "adminRoleNames": ["<YOUR_IAM_USER_OR_ROLE_NAME>"]
}

https://docs.amplify.aws/gen1/javascript/build-a-backend/graphqlapi/customize-authorization-rules/#use-iam-authorization-within-the-appsync-console

  1. AppSyncのコンソールから作成されたAPIを選択
  2. クエリを選択します
  3. 鍵のアイコンを選択します
  4. デフォルトはIAM認証になっているので、もし別の認証方法を選択した場合はここで設定してください。
  5. 設定できたので、試しにlistTeamsを実行してみます
  6. 結果が表示されます。今回はまだ何もDynamoDBに登録されていないので、何も帰ってきません。

    もし認証されていない場合は、以下のようなエラーが返却されます。
    エラーの場合

@functionについて

@modelディレクティブを付けることで、create、read、update、delete、subscription の各クエリやミューテーションが自動で作成されます。
@functionは自動作成されるクエリとは別に、独自のロジックを持つクエリを手動で定義したい場合に使用します。

使用方法

  1. カスタムクエリやミューテーションを定義する
type Query {
  myCustomQuery(args: String): String
}

type Mutation {
  myCustomMutation(args: String): String 
}
  1. リゾルバーオプションを設定する
    リゾルバーオプションを設定することで、データ取得や処理の際の挙動を制御できます。
  • Lambda関数リゾルバー: Lambda関数を使用してクエリやミューテーションを処理します
  • HTTPリゾルバー: クエリやミューテーション時にHTTPエンドポイントを呼び出します
  • AppSync JavaScript または VTLリゾルバー: JavaScriptリゾルバーかVTLマッピングテンプレートを使用してロジックをカスタマイズします

Lambda関数リゾルバーの場合

まず、Lambda関数を作成します。

echofunction.js
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は公式ドキュメントを確認しながら進めることで、効率的にバックエンド構築が可能です。しかし、リレーションシップや認可設定など細かい部分では注意点も多く、実際に試行錯誤することが重要です。

よく読めばドキュメントにあったり、なかったりするので検証しながら進めるのが吉だと感じました。

参考にさせていただいた記事

https://zenn.dev/jaga/articles/dc045c6918b11b

アイレット株式会社

Discussion