Amplify Tips解説 (Custom Resolver/テスト&タグ管理)

[AWS Amplify Advent Calendar 2024](https://qiita.com/advent-calendar/2024/amplify) の8日目の記事です！


# この記事 is 何

Amplifyを実装している中で使ったTipsを簡単にまとめる記事です。
テーマは以下の2つです。

1. バックエンドのCustom Resolverの実装&テスト
2. Amplify生成リソースのタグ管理


# 1. バックエンドのCustom Resolverの実装&テスト

Amplify Gen2からバックエンドのAPIのCustom Resolverを簡単に実装できるようになりました。
バックエンドのAPIにバリデーションなどの実装を追加する場合にはCustom Resolverを利用します。

参考:
https://docs.amplify.aws/react/build-a-backend/data/custom-business-logic/#step-2---configure-custom-business-logic-handler-code

例えば、郵便番号を受け取って保存するAPIを想定したときに、リクエストされるデータは「半角数字7桁」であることを強制したりするときに利用します。

## 実装方法

使い方は簡単で、backendのスキーマにCustom Resolverとなるjsのコードを参照するような実装を追記します。

```ts:resource.ts
const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
  createNumber7Todo: a // 7桁の10進数を表す文字列を受け取るCustom Resolverをエンドポイントに紐づける
    .mutation()
    .arguments({
      content: a.string(),
    })
    .returns(a.ref("Todo"))
    .authorization((allow) => [allow.publicApiKey()])
    .handler(a.handler.custom({
      dataSource: a.ref('Todo'),
      entry: './createNumber7.js'
    }))
});
```

あとはCustom Resolverとなるjsファイルを作成し、リクエストで受け取った値をutilsライブラリを使ってチェックをかけたりすることができます。


```ts:createNumber7.js
import { util } from '@aws-appsync/utils';

export function request(ctx) {
    const { content } = ctx.args;
    
    // 半角数字7桁かどうかをチェック
    if (content.length !== 7 || !util.matches('^[0-9]{7}$', content)) {
        return util.error('Invalid input: content must be a 7-digit number');
    }

    const now = util.time.nowISO8601();
    return {
        operation: 'PutItem',
        key: util.dynamodb.toMapValues({
            id: util.autoId()
        }),
        attributeValues: util.dynamodb.toMapValues({
            content,
            createdAt: now,
            updatedAt: now
        })
    };
}

export function response(ctx) {
  return ctx.result
}
```

utilsライブラリは他にも色々なmatcherなどの実装があるので、それらを利用しながらより複雑な実装をすることもできます。

https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-util-reference.html


## テストの実装方法

Custom Resolverでロジックの実装をする場合、きちんとテストを書きたくなります。

バックエンドに対するAPIレベルでのテストを実施するのも良いですが、ここではSmallなテストをする場合のサンプルを書いておきます。

こちらのGitHubを参考にさせていただいています！
https://github.com/naedx/amplify-playground/tree/dev/projects/amplify-appsync-vitest

基本的には `@aws-appsync/utils` をモックすることでSmallテストを実装します。
郵便番号のInputに関するテストは以下のように実装することでテストコードがかけます。

```ts
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { request } from './amplify/data/createNumber7Todo';
import { util } from '@aws-appsync/utils';

// util のモック
vi.mock('@aws-appsync/utils', () => ({
  util: {
    matches: vi.fn(),
    time: {
      nowISO8601: vi.fn().mockReturnValue('2024-03-20T00:00:00Z'),
    },
    autoId: vi.fn().mockReturnValue('mock-id'),
    dynamodb: {
      toMapValues: vi.fn(x => x),
    },
    error: vi.fn(msg => ({ message: msg })),
  },
}));

describe('createNumber7', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('request', () => {
    test('正常な7桁の数字を受け付ける', () => {
      util.matches.mockReturnValue(true);
      const ctx = {
        args: {
          content: '1234567',
        },
      };

      const result = request(ctx);

      expect(result).toEqual({
        operation: 'PutItem',
        key: {
          id: 'mock-id',
        },
        attributeValues: {
          content: '1234567',
          createdAt: '2024-03-20T00:00:00Z',
          updatedAt: '2024-03-20T00:00:00Z',
        },
      });
    });

    test('7桁未満の数字はエラーを返す', () => {
      util.matches.mockReturnValue(false);
      const ctx = {
        args: {
          content: '123456',
        },
      };

      const result = request(ctx);

      expect(result).toEqual({
        message: 'Invalid input: content must be a 7-digit number',
      });
    });

    test('7桁を超える数字はエラーを返す', () => {
      util.matches.mockReturnValue(false);
      const ctx = {
        args: {
          content: '12345678',
        },
      };

      const result = request(ctx);

      expect(result).toEqual({
        message: 'Invalid input: content must be a 7-digit number',
      });
    });

    test('数字以外の文字を含む場合はエラーを返す', () => {
      util.matches.mockReturnValue(false);
      const ctx = {
        args: {
          content: '123a567',
        },
      };

      const result = request(ctx);

      expect(result).toEqual({
        message: 'Invalid input: content must be a 7-digit number',
      });
    });
  });
});
```



# 2.Amplify生成リソースのタグ管理


Amplifyを使うといろんなAWSリソースがバンバン作られていきます。
1つのAWSアカウントで複数のAmplifyプロジェクトを利用する場合、似たようなリソースがたくさんできてしまい、コスト管理などに支障をきたします。

その対策として、タグ付けをしてリソース管理を実現できるようにします。

## 実装方法

やり方自体は簡単で、backendリソースを管理しているCDKにタグのコードを追記するだけです。
authやdataなどのリソース事に細かくタグ付けを分けることもできます。


```ts:backend.ts
import { Tags } from 'aws-cdk-lib';
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';

const backend = defineBackend({
  auth,
  data,
});


// タグをリソース毎に追加
const backendTags = Tags.of(backend.stack);
backendTags.add('project', 'amplify-test-app');

const authTags = Tags.of(backend.auth.stack);
authTags.add('service', 'amplify-test-app-auth');

const dataTags = Tags.of(backend.data.stack);
dataTags.add('service', 'amplify-test-app-data');

```

結果はこんな感じになります！

![タグついた画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114040/523ee75e-8a3e-05e2-51c5-ded0a79da693.png)



参考:
https://docs.amplify.aws/react/build-a-backend/add-aws-services/tagging-resources/



---

以上です！
なにかのお役に立てば幸いです。

こういった発信等、Xで行っていますのでよろしければフォローいただけると嬉しいです🙇

https://x.com/mot0aki