AppSyncで自分のデータだけSubscription

# はじめに

AppSyncのサンプルアプリとかはあちこちありますが、**自分のデータだけ** Subscriptionする方法が、最初取り組んだときに分かりにくかったので、ここにまとめます。

# サンプル

チャットアプリを考えます。
ここでは簡単のために、1対1のチャットを考えます。
以下は、まだSubscriptionを定義していないサンプルスキーマです。

```graphql:schema.graphql
type ChatRoom {
  id: ID!
  userIds: [Int!]!
}

type Message {
  chatRoomId: ID!
  fromUserId: Int!
  sendTimestamp: AWSTimestamp!
  content: AWSJSON!
}

type Mutation {
  addMessage(chatRoomId: ID!, toUserId: Int!, content: AWSJSON!): Message!
}

type Query {
  # 本当はページネーションとか考慮いるけど省略
  chatRooms: [ChatRoom!]!
  messages(chatRoomId: ID!):[Message!]!
}

schema {
  query: Query
  mutation: Mutation
}
```

## Subscriptionの設定

チャット相手がメッセージを送信したことを検知できるように、addMessageをSubscriptionします。

### とりあえずSubscription定義

```graphql
type Subscription {
  onAddMessage: Message @aws_subscribe(mutations: ["addMessage"])
}
```

とりあえずsubscriptionを定義しました！！
ただこの状態だと、自分のデータだけでなく他人のデータを含む全てのメッセージをSubscriptionします😆😆😆

### Subscriptionの引数を定義

自分のデータだけSubscriptionするようにしましょう。以下はスキーマの関連する部分を抜き出しています。

```graphql
type Message {
  chatRoomId: ID!
  toUserId: Int! # 追加！！
  fromUserId: Int!
  sendTimestamp: AWSTimestamp!
  content: AWSJSON!
}

type Mutation {
  addMessage(chatRoomId: ID!, toUserId: Int!, content: AWSJSON!): Message!
}

type Subscription {
  onAddMessage(toUserId: Int!): Message @aws_subscribe(mutations: ["addMessage"])
}
```

AppSyncでは、Subscriptionに引数を追加することで、特定のデータだけSubscriptionする仕組みがあります。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/real-time-data.html#using-subscription-arguments

ここでは「自分に送信された」メッセージだけSubscriptionしたいので、引数に`toUserId`を追加します。
さらに、`toUserId`で絞り込むためには、MutationのaddMessageの戻り値に`toUserId`を含む必要があります。
（1対1のチャットを前提としているので、MessageはfromUserIdだけあれば問題ないのですが、SubscriptionのためにtoUserIdを冗長に持たせています。改善の余地はありますが、とりあえず説明用のサンプルとして）

これで送信先が一致する場合のみSubscriptionできるようになりました！！

が、まだダメです、、toUserIdに自分のIDではなく他人のIDを指定した場合、他人のメッセージをSubscriptuonできちゃいます😆😆😆

### AppSync側でtoUserIdを確認する

ここでは、AppSyncのAPIはCognito認証が必要であり、Cogniteユーザの属性に自分のuserIdを含む前提となります。

まずは、Subscription（onAddMessage）にリゾルバーをアタッチします。

<img width="485" alt="Screen Shot 2019-12-06 at 23.44.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322301/c5bd1897-69f4-3b08-19a2-93e070cb800b.png">

リゾルバーのリクエストマッピングテンプレートは何もする必要はありません。

レスポンスマッピングテンプレートで、
Subscriptionの引数で指定されたtoUserIdと、認証トークンに含まれるuserIdが一致するか確認し、不一致の場合は認可エラーを返します。

<img width="793" alt="Screen Shot 2019-12-06 at 23.45.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322301/af9959e6-b684-b081-4786-c16c0e09b847.png">

toUserIdに自分以外のidを設定した場合は、エラーを返すようになりました。
これで完成です！！

参考：https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/security-authorization-use-cases.html#real-time-data

# まとめ

Subscriptionを含めたスキーマ全体です。

```graphql:schema.graphql
type ChatRoom {
  id: ID!
  userIds: [Int!]!
}

type Message {
  chatRoomId: ID!
  toUserId: Int!
  fromUserId: Int!
  sendTimestamp: AWSTimestamp!
  content: AWSJSON!
}

type Mutation {
  addMessage(chatRoomId: ID!, toUserId: Int!, content: AWSJSON!): Message!
}

type Query {
  chatRooms: [ChatRoom!]!
  messages(chatRoomId: ID!):[Message!]!
}

type Subscription {
  onAddMessage(toUserId: Int!): Message @aws_subscribe(mutations: ["addMessage"])
}

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}
```

↓レスポンスマッピングテンプレートのコピペ用

```
#if($context.identity.claims.get("custom:user_id") != \${context.arguments.toUserId})
  $utils.unauthorized()
#else
  null
#end
```