Timeline機能: @function

FollowRelationship APIと、Timeline APIが作成できました。 ここではcreatePostAndTimelineLambda Resolverを作成します。 その役割は、以下です。

  1. createPostMutationを行い、Post Tableに新たなPostを作成
  2. FollowRelationship Tableを読み、APIコールしたユーザーのフォロワーを取得
  3. Timeline Tableに、各フォロワーがフォロイーのPost内容を読み取れるよう、アイテムを作成

amplify add function

$ amplify add functionを実行します。いくつか質問されるので、以下のように入力してください。

Select the categorySelect the operations...の項目は複数選択式となります。 該当項目までカーソルで移動後、__スペースキー__を押すことで選択でき、Enterキーを押すことで選択が確定し次の項目に移ります。 スペースキーで選択ができていることを確認してください。

  • Select which capability you want to add: Lambda function (serverless function)
  • Provide a friendly name for your resource to be used as a label for this category in the project: createPostAndTimeline
  • Provide the AWS Lambda function name: createPostAndTimeline
  • Choose the runtime that you want to use: NodeJS
  • Choose the function template that you want to use: Hello World
  • Do you want to configure advanced settings? Yes
  • Do you want to access other resources in this project from your Lambda function? Yes
  • Select the category api
  • Select the operations you want to permit for BoyakiGql Query, Mutation
  • Do you want to invoke this function on a recurring schedule? No
  • Do you want to configure Lambda layers for this function? No
  • Do you want to edit the local lambda function now? No

$ amplify add functionでは、いくつかのテンプレートが用意されています。 例えばLambda Triggerを選択すると、@modelで作成したAmazon DynamoDB Tableの変更を検知してAWS Lambda関数を実行するDynamoDB Streamsがセットアップできたりします。 詳細はこちら

createPostAndTimeline Mutationの作成

createPostAndTimelineMutationを作成し、GraphQL API経由で先ほど作成したLambda関数を呼び出せるようにしましょう。 ./amplify/backend/api/BoyakiGql/schema.graphqlに次のコードを追加します。

type Mutation
{
  createPostAndTimeline(
		content: String!
	): Post
    @function(name: "createPostAndTimeline-${env}")
    @auth(rules: [
      {allow: private, provider: userPools},
    ])
}
  • type Mutation配下にcreatePostAndTimelineのように書く事で、Mutationを追加できます
  • createPostAndTimeline
    • String型のcontentを引数にします
    • 返り値は以前定義したPost型となります
  • @function
    • @functionを利用する事で、Lambda Resolverを設定できます
    • 先ほど作成したLambda関数の名前はcreatePostAndTimelineでしたが、実際に作成されるAWS Lambda関数のリソース名には-つなぎでenv名が追加された形になります
      • 今回は3.1.で作成したproduction envで作業しているため、作成されるリソース名はcreatePostAndTimeline-productionになります
      • Amplify Envについて、詳しくは第7章で扱っていきます
  • @auth
    • おなじみの@authで、Amazon Cognito User Poolsで認証されたユーザーであればだれでも、createPostAndTimeline Mutationを実行することが可能です

既存APIへのアクセス権の追加

今まではGraphQL APIをコールするのはAmazon Cognito User Poolsで認証されたユーザーだけでした。 そのためLambda関数はlistFollowRelationshipcreatePostといったOperationを実行することができません。 ここではLambda関数がこれらのOperationを実行できるようにします。

type Post
  @model (
    mutations: {create: "createPost", delete: "deletePost", update: null}
    timestamps: null
    subscriptions: { level: public}
  )
  @auth(rules: [
        {allow: owner, ownerField:"owner", provider: userPools, operations:[read, create]}
        {allow: private, provider: userPools, operations:[read]}
        {allow: private, provider: iam ,operations:[create]} #追加
  ])
type FollowRelationship
    @model(
        mutations: {create: "createFollowRelationship", delete: "deleteFollowRelationship", update: null}
        timestamps: null
    )
	@auth(rules: [
		{allow: owner, ownerField:"followerId", provider: userPools, operations:[read, create]}
		{allow: private, provider: userPools, operations:[read]}
		{allow: private, provider: iam ,operations:[read]} #追加
	])

現在のschema.graphqlは以下のようになっています。

AWS Lambda関数のコードの編集

作成したLambda関数は./amplify/backend/function/createPostAndTimeline配下に設定ファイルが格納されています。

以下のindex.jsの中身をコピーし、./amplify/backend/function/createPostAndTimeline/src/index.jsを置き換えてください。

  • index.js (4 kb)
  • また次のスクリプトをターミナルで実行し、Lambda関数の実行に必要なライブラリをインストールしてください。

    cd ./amplify/backend/function/createPostAndTimeline/src
    npm install --save aws-appsync@3.0.2 graphql-tag@2.10.3 node-fetch@2.6.0
    cd ../../../../..
    

    ポイントは以下です。

    他のプロジェクト内リソースへのアクセス

    /* Amplify Params - DO NOT EDIT
    You can access the following resource attributes as environment variables from your Lambda function
    var environment = process.env.ENV
    var region = process.env.REGION
    var apiGraphqlapiGraphQLAPIIdOutput = process.env.API_BOYAKIGQL_GRAPHQLAPIIDOUTPUT
    var apiGraphqlapiGraphQLAPIEndpointOutput = process.env.API_BOYAKIGQL_GRAPHQLAPIENDPOINTOUTPUT
    
    Amplify Params - DO NOT EDIT */
    
    • ファイルの先頭には$ amplify add functionを実行した際にアクセスするよう指定した他カテゴリのリソースに関する情報が記載されています
    • 例えばprocess.env.API_BOYAKIGQL_GRAPHQLAPIENDPOINTOUTPUTでGraphQL のエンドポイントが参照できます

    Postのバリデーション

    if(event.arguments.content.length > 140) {
        callback('content length is over 140', null);
    }
    
    • event.arguments.content:
      • AWS Lambdaが呼び出された時の引数はeventオブジェクトに格納されています
      • AppSyncがLambdaを呼び出した際、GraphQLのOperationを実行した際の入力はevent.argumentsに格納されています
    • 140字を超えていた場合、すぐさまエラーを返します
    • これにより、GraphQL APIリクエストをアプリ経由でなくマニュアルで作成してAPIコールするようなHackでも140字制限を超えたPostを作成することができなくなります

    環境に応じたAppSync Clientセットアップ

    if ('AWS_EXECUTION_ENV' in process.env && process.env.AWS_EXECUTION_ENV.startsWith('AWS_Lambda_')) {
        //for cloud env
        env = process.env;
        graphql_auth = {
            type: "AWS_IAM",
            credentials: {
                accessKeyId: env.AWS_ACCESS_KEY_ID,
                secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
                sessionToken: env.AWS_SESSION_TOKEN,
            }
        };
        graphql_endoint = env.apiGraphqlapiGraphQLAPIEndpointOutput;
    } else {
        // for local mock
        env = {
            API_GRAPHQLAPI_GRAPHQLAPIENDPOINTOUTPUT: 'http://192.168.1.2:20002/graphql',
            REGION: 'us-east-1',
        }
        graphql_auth = {
            type: "AWS_IAM",
            credentials: {
                accessKeyId: 'mock',
                secretAccessKey: 'mock',
                sessionToken: 'mock',
            }
        };
    }
    
    if (!graphqlClient) {
        graphqlClient = new AWSAppSyncClient({
            url: env.API_GRAPHQLAPI_GRAPHQLAPIENDPOINTOUTPUT,
            region: env.REGION,
            auth: graphql_auth,
            disableOffline: true,
        });
    }
    
    • if('AWS_EXECUTION_ENV'...では、このコードが動いてる環境がAWS Lambdaかそれ以外かを判定しています
      • local mockの条件分岐では、AWS Lambdaから渡される環境変数を手動で設定しています
      • API_GRAPHQLAPI_GRAPHQLAPIENDPOINTOUTPUThttp://192.168.1.2:20002/graphqlのIPアドレスは、ご自身の環境に合わせて書き換えてください
    • 現時点でAmplifyのJavaScriptライブラリはNode.jsランタイムで動作しないため、AWS Lambda関数からAWS AppSyncを呼び出す際にはAWS AppSync SDKを利用します

    createPost Mutationの実行

    //post to the origin
    const postInput = {
        mutation: gql(createPost),
        variables: {
            input: {
                type: 'post',
                timestamp: Math.floor(Date.now() / 1000),
                owner: event.identity.username,
                content: event.arguments.content,
            },
        },
    };
    const res = await graphqlClient.mutate(postInput);
    const post = res.data.createPost;
    
    • const res = await graphqlClient.mutate(postInput)でPostを作成します
      • ownerやtype、timestampなどをサーバーサイドで付与することにより不正なMutationを防ぎます
    • event.identity.username: Postを実行したユーザー情報の取得
      • AWS Lambdaが呼び出された時の引数はeventオブジェクトに格納されています
      • AppSyncがLambdaを呼び出した際、GraphQLのOperationを実行したユーザー情報はevent.identityに格納されています

    Postしたユーザーのフォロワーの取得

    const queryInput = {
        followeeId: event.identity.username,
        limit: 100000,
    }
    const listFollowRelationshipsResult = await graphqlClient.query({
        query: gql(listFollowRelationships),
        fetchPolicy: 'network-only',
        variables: queryInput,
    });
    const followers = listFollowRelationshipsResult.data.listFollowRelationships.items;
    
    • graphqlClient.query
      • fetchPolistynetwork-onlyにすることにより、つねにキャッシュデータでなくAppSyncからデータを取ってきます

    AmplifyでセットアップしたAppSyncがLambda関数を呼び出す時のeventの構造についてはこちらをご覧ください。

    Timelineの作成

    //post to timeline
    followers.push({
        followerId: post.owner,
    })
    const results = await Promise.all(followers.map((follower)=> createTimelineForAUser({follower: follower, post: post})));
    
    • followers.push...の箇所では、Post作成者のTimelineに表示するよう、followersにPost作成者のUsernameを追加しています
    • const results...では、フォロワーごとにcreateTimelineForAUserメソッドを呼び出し、Timelineアイテムの作成を行なっています
    const createTimelineForAUser = async ({follower, post}) => {
        const timelineInput = {
            mutation: gql(createTimeline),
            variables: {
                input: {
                    userId: follower.followerId,
                    timestamp: post.timestamp,
                    postId: post.id,
                },
            },
        }
        const res = await graphqlClient.mutate(timelineInput);
    }
    

    Amplify Mockingによる動作確認

    Amplify Mocking(手元での動作確認)には20002番ポートが利用可能である必要があり、一部の Cloud IDE では利用できないことがあります。 該当するCloud IDEをご利用の方は本手順は実施せず、雰囲気を味わっていただけますと幸いです。

    createPostAndTimeline

    $ amplify mock apiのログに、Could not find ref for "apiBoyakiGqlGraphQLAPIIdOutput". Using unsubstituted value. Could not find ref for "apiBoyakiGqlGraphQLAPIEndpointOutput". Using unsubstituted value.と出る場合がありますが、気にせず以下の手順を実施頂くことができます。

    1. 左下のADD NEW Queryと書いてある場所をクリックし、Mutationを選択したのち+をクリックします
    2. 左のペインにあるcreatePostAndTimelineをクリックし、contentに140字以内の内容を入力しましょう
    3. 画面上部のUpdate Authをクリックし、Usernameを5.1で作成したtest_followeeに変更し、Generate Tokenをクリックします
    4. ▶︎をクリックしてGraphQLのMutationを実行します
    5. timestampidなど、作成したPostが右側のペインに表示されることを確認します

    createPostAndTimeline バリデーションの確認

    1. 左のペインにあるcreatePostAndTimelineをクリックし、contentに141字以上の内容を入力しましょう。
    2. ▶︎をクリックしてGraphQLのMutationを実行します
    3. 右側のペインにエラーメッセージが表示されることを確認します。

    141字以上のテキストを作成する際は、作成したフロンエンドアプリケーションを利用すると捗ります。(140字越えるとエラーがでるため)

    listTimelines

    1. 左下のADD NEW Mutationと書いてある場所をクリックし、Queryを選択したのち+をクリックします。
    2. 左のペインにあるlistTimelineをクリックし、次の図を参考にQueryを作成していきます
    3. userIdtest_followerを入力します。
    4. 画面上部のUpdate Authをクリックし、Usernameを5.1で作成したtest_followerにします
    5. ▶︎をクリックしてGraphQLのQueryを実行します
    6. 先ほど作成したPostが表示されることを確認します