リゾルバーの上書きとカスタマイズ
リゾルバーの上書き
シンプルな schema.graphql があるとします。
type Todo @model { id: ID! name: String! description: String}プロジェクトがコンパイルされるときに生成される Query.getTodo リゾルバーのリクエスト マッピング テンプレートの動作を変更したい場合、API プロジェクトの resolvers ディレクトリに Query.getTodo.req.vtl という名前のファイルを作成します。次に amplify push または amplify api gql-compile を実行すると、自動生成されたテンプレートの代わりにリゾルバー テンプレートが使用されます。同様に Query.getTodo.res.vtl ファイルを作成して、リゾルバーのレスポンス マッピング テンプレートの動作を変更することができます。
カスタムリゾルバー
生成されたリゾルバーでユースケースがカバーされていない場合、カスタム Query、Mutation、Subscription を追加できます。
- スキーマに必要な
Query、MutationまたはSubscriptionタイプを追加します。 - 新しく作成した
Query、MutationまたはSubscription用のリゾルバーを作成するには、<project-root>/amplify/backend/api/<api-name>/resolversフォルダー内にリクエストおよびレスポンス テンプレートを作成します。GraphQL Transformer は<TypeName>.<FieldName>.<req/res>.vtlという規則に従ってリゾルバーに名前を付けます。したがって、カスタム クエリmyCustomQueryを追加する場合、リゾルバーはQuery.myCustomQuery.req.vtlおよびQuery.myCustomQuery.res.vtlという名前になります。 - API の
<project-root>/amplify/backend/api/<api-name>/stacksディレクトリ内にカスタム スタックを作成してリゾルバー リソースを追加します。
カスタム フィールドを追加するには、スキーマに次を追加します。
# <project-root>amplify/backend/api/<api-name>/schema.graphql
type Query { # ここにすべてのカスタム クエリを追加します }
type Mutation { # ここにすべてのカスタム ミューテーションを追加します }
type Subscription { # ここにすべてのカスタム サブスクリプションを追加します }GraphQL Transformer は、デフォルトで CustomResources.json というファイルを <project-root>/amplify/backend/api/<api-name>/stacks 内に作成します。このファイルは、新しく追加された Query、Mutation または Subscription のカスタムリゾルバーを追加するために使用できます。カスタム スタックには、API に関する詳細を取得できるように、以下の引数が渡されます。
| パラメーター | タイプ | 可能な値 | 説明 |
|---|---|---|---|
| AppSyncApiId | String | このプロジェクトに関連付けられた AppSync API の ID | |
| AppSyncApiName | String | AppSync API の名前 | |
| env | String | 環境名 | |
| S3DeploymentBucket | String | プロジェクトのすべてのデプロイ アセットを含む S3 バケット | |
| S3DeploymentRootKey | String | S3DeploymentBucket を基準とした S3 キー。デプロイ ディレクトリのルートを指します。 | |
| DynamoDBEnableServerSideEncryption | String | true または false | KMS により提供されるサーバー側暗号化を有効にします。 |
| AuthCognitoUserPoolId | String | 接続する既存のユーザー プールの ID | |
| DynamoDBModelTableReadIOPS | Number | テーブルがサポートする読み取り IOPS の数 | |
| DynamoDBModelTableWriteIOPS | Number | テーブルがサポートする書き込み IOPS の数 | |
| DynamoDBBillingMode | String | PAY_PER_REQUEST または PROVISIONED | @model タイプを設定して、DynamoDB テーブルを PAY_PER_REQUEST または PROVISIONED 課金モードで作成します |
| DynamoDBEnablePointInTimeRecovery | String | true または false | テーブルでポイント イン タイム リカバリを有効にするかどうか |
| APIKeyExpirationEpoch | Number | API キーが期限切れになるべきエポック時刻 (秒単位) | |
| CreateAPIKey | Number | 0 または 1 | API キーを作成するかどうかを制御するブール値。プロパティの値は CLI によって自動的に設定されます。値が 0 に設定されている場合、API キーは作成されません |
カスタム スタックに追加された追加の値は、ルート スタックのパラメーターとして公開され、値は <project-root>/amplify/backend/api/<api-name>/parameters.json ファイルに値を追加することで設定できます。
カスタムリゾルバーを追加するには、CustomResource.json のリソース セクションに以下を追加します。
{ "Resources": { "CustomQuery1": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": "CommentTable", "TypeName": "Query", "FieldName": "myCustomQuery", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.myCustomQuery.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.myCustomQuery.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } }}リクエストおよびレスポンス テンプレートは <project-root>/amplify/backend/api/<api-name>/resolvers フォルダー内に配置する必要があります。リゾルバー テンプレートは Apache Velocity Template Language (通常は VTL と呼ばれます) で記述されます。Query.myCustomQuery.req.vtl はリクエスト マッピング テンプレートであり、AppSync からの受信リクエストを受け取って、その後 GraphQL リゾルバーに渡される JSON ドキュメントに変換します。同様に、Query.myCustomQuery.res.vtl はレスポンス マッピング テンプレートです。これらのテンプレートは GraphQL リゾルバーのレスポンスを受け取り、ユーザーに返す前にデータを変換します。
後でこのドキュメントで説明する VTL ファイルの例がいくつかあります。VTL の詳細情報、および GraphQL リゾルバーのコンテキストで VTL を使用する方法については、公式の AppSync リゾルバー マッピング テンプレート リファレンス を参照してください。
@model の DynamoDB テーブルをターゲットにするカスタムリゾルバーを追加する
これは、@model で作成された DynamoDB テーブルに対してより具体的なクエリを記述したい場合に便利です。たとえば、2 つの @model タイプと @connection ディレクティブのペアを持つこのスキーマがあるとします。
type Todo @model { id: ID! name: String! description: String comments: [Comment] @connection(name: "TodoComments")}type Comment @model { id: ID! content: String todo: Todo @connection(name: "TodoComments")}このスキーマは、Query.getTodo、Query.listTodos、Query.getComment、Query.listComments のトップ レベルのリゾルバーを生成し、@connection を実装するための Todo.comments および Comment.todo のリゾルバーも生成します。内部的には、トランスフォーマーは Comment テーブル上の DynamoDB グローバル セカンダリ インデックスを作成しますが、Query.getTodo.comments クエリ パスを介して特定の Todo オブジェクトのコメントをフェッチできるため、GSI をクエリするトップ レベルのクエリ フィールドは生成されません。Query.commentsForTodo などのトップ レベルのクエリ フィールドを介して Todo オブジェクトのすべてのコメントをフェッチしたい場合は、以下を実行します。
- 目的のフィールドを schema.graphql に追加します。
# ... 上記の Todo および Comment タイプ
type CommentConnection { items: [Comment] nextToken: String}type Query { commentsForTodo(todoId: ID!, limit: Int, nextToken: String): CommentConnection}- リゾルバー リソースを stacks/ ディレクトリ内のスタックに追加します。
DataSourceNameは自動生成されます。ほとんどの場合、{MODEL_NAME}Tableのようになります。データ ソース名を確認するには、AppSync コンソール (amplify console api) 内から検証し、Data Sources タブをクリックします。
{ // ... テンプレートの残りの部分 "Resources": { "QueryCommentsForTodoResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": "CommentTable", "TypeName": "Query", "FieldName": "commentsForTodo", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.commentsForTodo.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.commentsForTodo.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } }}- リゾルバー テンプレートを記述します。
## Query.commentsForTodo.req.vtl **
#set( $limit = $util.defaultIfNull($context.args.limit, 10) ){ "version": "2017-02-28", "operation": "Query", "query": { "expression": "#connectionAttribute = :connectionAttribute", "expressionNames": { "#connectionAttribute": "commentTodoId" }, "expressionValues": { ":connectionAttribute": { "S": "$context.args.todoId" } } }, "scanIndexForward": true, "limit": $limit, "nextToken": #if( $context.args.nextToken ) "$context.args.nextToken" #else null #end, "index": "gsi-TodoComments"}## Query.commentsForTodo.res.vtl **
$util.toJson($ctx.result)AWS Lambda 関数をターゲットにするカスタムリゾルバーを追加する
Velocity は任意のコードを実行するための高速で安全な環境として便利ですが、複雑なビジネス ロジックを記述する場合、AWS Lambda 関数に同様に簡単に呼び出すことができます。以下はその方法です。
-
まず
amplify add functionを実行して関数を作成します。例の残りの部分では、amplify add functionコマンドで「echofunction」という名前の関数を作成したことを前提としています。既に関数がある場合は、このステップをスキップできます。 -
AppSync Lambda 関数を呼び出す AWS Schema.graphql にフィールドを追加します。
type Query { echo(msg: String): String}- スタックの Resources ブロック内にデータソースとして関数を追加します。
"EchoLambdaDataSource": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "EchoFunction", "Type": "AWS_LAMBDA", "ServiceRoleArn": { "Fn::GetAtt": [ "EchoLambdaDataSourceRole", "Arn" ] }, "LambdaConfig": { "LambdaFunctionArn": { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}", { "env": { "Ref": "env" } } ] } } }}- AppSync があなたに代わって Lambda 関数を呼び出すことを許可する AWS IAM ロールをスタックの Resources ブロックに作成します。
"EchoLambdaDataSourceRole": { "Type": "AWS::IAM::Role", "Properties": { "RoleName": { "Fn::Sub": [ "EchoLambdaDataSourceRole-${env}", { "env": { "Ref": "env" } } ] }, "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Policies": [ { "PolicyName": "InvokeLambdaFunction", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:invokeFunction" ], "Resource": [ { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}", { "env": { "Ref": "env" } } ] } ] } ] } } ] }}- スタックの Resources ブロックに AppSync リゾルバーを作成します。
"QueryEchoResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": { "Fn::GetAtt": [ "EchoLambdaDataSource", "Name" ] }, "TypeName": "Query", "FieldName": "echo", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.echo.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.echo.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } }}- プロジェクトの resolvers ディレクトリにリゾルバー テンプレートを作成します。
resolvers/Query.echo.req.vtl
{ "version": "2017-02-28", "operation": "Invoke", "payload": { "type": "Query", "field": "echo", "arguments": $utils.toJson($context.arguments), "identity": $utils.toJson($context.identity), "source": $utils.toJson($context.source) }}resolvers/Query.echo.res.vtl
$util.toJson($ctx.result)amplify push を実行した後、amplify api console で AppSync コンソールを開き、この簡単なクエリで API をテストします。
query { echo(msg: "Hello, world!")}@searchable で作成された OpenSearch ドメインをターゲットにするカスタム ジオロケーション検索リゾルバーを追加する
API にジオロケーション検索機能を追加するには、@searchable ディレクティブを @model タイプに追加します。
type Todo @model @searchable { id: ID! name: String! description: String comments: [Comment] @connection(name: "TodoComments")}次に amplify push を実行すると、Amazon OpenSearch ドメインが作成され、DynamoDB から OpenSearch にデータが自動的にストリーミングされるように設定されます。Todo タイプの @searchable ディレクティブは Query.searchTodos クエリ フィールドとリゾルバーを生成しますが、より具体的な検索機能が必要なことは珍しくありません。以下の手順に従ってカスタム検索リゾルバーを記述できます。
- スキーマに関連するロケーションおよび検索フィールドを追加します。
type Comment @model { id: ID! content: String todo: Todo @connection(name: "TodoComments")}type Location { lat: Float lon: Float}type Todo @model @searchable { id: ID! name: String! description: String comments: [Comment] @connection(name: "TodoComments") location: Location}type TodoConnection { items: [Todo] nextToken: String}input LocationInput { lat: Float lon: Float}type Query { nearbyTodos(location: LocationInput!, km: Int): TodoConnection}- スタックの Resources ブロック内にリゾルバー レコードを作成します。
"QueryNearbyTodos": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": "ElasticSearchDomain", "TypeName": "Query", "FieldName": "nearbyTodos", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyTodos.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyTodos.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } }}- リゾルバー テンプレートを記述します。
## Query.nearbyTodos.req.vtl## Todo タイプのオブジェクトは /todo インデックスに格納されます
#set( $indexPath = "/todo/doc/_search" )#set( $distance = $util.defaultIfNull($ctx.args.km, 200) ){ "version": "2017-02-28", "operation": "GET", "path": "$indexPath.toLowerCase()", "params": { "body": { "query": { "bool": { "must": { "match_all": {} }, "filter": { "geo_distance": { "distance": "${distance}km", "location": $util.toJson($ctx.args.location) } } } } } }}## Query.nearbyTodos.res.vtl
#set( $items = [] )#foreach( $entry in $context.result.hits.hits ) #if( !$foreach.hasNext ) #set( $nextToken = "$entry.sort.get(0)" ) #end $util.qr($items.add($entry.get("_source")))#end$util.toJson({ "items": $items, "total": $ctx.result.hits.total, "nextToken": $nextToken})amplify pushを実行します
Amazon OpenSearch ドメインのデプロイには時間がかかる場合があります。この間に OpenSearch について読んで、これからアンロックしようとしている機能を確認してください。
- 更新が完了したら、オブジェクトを作成する前に、OpenSearch インデックス マッピングを更新します。
インデックス マッピングは OpenSearch に対して、保存しようとしているデータをどのように扱うべきかを伝えます。デフォルトでは、"location": { "lat": 40, "lon": -40 } フィールドを持つオブジェクトを作成すると、OpenSearch はそのデータを object タイプとして扱いますが、実際には geo_point として扱う必要があります。マッピング API を使用して、OpenSearch にこの方法を伝えます。
インデックス内のオブジェクトを作成する前に、必ず location フィールドが geo_point であることを OpenSearch に伝えてください。そうしないと、インデックスを削除して再試行する必要があります。Amazon OpenSearch コンソール に移動して、この環境の GraphQL API ID を含む OpenSearch ドメインを見つけます。これをクリックして、OpenSearch ダッシュボード リンクを開きます。OpenSearch ダッシュボードを表示するには、AWS Agent などのブラウザー拡張機能をインストールし、AWS プロファイルの公開鍵と秘密鍵で設定して、ブラウザーがセキュリティの理由から OpenSearch ダッシュボードへのリクエストに署名できるようにする必要があります。OpenSearch ダッシュボードが開いたら、左側の「Dev Tools」タブをクリックして、ブラウザー内のコンソールを使用して以下のコマンドを実行します。
# /todo インデックスが存在しない場合は作成しますPUT /todo
# location フィールドが geo_point であることを OpenSearch に伝えますPUT /todo/_mapping/doc{ "properties": { "location": { "type": "geo_point" } }}- API を使用してオブジェクトを作成し、すぐにそれらを検索します。
OpenSearch インデックス マッピングを更新した後、amplify api console で AWS AppSync コンソールを開き、これらのクエリを試してみてください。
mutation CreateTodo { createTodo( input: { name: "Todo 1" description: "The first thing to do" location: { lat: 43.476446, lon: -110.767786 } } ) { id name location { lat lon } description }}
query NearbyTodos { nearbyTodos(location: { lat: 43.476546, lon: -110.768786 }, km: 200) { items { id name location { lat lon } } }}Mutation.createTodo を実行すると、データは AWS Lambda 経由で OpenSearch に自動的にストリーミングされ、Query.nearbyTodos を介してほぼ即座に利用可能になります。