【Amplify Gen2】リアルタイムサブスクリプションで発生する"Cannot read properties of null (reading 'id')"のエラー対応方法

【Amplify Gen2】リアルタイムサブスクリプションで発生する"Cannot read properties of null (reading 'id')"のエラー対応方法

Clock Icon2024.12.25

はじめに

コンサル部の神野です。

Amplify関連の検証を進めていた際に、画面側でリアルタイムサブスクリプションがうまくいかず、下記エラーが発生したことがありました。

Uncaught TypeError: Cannot read properties of null (reading 'id')

一般的なエラーメッセージであまり情報もなく、何が原因なんだろうと思い調べて検証していたところ解決したので、共有させていただきます。

前提

今回もリアルタイムサブスクリプション実装の記事をベースに話を進めていきます。
記事やレポジトリのリンクを下記に記載しているので必要に応じてご参照ください。

リアルタイムサブスクリプション実装の記事

https://dev.classmethod.jp/articles/amplify-gen2-realtime-subscription/

レポジトリ

https://github.com/yuu551/amplify-realtime-subscription-blog

まず原因

原因として自分で作成したMutationが適切ではなかったというだけでした。
Amplify側で設定されるidcreatedAtupdatedAtも返却するようMutationに含める必要があります。

自分が実行したMutation

自分が実行したMutation
mutation CreateDeviceStatus($input: CreateDeviceStatusInput!) {
    createDeviceStatus(input: $input) {
      id
      device_Id
      humidity
      temperature
      voltage
      last_updated
      status_code
      status_description
      status_state
    }
  }

正しいMutation

正しいMutation
mutation CreateDeviceStatus($input: CreateDeviceStatusInput!) {
    createDeviceStatus(input: $input) {
      id
      device_Id
      humidity
      temperature
      voltage
      last_updated
      status_code
      status_description
      status_state
+     createdAt
+     updatedAt
    }
  }

差分としてはAmplify側で自動設定されるcreatedAtupdatedAtを含めているかどうかです。
ただ、どちらのMutationもエラーなく実行されるのにどうして画面側でエラーが出てしまうのかと気になり深掘りしていきたいと思います。

調査

Amplifyで設定するデータの定義は下記の通りとします。

データ定義

データ定義
const schema = a
  .schema({
    DeviceStatus: a
      .model({
        device_Id: a.string(),
        status_code: a.string(),
        status_state: a.string(),
        status_description: a.string(),
        temperature: a.float(),
        humidity: a.float(),
        voltage: a.string(),
        last_updated: a.string(),
      })
  })
  .authorization((allow) => [allow.authenticated()]);

Mutation実行

定義に従ってidcreatedAtupdatedAtなどは一旦気にせず、下記Mutationをコンソール上から実行してみます。

実行するMutation
mutation MyMutation {
  createDeviceStatus(input: {device_Id: "device_003", humidity: 45.7, last_updated: "2024-01-20T15:30:22Z", status_code: "200", status_description: "Normal operation", status_state: "ACTIVE", temperature: 23.4, voltage: "12.3"}) {
    device_Id
    humidity
    last_updated
    status_code
    status_description
    status_state
    temperature
    voltage
  }
}

実行結果

エラーなく実行され、登録されたデータが返却されました。

実行結果
{
  "data": {
    "createDeviceStatus": {
      "device_Id": "device_003",
      "humidity": 45.7,
      "last_updated": "2024-01-20T15:30:22Z",
      "status_code": "200",
      "status_description": "Normal operation",
      "status_state": "ACTIVE",
      "temperature": 23.4,
      "voltage": "12.3"
    }
  }
}

DynamoDBにもデータが登録されているか確認してみます。

CleanShot 2024-12-24 at 21.55.35@2x

DynamoDBへ登録されたデータ
{
  "id": {
    "S": "89de53de-2536-401a-8073-c5012e40c212"
  },
  "createdAt": {
    "S": "2024-12-24T12:54:43.364Z"
  },
  "device_Id": {
    "S": "device_003"
  },
  "humidity": {
    "N": "45.7"
  },
  "last_updated": {
    "S": "2024-01-20T15:30:22Z"
  },
  "status_code": {
    "S": "200"
  },
  "status_description": {
    "S": "Normal operation"
  },
  "status_state": {
    "S": "ACTIVE"
  },
  "temperature": {
    "N": "23.4"
  },
  "updatedAt": {
    "S": "2024-12-24T12:54:43.364Z"
  },
  "voltage": {
    "S": "12.3"
  },
  "__typename": {
    "S": "DeviceStatus"
  }
}

idcraetedAtupdatedAtも登録されていますね。
AppSync のリゾルバーで実行される VTL(Velocity Template Language)スクリプトによって処理され、DynamoDB に保存される前に自動的に付与されます。

補足 VTLスクリプト

## [Start] Initialization default values. **
$util.qr($ctx.stash.put("defaultValues", $util.defaultIfNull($ctx.stash.defaultValues, {})))
$util.qr($ctx.stash.defaultValues.put("id", $util.autoId()))
#set( $createdAt = $util.time.nowISO8601() )
$util.qr($ctx.stash.defaultValues.put("createdAt", $createdAt))
$util.qr($ctx.stash.defaultValues.put("updatedAt", $createdAt))
$util.toJson({
  "version": "2018-05-29",
  "payload": {}
})
## [End] Initialization default values. **

画面側のエラーログ

一方で画面側を見てみると下記エラーが発生して画面が更新されていませんでした。
Amplify内部のエラーが発生していて、リアルタイムサブスクリプションの実装箇所まで到達していないような動きをしていました。

CleanShot 2024-12-24 at 21.57.56@2x

同じような原因に直面している方はいないかと調べていたところ下記Issueで議論がされていました。

https://github.com/aws-amplify/amplify-category-api/issues/2552

これをみると、同じようなエラーだがちょっと違う事象なのかなーと思っていたところ下記一文が気になりました。

The errors were essentially that createdAt, updatedAt, owner can't be null.

その方の場合は、自作のMutationのクエリにcreatedAtupdatedAtownerも返却する必要があったそうです(idについては既に追加済みでした)。

この情報を参考に、私も検証を進めてみました。まず全てのカラム(idcreatedAtupdatedAtも)を返却するMutationを実行し、その後、一つずつカラムを除外していく形で検証を行いました。

その結果、以下の3つのカラムが1つでも返却しない場合にのみ、タイトルのエラーが発生し、Amplify側内部処理で使用する必要なカラムということがわかりました。

  • id
  • createdAt
  • updatedAt

自分で定義したカラム(device_Idtemperatureなど)については、Mutationで返却するよう含めなくてもエラーは発生せず、フロント側のリアルタイムサブスクリプションは正常に動作しました。

補足:ownerについて

ownerについては今回の設定では、データの所有者のみアクセスではなく認証されたユーザー全員アクセス可としているので、カラムとして存在しない状態でした。下記設定のようにデータの所有者のみアクセスとすると、別途カラムにデータが登録されてデータを返却するよう考慮する必要があるかと思います。

データ定義
const schema = a
  .schema({
    DeviceStatus: a
      .model({
        device_Id: a.string(),
        status_code: a.string(),
        status_state: a.string(),
        status_description: a.string(),
        temperature: a.float(),
        humidity: a.float(),
        voltage: a.string(),
        last_updated: a.string(),
      })
  })
- .authorization((allow) => [allow.authenticated()]);
+ .authorization((allow) => [allow.owner()]);

まとめ

今までの結論をまとめると下記の通りとなります。

  • エラーの原因は、Amplifyが自動生成する必須フィールドが自作のMutationで返却しない作りであった
  • idcreatedAtupdatedAtをMutationに返却するよう追加しエラーは解消
  • これらのフィールドはAmplifyの内部処理に必要で存在しない場合はサブスクリプションに失敗する

おわりに

簡単ではありますが、一般的なエラーメッセージで情報が少なかったため、本記事を共有させていただきました。

少しでも本記事が参考になりましたら幸いです!
最後までご覧いただきありがとうございました!!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.