Amplifyで自動構築される内容の観察:VTL編

Amplifyで自動構築される内容の観察:VTL編

こんにちは!

この記事は Amplify CLI を使って自動構築されたリソースを観察するシリーズです。Schema.graphql 編と、DynamoDB 編もありますので、こちらも併せてご覧ください!

想定する読者

  • Amplify が気になっているヒト
  • Amplify CLI を使ってみたいヒト
  • 最近 Amplify CLI を知ったヒト

はじめに

GraphQL を作成するコマンドは対話型のため、以下のように設定しました。

なお「Amplfiy で自動構築される内容の観察」シリーズすべての記事は、下記の設定を利用しております。

$ 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
 Do you want to use the default authentication and security configuration? Default configuration
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? No
$ amplify push
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? 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

※ Amplify CLI のバージョンは 4.44.1になります。

作成される VTL を解説

それでは作成される VTL を確認しましょう。Amplify CLI は型の定義を更新すると、それに応じて自動的にスキーマや VTL が作成されます。

まずは Todo テンプレートのスキーマからどんな VTL が作成されたのかを見てみましょう。

type Todo @model {
  id: ID!
  name: String!
  description: String
}

上記はデフォルトで記載されているスキーマです。こちらを amplify push した場合は以下のような VTL が作成されます。こちらは、updateTodo のリゾルバーのリクエストマッピングテンプレートになります。

#if( $authCondition && $authCondition.expression != "" )
  #set( $condition = $authCondition )
  #if( $modelObjectKey )
    #foreach( $entry in $modelObjectKey.entrySet() )
      $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)"))
      $util.qr($condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key"))
    #end
  #else
    $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#id)"))
    $util.qr($condition.expressionNames.put("#id", "id"))
  #end
#else
  #if( $modelObjectKey )
    #set( $condition = {
  "expression": "",
  "expressionNames": {},
  "expressionValues": {}
} )
    #foreach( $entry in $modelObjectKey.entrySet() )
      #if( $velocityCount == 1 )
        $util.qr($condition.put("expression", "attribute_exists(#keyCondition$velocityCount)"))
      #else
        $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)"))
      #end
      $util.qr($condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key"))
    #end
  #else
    #set( $condition = {
  "expression": "attribute_exists(#id)",
  "expressionNames": {
      "#id": "id"
  },
  "expressionValues": {}
} )
  #end
#end
#############長文のため一部省略して記載しております#############
{
  "version": "2018-05-29",
  "operation": "UpdateItem",
  "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
  "id": {
      "S": $util.toJson($context.args.input.id)
  }
} #end,
  "update": $util.toJson($update),
  "condition": $util.toJson($condition)
}

データソースに DynamoDB を指定しているため、”operation” のところで DynamoDB に対するアクションを指定し、”key” の部分で投げられた ID をもとに DynamoDB のテーブルから値を取得する記述となっております。なお $modelObjectKey はテンプレートに定義されていない場合は無視されます。

これは最小限かつシンプルな記述です。ではコードの始めのあたりにある if 文などは一体何に利用されるのでしょうか?次の章で確認していきましょう。

認証ルールに応じた VTL

では試しに以下のスキーマを定義して amplify push してみます。このスキーマでは Cognito ユーザープールを用いて認証されたユーザーが、オブジェクトの所有者の場合、そのオブジェクトに対する管理者権限を持つように指定しています。

type Todo @model 
  @auth(rules: [{ allow: owner }]) {
  id: ID! 
  name: String!
  owner: String
  description: String!
  updatedAt: AWSDateTime 
  createdAt: AWSDateTime 
}

認証ルールを利用したスキーマを amplify push すると、VTL の始めのほうにある if 文が変動し認証ルールに合わせた形になります。以下の記述は Amplify CLI で認証ルールのあるスキーマで更新したあとに、updateTodo の VTL に追記されたコードです。

## [Start] Determine request authentication mode **
#if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) )
  #set( $authMode = "userPools" )
#end
## [End] Determine request authentication mode **
## [Start] Check authMode and execute owner/group checks **
#if( $authMode == "userPools" )
  ## No Static Group Authorization Rules **


  #if( ! $isStaticGroupAuthorized )
    ## No dynamic group authorization rules **


    ## [Start] Owner Authorization Checks **
    #set( $ownerAuthExpressions = [] )
    #set( $ownerAuthExpressionValues = {} )
    #set( $ownerAuthExpressionNames = {} )
    ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **
    $util.qr($ownerAuthExpressions.add("#owner0 = :identity0"))
    $util.qr($ownerAuthExpressionNames.put("#owner0", "owner"))
    $util.qr($ownerAuthExpressionValues.put(":identity0", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")))))
    ## [End] Owner Authorization Checks **


    ## [Start] Collect Auth Condition **
    #set( $authCondition = $util.defaultIfNull($authCondition, {
  "expression": "",
  "expressionNames": {},
  "expressionValues": {}
}) )
    #set( $totalAuthExpression = "" )
    ## Add dynamic group auth conditions if they exist **
    #if( $groupAuthExpressions )
      #foreach( $authExpr in $groupAuthExpressions )
        #set( $totalAuthExpression = "$totalAuthExpression $authExpr" )
        #if( $foreach.hasNext )
          #set( $totalAuthExpression = "$totalAuthExpression OR" )
        #end
      #end
    #end
    #if( $groupAuthExpressionNames )
      $util.qr($authCondition.expressionNames.putAll($groupAuthExpressionNames))
    #end
    #if( $groupAuthExpressionValues )
      $util.qr($authCondition.expressionValues.putAll($groupAuthExpressionValues))
    #end
    ## Add owner auth conditions if they exist **
    #if( $totalAuthExpression != "" && $ownerAuthExpressions && $ownerAuthExpressions.size() > 0 )
      #set( $totalAuthExpression = "$totalAuthExpression OR" )
    #end
    #if( $ownerAuthExpressions )
      #foreach( $authExpr in $ownerAuthExpressions )
        #set( $totalAuthExpression = "$totalAuthExpression $authExpr" )
        #if( $foreach.hasNext )
          #set( $totalAuthExpression = "$totalAuthExpression OR" )
        #end
      #end
    #end
    #if( $ownerAuthExpressionNames )
      $util.qr($authCondition.expressionNames.putAll($ownerAuthExpressionNames))
    #end
    #if( $ownerAuthExpressionValues )
      $util.qr($authCondition.expressionValues.putAll($ownerAuthExpressionValues))
    #end
    ## Set final expression if it has changed. **
    #if( $totalAuthExpression != "" )
      #if( $util.isNullOrEmpty($authCondition.expression) )
        #set( $authCondition.expression = "($totalAuthExpression)" )
      #else
        #set( $authCondition.expression = "$authCondition.expression AND ($totalAuthExpression)" )
      #end
    #end
    ## [End] Collect Auth Condition **
  #end


  ## [Start] Throw if unauthorized **
  #if( !($isStaticGroupAuthorized == true || ($totalAuthExpression != "")) )
    $util.unauthorized()
  #end
  ## [End] Throw if unauthorized **
#end
## [End] Check authMode and execute owner/group checks **

コメントアウトに注目してみましょう。はじめのほうに [Start] Determine request authentication mode とあります。これは Conginito ユーザープールが認証方式であることを示しています。

次に [Start] Owner Authorization Checks とありますが、こちらでアクセスの管理を行っております。今回は オブジェクトの所有者のみに update/delete の権限などを付与しているので、それ以外のユーザーは操作できないようにコントロールをしています。

このように、スキーマに定義した内容に従って VTL は変化していきます。

Query 型のフィールド

Query 型のフィールドのリクエストマッピングテンプレートは以下のようになります。

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
  "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
} #end
}

基本的な記述が用いられています。

ではスキーマを以下のように定義して amplify push するとどうでしょうか。

type Item @model
  @key(fields: ["orderId", "status", "createdAt"])
  @key(name: "ByStatus", fields: ["status", "createdAt"], queryField: "itemsByStatus") {
  orderId: ID!
  status: Status!
  createdAt: AWSDateTime!
  name: String!
}

enum Status {
  DELIVERED
  IN_TRANSIT
  PENDING
  UNKNOWN
}

デプロイ後、getItem の VTL を確認します。

## [Start] Set the primary @key. **
#set( $modelObjectKey = {
  "orderId": $util.dynamodb.toDynamoDB($ctx.args.orderId),
  "status#createdAt": $util.dynamodb.toDynamoDB("${ctx.args.status}#${ctx.args.createdAt}")
} )
## [End] Set the primary @key. **
{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
  "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
} #end
}

@key ディレクティブを加えたことにより、[Start] Set the primary @key. の部分の記述が追加されました。

@key ディレクティブは、DynamoDB のパーティション・ソートキーやインデックスを設定するのに利用されるものです。CloudFormation でいえば、AttributeDefinitions や KeySchema、GlobalSecondaryIndexesの箇所を設定しています。

新たに追記された記述により、status と createdAt を指定しないと、アイテムの情報が取れないようにコントロールされました。

このように Query 型も Mutation 型と同様に、実行する条件をスキーマで定義すれば、自動的に VTL が作成されます。

レスポンスマッピングテンプレート

最後にレスポンスマッピングテンプレートです。こちらは全てのリゾルバーで以下のように記述されています。

#if( $ctx.error )
$util.error($ctx.error.message, $ctx.error.type)
#else
$util.toJson($ctx.result)
#end

if 文でエラーがあればメッセージとエラータイプを返して、何もなければ結果を返すものとなっております。

レスポンスに関しては、Amplify 側でいろんな操作をしても特に変化はありませんでした。ここで複雑な処理は必要ないからでしょうか、シンプルな記述となっています。

関連記事

まとめ

指定したスキーマで、様々なディレクティブを使用して制限を設けた場合、VTL がそれに合わせて変化およびコントロールしてくれていることが分かりました。

Amplify の自動生成は、スキーマ以外でも隅々まで記述してくれるのでとても便利ですね。もっと使いこなしたくなってきます。

このブログでは、GraphQL や GraphQL のマネージドサービスである AWS AppSync の記事をどんどん公開しておりますので、ご興味のある方はほかの記事もご覧いただければと思います。

AppSync に関する開発は、お気軽にお問い合わせください。