はじめに
これまでの全てのオペレーション(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_asset
aメソッドで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_template
とresponse_mapping_template
のプロパティを使う- JSリゾルバーとは使うプロパティが異なる(
runtime
,code
プロパティは使わない!)
- JSリゾルバーとは使うプロパティが異なる(
単純に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では両方の実装がありそうなので、これを参考にするとよいかもしれない。