nextTokenでしかページネーションできません

結論、nextTokenを使って指定のページネーション位置までぶん回します。

例えば下記のようなGraphQLスキーマが存在するとします。

type Product
@model
@searchable
@auth(
  rules: [
    {allow: groups, groups: ["admin"]},
    {allow: groups, groups: ["customer"], operations: [read]}
  ])
{
  id: ID!
  name: String!
  price: Int!
  status: ProductStatus!
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime!
}

enum ProductStatus {
  OPEN
  SOLT_OUT
}

型定義:https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/scalars.html

@searchableが入っていると、amplify push時に、ElasticSearchが生成される。
上記のスキーマによって生成されたGraphqlコードを見ていこう。

export const searchProduct = /* GraphQL */ `
  query SearchClients(
    $filter: SearchableProductFilterInput
    $sort: SearchableProductSortInput
    $limit: Int
    $nextToken: String
  ) {
    searchProduct(
      filter: $filter
      sort: $sort
      limit: $limit
      nextToken: $nextToken
    ) {
      items {
        id
        name
        price
        status
        createdAt
        updatedAt
      }
      nextToken
      total
    }
  }
`;

実際に呼び出すとこんな感じ。

import {API, graphqlOperation} from 'aws-amplify'
import {searchProduct} from '~/src/graphql/queries'

const queryArgument = Object.assign({},{
  sort : { // Defaultはパーティションキー順になってしまうので対策
      field: 'createdAt',
      direction: 'desc'
  },
  limit : 30
})

const res = await API.graphql(graphqlOperation(searchProduct, queryArgument))
console.log(res)
// data.searchProduct.items 取得したコレクション
// data.searchProduct.nextToken ページネーションのトークン

そして次回のリクエストでnextTokenを設定すれば、前回の続きからコレクション情報を取得できる。
ただし、次のコレクション情報が存在しなければ、nullで返ってくるので注意が必要だ。

ここで引っかかるのが、下記のようなケースである。

  • 知り合いに検索結果の3ページのURLを共有した
  • 知り合いは共有されたURLへアクセス

そう、ElasticSearchにおけるページネーションはnextTokenを用いて行われるので、nextTokenを保有していない状態で途中の番号からコレクションを取得できないのだ。(もしかしたら最善の方法あるかもですが検索しても情報なかったです)

私は下記のような対応を行いました。

// forcePagination.js
import _ from "lodash";
import {API, graphqlOperation} from "aws-amplify";

/**
 * Amplifyでpage=1以上のリンク直打ちの際に利用するページネーション処理
 *
 * @description 現在のページ位置まで強制的に移動し配列を取得
 * @param page                現在のページネーション番号
 * @param graphqlQuery        グラフQLのクエリー文字列(src/graphql/queries.js)
 * @param queryArgument       クエリーへ適用したい引数
 * @param dataIndex           データ取得後のdataオブジェクトの直下のインデックス名
 * @returns {nextToken,items} カレントのnextTokenと表示する配列
 */
export default async function (page, graphqlQuery, queryArgument, dataIndex) {
  let nextToken = undefined
  let loopCount = page - 1
  let items = []
  while (1) {
    if (loopCount < 0) break;
    const currentQuery = _.assign(queryArgument, {nextToken})
    const res = await API.graphql(graphqlOperation(graphqlQuery, currentQuery))
    nextToken = res.data[dataIndex].nextToken
    items = res.data[dataIndex].items
    loopCount = loopCount - 1
  }
  return _.assign({}, {
    nextToken,
    items
  });
}

お役立ていただければ幸いです。

所感

CloudSearch時代にも同じような対応をした経験があるので、上記のような対応を施しました。
もっといい方法あればコメント願います🙇‍♂️