CDKでAppSyncのユニットリゾルバーをJavaScriptで実装してみた

AWS

はじめに

これまでの全てのオペレーション(Query,Mutation)はLambdaリゾルバーをアタッチする実装をしていた。

今回、DynamoDBへシンプルにget/putするだけの新規オペレーションを実装ことになったのでJavaScriptリゾルバーで実装してみた。

ざっくりというと、DynamoDBをデータソースとしてRuntimeがJavaScriptのユニットリゾルバーを作成した。

コンソールから作成する際は特に躓く点はなかったが、CDK(Cfn)で実装するときにVTLとは違うパラメータを使う必要があって躓いた。

厳密には「JavaScriptリゾルバー」という単語が適切なのかは怪しいと思っている。上記のとおりでユニットリゾルバーのRuntimeが APPSYNC_JS (JavaScript)であるだけなのでちょっと違和感がある。まぁ、AWSドキュメントでJavaScript resolvers overview という記載があるので問題ないと思うが。

実装

CDK(python)のコード

from aws_cdk import (
    Stack,
    aws_appsync as appsync,
    aws_dynamodb as dynamodb,
)
from constructs import Construct

class CdkAppsyncJsResolverStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        ddb = dynamodb.Table(self, "Table",
                             partition_key=dynamodb.Attribute(
                                 name="id", type=dynamodb.AttributeType.STRING),
                             )

        api = appsync.GraphqlApi(self, "Api",
                                 name="Api",
                                 definition=appsync.Definition.from_file(
                                     "graphql/schema.graphql"),
                                 )
				# 1️⃣ DDBのデータソースをL2で定義
        ddb_ds = api.add_dynamo_db_data_source("DataSource", ddb)
        # 2️⃣ L2データソースのメソッドでJSリゾルバーを定義
        ddb_ds.create_resolver(
            'getDemo20240709Resolver',
            type_name="Query",
            field_name="getDemo20240709",
            runtime=appsync.FunctionRuntime.JS_1_0_0,
            code=appsync.Code.from_asset("graphql/Query.getDemo20240709.js"),
        )

        ddb_ds.create_resolver(
            'createDemo20240709Resolver',
            type_name="Mutation",
            field_name="createDemo20240709",
            runtime=appsync.FunctionRuntime.JS_1_0_0,
            code=appsync.Code.from_asset(
                "graphql/Mutation.createDemo20240709.js"),
        )

ポイント解説)

  • runtime プロパティは JS_1_0_0
  • code プロパティは from_assetaメソッドでCDKプロジェクトからの相対パスでJavaScriptリゾルバーのプログラムファイルを指定

余談)

大量のリゾルバーを定義する際は、 create_resolver メソッドをラップしたファクトリのようなものを定義して、各リゾルバーのパラメータを定義したイテレーターをループさせたほうがよいかもしれない。

  • L2メソッドが隠蔽されるため、人によっては分かりづらいと思う(個人的にはシンプルなものなら良いと思うが複雑になると微妙に感じる)
  • 作成されるコンストラクタが2つ以上になるならば、素直にカスタムコンストラクタにしたい派
    • だがしかしカスタムコンストラクタを乱用すると、リファクタ時などにネームスペースが一致しなくなり別リソース(replaceがおきる)になることが起きるケースが発生しがちなので要注意

VTLリゾルバーとの違いよる躓きポイント

L1での実装例になるが、VTLの実装は下記のようになる。

一部抜粋


        fields = [
            ('createDemo20240709', 'Mutation'),
            ('getDemo20240709', 'Query'),
        ]

        for field in fields:
            # リクエストテンプレートの読み込み
            with open(os.path.join(os.path.dirname(__file__), f"resolver/{field[0]}-request-template.vtl"), "r") as file:
                request_template = file.read()

            # レスポンステンプレートの読み込み
            with open(os.path.join(os.path.dirname(__file__), f"resolver/{field[0]}-response-template.vtl"), "r") as file:
                response_template = file.read()

            # リゾルバーの作成
            resolver = appsync.CfnResolver(
                self, f"{field[0]}Resolver",
                api_id=graphql.graphQLApi.attr_api_id,
                type_name=field[1],
                field_name=field[0],
                data_source_name=graphql.datasource.name,
                request_mapping_template=request_template,
                response_mapping_template=response_template
            )

ポイント解説)

  • VTLの場合は request_mapping_templateresponse_mapping_templateのプロパティを使う
    • JSリゾルバーとは使うプロパティが異なる( runtime, codeプロパティは使わない!)

単純にRuntimeがVTLかJSの差であるのに、使用するプロパティが全然違うので分かりづらかった。

余談)

上記コードだと、リゾルバーのidとしてフィールド名しか使ってないため、 Type で定義したオブジェクトの任意の fieldにリゾルバーをアタッチしたいときに、リソースidの重複が発生する可能性があるため微妙なコードである。

例えば、Typeで定義されたTeacherとStudentの2つがあったときに、両方のnameフィールドに対してリゾルバーをアタッチできない。なぜなら、両方ともリソースidが nameResolverとなり重複するから。

パラメータのリストであるfieldsは各パラメーターの量を考慮して、型付のクラスなどに置き換えると良いと思う。

pythonでいうと、 list[AppSyncUnitResolverOfDdb] みたいなイメージ。

JavaScriptリゾルバーのコード

このコードは参考程度にしてほしい。

Query.getDemo20240709.js

import { util } from "@aws-appsync/utils";
import { get } from "@aws-appsync/utils/dynamodb";

/**
 * Sends a request to get an item with id `ctx.args.id` from the DynamoDB table.
 * @param {import('@aws-appsync/utils').Context<{id: unknown;}>} ctx the context
 * @returns {import('@aws-appsync/utils').DynamoDBGetItemRequest} the request
 */
export function request(ctx) {
  const { id } = ctx.args;
  const key = { id };
  return get({
    key,
  });
}

/**
 * Returns the fetched DynamoDB item.
 * @param {import('@aws-appsync/utils').Context} ctx the context
 * @returns {*} the DynamoDB item
 */
export function response(ctx) {
  const { error, result } = ctx;
  if (error) {
    return util.appendError(error.message, error.type, result);
  }
  return result;
}

Mutation.createDemo20240709.js

import { util } from "@aws-appsync/utils";
import { put } from "@aws-appsync/utils/dynamodb";

/**
 * Puts an item into the DynamoDB table.
 * @param {import('@aws-appsync/utils').Context<{input: any}>} ctx the context
 * @returns {import('@aws-appsync/utils').DynamoDBPutItemRequest} the request
 */
export function request(ctx) {
  const { id } = ctx.args.input;
  const key = { id };
  const condition = { and: [] };
  for (const k in key) {
    condition.and.push({ [k]: { attributeExists: false } });
  }

  return put({
    key,
    item: ctx.args.input,
    condition,
  });
}

/**
 * Returns the item or throws an error if the operation failed.
 * @param {import('@aws-appsync/utils').Context} ctx the context
 * @returns {*} the result
 */
export function response(ctx) {
  const { error, result } = ctx;
  if (error) {
    return util.appendError(error.message, error.type, result);
  }
  return result;
}

conditionの実装がよくない。

上記の例だと、素直に {id: {attributeExists: false }} と記載するのもあり。

今の実装の意図しては汎用性をあげるための実装になっている。

しかしfor~inで実装するとプロトタイプの考慮などが必要になるので、Object.keysを使うほうがよさそう。

payloadのデータ構造をシンプルにしたくて下記のようにしているが

  const { id } = ctx.args.input;
  const key = { id };

paylaodをこのようにすると、リゾルバーのコードはもっとスッキリするはず。

// リゾルバーのすっきりさせるpaylaod
{
	key: {id: 123},
	item: {foo: 1, bar: 2},
}

// payloadの構造がシンプル
{
	id: 123,
	foo: 1,
	bar: 2
}

ちなみに、 import { put } from "@aws-appsync/utils/dynamodb";は一番抽象度を高く実装できるユーティリティ。

ドキュメントが充実していないと思っているのでそこが欠点。

おわりに

単純なCRUD処理でもリッチなバリデーションの要件がでてくると、さらに辛みがでてくると思うので

本格的にJSリゾルバーを運用していくのはハードルが高そうだと感じている。

またJavascriptの構文も対応してないものがあった気がするし、聞いた記憶もあるのでそこも懸念点である。

CDKのJSリゾルバーをググると、基本的なミューテーションリゾルバーの作成などでもパイプラインリゾルバーでの実装が紹介されていて、感覚的にはユニットリゾルバーの実装が少なかった。

aws-appsync-resolver-samplesでは両方の実装がありそうなので、これを参考にするとよいかもしれない。

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!