Amplify を触ってみた

Amplify でチャット風なアプリケーションを書いてみました。

初期設定

  1. 現時点では Amplify は Angular 9 をサポートしていません。Angular CLI の Version 8 をインストールします。
    1
    sudo npm install @angular/cli@8 --update -g
  2. Amplify の設定を行います
    1
    amplify configure
  3. Angular のプロジェクトを作成します。
    1
    ng new amplify-test --style=scss --routing
  4. Amplify の Angular サポートをインストールします。
    1
    2
    cd amplify-test
    npm install aws-amplify aws-amplify-angular --save-dev
  5. src/polyfills.ts に以下を追加します。
    1
    2
    3
    4
    (window as any).global = window;
    (window as any).process = {
    env: { DEBUG: undefined },
    };
  6. Amplify のセットアップをします。
    1
    amplify init
  7. CloudFormation のスタックが作成されます。
  8. Amplify の機能を追加します。認証を追加します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ amplify add auth
    Using service: Cognito, provided by: awscloudformation

    The current configured provider is Amazon Cognito.

    Do you want to use the default authentication and security configuration? Default configuration
    Warning: you will not be able to edit these selections.
    How do you want users to be able to sign in? Email
    Do you want to configure advanced settings? No, I am done.
    Successfully added resource amplifytestab53a8bb locally
    ...
  9. push します。
    1
    amplify push
  10. src/tsconfig.app.json に以下を追加します。
    1
    2
    3
    "compilerOptions": {
    "types" : ["node"]
    }
  11. main.ts に以下を追加します。
    1
    2
    3
    4
    import Amplify from 'aws-amplify';
    import awsconfig from './aws-exports';

    Amplify.configure(awsconfig);
  12. src/app/app.module.ts に以下を追加します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { AmplifyAngularModule, AmplifyService } from 'aws-amplify-angular';

    @NgModule({
    ...
    imports: [
    ...
    AmplifyAngularModule
    ],
    ...
    providers: [
    ...
    AmplifyService
    ]
    ...
    });
  13. npm start してみます。

認証画面を作る

  1. 認証画面のコンポーネントを作成します。
    1
    ng g component pages/auth
  2. src/app/pages/auth/auth.component.html を以下のようにします。
    1
    <amplify-authenticator></amplify-authenticator>
  3. src/app/app.component.html を以下のようにします。
    1
    <router-outlet></router-outlet>
  4. src/app/app-routing.module.ts にパスを追加します。
    1
    2
    3
    const routes: Routes = [
    { path: 'auth', component: AuthComponent },
    ];
  5. npm start します。
  6. ブラウザで http://localhost:4200/auth にアクセスします。
  7. アカウントを作ってみます。「Create account」をクリックします。
  8. 項目を埋めます。Username はメールアドレスである必要があります。
  9. 「Create account」を押します。
  10. メールで確認コードが飛んできますのでそれを入力します。
  11. 元の画面に戻ります。
  12. メールアドレスとパスワードを入力します
  13. ログインに成功します。

トップページの作成

  1. トップページのコンポーネントを作成します。
    1
    ng g component pages/top
  2. src/app/app-routing.module.ts にパスを追加します。
    1
    2
    3
    4
    const routes: Routes = [
    ...
    { path: '', component: TopComponent },
    ];

DataStore の作成

  1. 必要なパッケージをインストールします。
    1
    2
    npx amplify-app@latest
    npm i @aws-amplify/core @aws-amplify/datastore --save-dev
  2. Amplify API を追加します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    $ amplify add api

    ? Please select from one of the below mentioned services: GraphQL
    ? Provide API name: amplifyTest
    ? Choose the default authorization type for the API Amazon Cognito User Pool
    Use a Cognito user pool configured as a part of this project.
    ? Do you want to configure advanced settings for the GraphQL API No, I am done.
    ? Do you have an annotated GraphQL schema? No
    ? Do you want a guided schema creation? Yes
    ? What best describes your project: One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
    ? Do you want to edit the schema now? Yes
    Please edit the file in your editor: /home/vagrant/amplify-test/amplify/backend/api/amplifyTest/schema.graphql
    ? Press enter to continue
  3. amplify/backend/api/amplifyTest/schema.graphql ファイルを編集します。
    1
    2
    3
    4
    5
    6
    7
    8
    type Status
    @model
    @auth(rules: [{allow: owner}]) {
    id: ID!
    posted: AWSDateTime!
    content: String!
    poster: String
    }
  4. npm run amplify-modelgen を実行します。
  5. npm run amplify-push を実行します。
  6. AppSync に API が作成されます。
  7. 投稿ボタン、削除ボタンを実装します。
    • top.component.ts
      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
      import { Component, OnInit } from '@angular/core';
      import { FormControl, FormGroup } from '@angular/forms';
      import { DataStore, Predicates } from '@aws-amplify/datastore';
      import { AmplifyService } from 'aws-amplify-angular';
      import { from } from 'rxjs';
      import { Status } from '../../../models';

      @Component({
      selector: 'app-top',
      templateUrl: './top.component.html',
      styleUrls: ['./top.component.scss']
      })
      export class TopComponent implements OnInit {

      // データ
      data = from(this.query());

      // 投稿
      fcStatus = new FormControl('');
      formGroup = new FormGroup({
      status: this.fcStatus,
      });

      constructor(
      private amplifyService: AmplifyService,
      ) { }

      ngOnInit() {
      this.subscription();
      }

      isLoggedIn(): boolean {
      return this.amplifyService.auth().user !== null;
      }

      onSubmit() {
      DataStore.save(new Status({
      content: this.fcStatus.value,
      posted: new Date().toISOString(),
      poster: this.amplifyService.auth().user.attributes.email,
      }));
      this.fcStatus.setValue('');
      }

      doDelete(id: string) {
      DataStore.query(Status as any, id).then((status) => {
      return DataStore.delete(status);
      // this.list();
      }).catch((error) => {
      console.error(error);
      });
      }

      private async query() {
      const retVal = DataStore.query(Status, Predicates.ALL).then((statuses: Status[]) => {
      statuses.sort((l, r) => {
      if (l.posted > r.posted) {
      return -1;
      } else if (l.posted < r.posted) {
      return 1;
      } else {
      return 0;
      }
      });
      return statuses;
      });
      return retVal;
      }

      private subscription() {
      DataStore.observe(Status as any).subscribe((status) => {
      this.list();
      console.log(status);
      });
      }

      private list() {
      this.data = from(this.query());
      }
      }
    • top.component.html
      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
      <div class="wrapper">
      <div class="left">
      <div class="post_box" *ngIf="isLoggedIn()">
      <form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
      <textarea placeholder="何しとんじゃわれぇ" formControlName="status"></textarea>
      <div class="post_box_action">
      <button type="submit">投稿</button>
      </div>
      </form>
      </div>
      <div *ngIf="!isLoggedIn()">
      <p>今なら無料! 今すぐ契約!!</p>
      </div>
      </div>
      <div class="right">
      <div class="contents">
      <div class="status" *ngFor="let status of data | async">
      <div class="status_poster"><span class="poster">{{status.poster}}</span></div>
      <div class="status_inner">
      <div class="status_content">{{status.content}}</div>
      <div class="status_posted">
      <a class="delete_link" href="javascript:void(0);" (click)="doDelete(status.id)">Delete</a>&nbsp;
      <span class="posted_time">{{status.posted}}</span>
      </div>
      </div>
      </div>
      </div>
      </div>
      </div>
  8. npm start で実行します。

ところが…

DynamoDB にいつまでたっても反映されません。ブラウザのコンソールを見ると以下のようなエラーが発生していました。

1
2
3
4
5
6
7
8
[WARN] 27:23.221 DataStore - Sync error subscription failed Connection
failed: {"errors":[{"message":"Validation error of type
FieldUndefined: Field '_version' in type 'Status' is undefined @
'onCreateStatus/_version'"},{"message":"Validation error of type
FieldUndefined: Field '_lastChangedAt' in type 'Status' is undefined @
'onCreateStatus/_lastChangedAt'"},{"message":"Validation error of type
FieldUndefined: Field '_deleted' in type 'Status' is undefined @
'onCreateStatus/_deleted'"}]}

作り方を変えてみる

今まで作ったものを捨てて作り方を変えてみます。

  1. まず amplify delete で CloudFormation スタックを削除します。
  2. プロジェクトを作成します。
    1
    2
    3
    4
    ng new sakai-amplify-test --style=scss --routing
    cd sakai-amplify-test
    npx amplify-app@latest
    npm i @aws-amplify/core @aws-amplify/datastore --save-dev
  3. Amplify Auth を追加します。
    1
    2
    amplify init
    amplify add auth
  4. amplify/backend/api/amplifyDatasource/schema.graphql を編集します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    type Status
    @model
    @auth(
    rules: [
    {
    allow: owner,
    operations: [create, update, delete, read]
    }
    ])
    {
    id: ID!
    posted: AWSDateTime!
    content: String!
    poster: String!
    }
  5. モデルを生成します。
    1
    2
    amplify update api
    npm run amplify-modelgen
  6. npm install aws-amplify aws-amplify-angular @aws-amplify/ui --save-dev
  7. amplify push を実行します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $ amplify push
    ✔ Successfully pulled backend environment dev from the cloud.

    Current Environment: dev

    | Category | Resource name | Operation | Provider plugin |
    | -------- | ------------------------ | --------- | ----------------- |
    | Api | amplifyDatasource | Create | awscloudformation |
    | Auth | sakaiamplifytestcce6b8e1 | Create | awscloudformation |
    ? Are you sure you want to continue? Yes

    GraphQL schema compiled successfully.

    Edit your schema at /home/vagrant/sakai-amplify-test/amplify/backend/api/amplifyDatasource/schema.graphql or place .graphql files in a directory at /home/vagrant/sakai-amplify-test/amplify/backend/api/amplifyDatasource/schema
    ? Do you want to generate code for your newly created GraphQL API Yes
    ? Choose the code generation language target angular
    ? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.graphql
    ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
    ? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
    ? Enter the file name for the generated code src/app/service/api.service.ts
    ディレクトリを掘らなかったので src/app/service/api.service.ts の作成に失敗しました。
  8. 古いのから移植します。
  9. 起動します。
    1
    npm start
  10. ユーザーを作成します。
  11. 今度はうまくいきました。

まとめ

  • 単純なシステムであれば、簡単に実装することができる。
  • スキーマ設計は GraphQL だけで行うので、DynamoDB 側のことは意識する必要がなくなる。
  • 自動的に CloudFormation スタックが構築されるので、システム開発に集中できる。ただし、インフラの細かい制御ができるかは不明。
  • DataStore はオフラインでも動作するのでスマホサイトには最適
  • プロジェクトの作り方に気を付けないとハマる。
  • バックエンドがシンプルになる分、フロントエンドの負荷が高くなる。
  • フロントエンドとバックエンドの両方から DynamoDB を触りたい場合、どう設計するべきか ?
  • ログインが遅い。
  • データのオーナーが自動的に設定されるせいか、チャットアプリにならなかった…

参考