ファウナでページ抜きを取り扱う方法


導入


FQL(Foounaの照会言語)のレゾルバを書くことは、このタイプのプログラミングに精通していない場合は特に地球上で最も簡単なことではありません.一見したら変に見えるかもしれない.私が聞いている主なことは、「関数の中に関数の中に関数を持たなければならないことがあります.」
この記事ではget started with Fauna 既定のクエリを持つ基本的なセットアップ(スキーマをインポートするときに自動的に構築)からレゾルバを使用してクエリを作成するには、デフォルトのペグ化リゾルバを再作成します.最後に、複数のフィルタを受け入れる複雑なレゾルバに移動します.
スキーマの作成から始めましょう.我々のデータモデルは、レゾルバと実装からあなたを邪魔しないように簡単です.

第1章:スキーマ


データのスキーマを使用します.
type Student {
    name: String
    tests: [Test] @relation
}

type Project {
    name: String
    tests: [Test] @relation
}

type Test {
    name: String
    student: Student!
    project: Project!
}
私たちは3つのモデルを持っている.学生、プロジェクト、テスト.
それぞれのテストをプロジェクトに割り当てる必要があります.学生は、後で学生にリンクされているテストを受けることができます.
今のようにスキーマをアップロードする場合、ボックスからいくつかのクエリを受け取ります.スキーマをアップしましょう.
GraphSQLタブに移動し、インポートスキーマを押します.

スキーマをアップロードした後、クエリや突然変異のいくつかは自動的に私たちのために作成されます.我々は3クエリをご利用いただけますfindProjectByID , findStudentByID and findTestByID .
つのドキュメントプロジェクト、学生またはテストからのすべてのデータを受信します.
すべてのテストやプロジェクトを取得するには、スキーマに適切なクエリを追加する必要があります.
# ...
type Query {
  projects: [Project]
  tests: [Test]
}
スキーマのアップロード後、すべてのテストとプロジェクトを取得することもできます

上記の手順の後、スキーマは次のようになります.
type Student {
    name: String
    tests: [Test] @relation
}

type Project {
    name: String
    tests: [Test] @relation
}

type Test {
    name: String
    student: Student
    project: Project
}

type Query {
  projects: [Project]
  tests: [Test]
}
各ステップを自分で試してみたいなら、いくつかのダミーデータを持っているのが役に立つでしょう.これが使えますgist 上記のスキーマに一致する偽のデータを追加するには.
  • 秘密のセキュリティタブを作成し、<fauna_secret>
  • パッケージをインストールすることを忘れないでください
  • ランnode generate.js
  • 第2章:ページ付きクエリ


    たった今、我々は3つのデフォルト質問を我々が一つの文書と我々の2つの質問からデータを得るのを許して、テストまたはプロジェクトを得ます.
    どのような場合は、プロジェクトXからのみテストのような特定のデータを取得したいですか?
    特定のプロジェクトを取得し、テストフィールドを使用して関連テストを取得できます.
    query TestByProject {
      findTestByID(id: <test_id>) {
        project: {
          data: {...}
        }
      }
    }
    
    それは常に簡単ではないが、カスタムレゾルバで新しいクエリを作成する理由です.
    新しいクエリをスキーマに追加します.グラフファイル
    type Query {
        ...
        getTestsByProject(id: ID): [Project] @resolver(name: "tests_by_project", paginated: true)
    }
    
    使用によって@resolver ディレクティブは、このクエリのリゾルバを使用することを指定します.リクエストを処理する関数の名前を渡します.paginated: true フラグは、新しいクエリが既定のものと同じように動作することを確認します.それは代わりにすべてを一度に送信するデータをページ化します.
    スキーマの更新後、新しい関数tests_by_project 「機能」タブに表示されます.今すぐに新しいクエリを使用しようとすると、エラーが表示されます.それで、それをしましょう.
    このような問い合わせを処理できるインデックスがあるかどうかを確認する必要があります.指定したプロジェクトIDに一致するすべてのテストを取得したい場合は、インデックスタブに移動すると、既に作成された名前のインデックスが表示されますproject_tests_by_project . それはまさに我々が必要とすることです.

    このインデックスを持っていない場合や、そのように作成する方法を知りたい場合は、シェルタブのコンソールで使用できるスクリプトがあります.
    CreateIndex({
        name: "project_tests_by_project",
        source: Collection("Test"),
        terms: [
            {
              field: ["data", "project"]
            }
          ]
    })
    
    ここで、いくつかのコードをTestsCore Bytleプロジェクト関数に追加する必要があります.基本的に2つのことをする必要があります.最初の部分から始めましょう.
    Query(
      Lambda(
        ["projectID"],
        Let({
            project: Ref(Collection("Project"), Var("projectID")),
            match: Match(Index("project_tests_by_project"), Var("project")),
            data: Paginate(Var("match"))
          },
            Map(Var("data"), Lambda("ref", Get(Var("ref"))))
          )
       )
    )
    
    ラムダが取る最初の引数は、我々の問い合わせが探す射撃です.次に、使用Let() 関数は、ラムダが段階的に行うものを明確にする変数のいくつかを定義します.
    プロジェクトの下に、プロジェクトのIDを表す文字列を格納します.実際のドキュメントでフィルタリングするには、ドキュメントにrefを必要とするので、「プロジェクト」変数の下に1つを作成します.
    マッチの下にある変数は問い合わせを満たすすべてのドキュメントを探します、そして、最終的に「データ」変数は文書を保存します.match ()によって返される集合からドキュメントを"展開する"には、Paginate関数を使用する必要があります.次の手順では、見つかった各ドキュメントを反復処理してデータを取得します.
    ページネーション.追加後paginated レゾルバラムダへのフラグは3つの追加引数を受け取ります.

  • size -単一のクエリで返されるドキュメントの数を指定する

  • 後/前-クエリを開始する場所を示します(両方のクエリで返されるので、我々は最後のクエリから“後”を使用することができますし、データの次のセットを取得する)
  • 我々は今、それらを渡すことができますPaginate() 関数.これらの引数は、値が格納されているかどうかをスキップします.
    Query(
      Lambda(
        ["projectID", "size", "after", "before"],
        Let(
          {
            ...
            data: If(
              And(IsNull(Var("after")), IsNull(Var("before"))),
              Paginate(Var("match"), { size: Var("size") }),
              If(
                IsNull(Var("before")),
                Paginate(Var("match"), { after: Var("after"), size: Var("size") }),
                Paginate(Var("match"), { before: Var("before"), size: Var("size") })
              )
            )
          },
          ...
        )
      )
    )
    

    第3章:データの表示


    データを表示するにはreact-table 図書館.ページに表示するドキュメントの数だけを取得するには、ページ検索クエリを使用します.Faluna Graphical EdpointへのAPI呼び出しを実行するには、react-query ライブラリとgraphql-request .
    つの基本構成から始めて、「すべてのプロジェクト」ページを作成しましょう.
    // AllProjects.js
    import React, { useContext } from "react";
    import { useQuery } from "react-query";
    import { gql } from "graphql-request";
    import Table from "./Table";
    import { GraphqlClientContext } from "./App";
    
    export default function AllProjects() {
      const { data, isLoading } = useProjects();
    
      if (isLoading) {
        return <span>Loading...</span>;
      }
    
      return <Table columns={columns} data={data} />;
    }
    
    function useProjects() {
      const graphqlClient = useContext(GraphqlClientContext);
      return useQuery("projects", async () => {
        const {
          projects: { data },
        } = await graphqlClient.request(
          gql`
            query {
              projects {
                data {
                  _id
                  name
                }
              }
            }
          `
        );
        return projects;
      });
    }
    
    const columns = [
      {
        Header: "ID",
        accessor: "_id",
      },
      {
        Header: "Name",
        accessor: "name",
      },
    ];
    
    // Table.js
    import { useTable } from "react-table";
    import "./Table.scss";
    
    export default function Table({ columns, data }) {
      const {
        getTableProps,
        getTableBodyProps,
        headerGroups,
        rows,
        prepareRow,
      } = useTable({
        columns,
        data,
      });
      return (
        <table {...getTableProps()}>
          <thead>
            {headerGroups.map((headerGroup) => (
              <tr {...headerGroup.getHeaderGroupProps()}>
                {headerGroup.headers.map((column) => (
                  <th {...column.getHeaderProps()}>{column.render("Header")}</th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody {...getTableBodyProps()}>
            {rows.map((row, i) => {
              prepareRow(row);
              return (
                <tr {...row.getRowProps()}>
                  {row.cells.map((cell) => {
                    return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
                  })}
                </tr>
              );
            })}
          </tbody>
        </table>
      );
    }
    
    // App.js
    import React from "react";
    import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
    import { QueryClient, QueryClientProvider } from "react-query";
    import { GraphQLClient } from "graphql-request";
    import AllProjects from "./AllProjects";
    
    const queryClient = new QueryClient();
    const graphQLClient = new GraphQLClient(`https://graphql.fauna.com/graphql`, {
      headers: {
        authorization: "Bearer <fauna_secret>",
      },
    });
    
    export const GraphqlClientContext = React.createContext();
    
    function Main() {
      return (
        <Router>
            <Switch>
              <Route path="/projects">
                <AllProjects />
              </Route>
            </Switch>
        </Router>
      );
    }
    
    function App() {
      return (
        <GraphqlClientContext.Provider value={graphQLClient}>
          <QueryClientProvider client={queryClient}>
            <Main />
          </QueryClientProvider>
        </GraphqlClientContext.Provider>
      );
    }
    
    export default App;
    
    これが基本的なセットアップです.フルリポジトリを見つけることができますhere .
    現在のセットアップでは、ページネーションをまったく処理しません.いくつかのケースでOKです.(例えば、私が確認できるならば、いくつかのプロジェクトを利用できるだけでしょう)
    しかし、私たちのケースでは、私は確かにサーバー側のページ付けの利点を使用したいと思うので、多くのテストがあるでしょう.
  • 私はデータと一緒に行ったり来たりしたい
  • ページごとに表示されるドキュメント数を変更できます
  • テーブルコンポーネントをページネーションコントロールで拡張してから始めましょう.
    PageMethodリクエストを送信することでページ化処理を行います.
    // Table.js
    import React from "react";
    import { useTable, usePagination } from "react-table";
    import "./Table.scss";
    
    const pageSizeVariants = [50, 75, 100];
    
    export default function Table({
      columns,
      data,
      fetchData,
      loading,
      initialPageSize,
      pageCount: controlledPageCount,
    }) {
      const {
        getTableProps,
        getTableBodyProps,
        headerGroups,
        prepareRow,
        page,
        canPreviousPage,
        canNextPage,
        nextPage,
        previousPage,
        setPageSize,
        // Get the state from the instance
        state: { pageIndex, pageSize },
      } = useTable(
        {
          columns,
          data,
          initialState: { pageIndex: 0, pageSize: initialPageSize },
          // We will be handling pagination by sending paginated request, 
          // not default client side, hence the manualPagination option
          manualPagination: true,
          pageCount: controlledPageCount,
        },
        usePagination
      );
    
      function changeSize(e) {
        setPageSize(Number(e.target.value));
      }
    
      React.useEffect(() => {
        fetchData({ pageIndex, pageSize });
      }, [fetchData, pageIndex, pageSize]);
    
      return (
        <>
          <table {...getTableProps()}>
            <thead>{headerGroups.map(renderHeaderGroup)}</thead>
            <tbody {...getTableBodyProps()}>
              {page.map(renderPage(prepareRow))}
            </tbody>
          </table>
          <div>
            <button onClick={previousPage} disabled={!canPreviousPage}>
              {"<"}
            </button>{" "}
            <button onClick={nextPage} disabled={!canNextPage}>
              {">"}
            </button>{" "}
            <select value={pageSize} onChange={changeSize}>
              {pageSizeVariants.map(renderOption)}
            </select>
          </div>
        </>
      );
    }
    
    function renderHeaderGroup(headerGroup) {
      return (
        <tr {...headerGroup.getHeaderGroupProps()}>
          {headerGroup.headers.map((column) => (
            <th {...column.getHeaderProps()}>{column.render("Header")}</th>
          ))}
        </tr>
      );
    }
    
    function renderPage(prepareRow) {
      return function (row, i) {
        prepareRow(row);
        return (
          <tr {...row.getRowProps()}>
            {row.cells.map((cell) => {
              return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
            })}
          </tr>
        );
      };
    }
    
    function renderOption(val) {
      return (
        <option key={val} value={val}>
          Show {val}
        </option>
      );
    }
    
    テーブルコンポーネントに渡すための追加の小道具を必要としません.
  • fetchdata -すべてのページ/サイズ変更のデータを取得するAPIを呼び出す関数
  • initialpagesize -最初の表示に表示するドキュメント数を設定する
  • PageCount -最初に、データがどのように多くのページが利用可能であるかを示します、我々はその情報を得ることができません、しかし、我々はそれが表示する多くのデータがあるかどうか制御するのにそれを使用しなければなりません.ページの現在の数がページ数と同じである場合、テーブルブロックのページ付けを扱います.より多くのデータがあるか、そうでなければ同じように保つならば、我々は1によってPageCountを増やします.
  • 我々のコンポーネントは適切にページとサイズ変更に反応しなければならなくて、それらのいずれかが変わったならば、新しい要求をしなければなりません.
    クエリから始めましょう.私たちはgetTestsByProject . クエリ変数を定義する必要があります.
    query($id: ID, $size: Int, $cursor: String) {
        getTestsByProject(id: $id, _size: $size, _cursor: $cursor) {
          data {
            id: _id
            name
            student {
              id: _id
            }
          }
          after
          before
        }
      }
    }
    
  • つのクエリで返されるドキュメントの数を設定する-サイズparam
  • カーソルのパームを示すかどうかを次のデータまたは前の1つを設定します
  • 前後にカーソルのパームとして渡す前に、次の(または前)のドキュメントを取得する.
  • ページのパームはありませんので、ページ3からドキュメントを教えてください.我々は、次/前の方法でのみ動作することができます.これは、我々のフェッチ*()メソッドに追加の複雑さを追加しますが、我々はそれを処理します..
    それが理論です.コードを書きましょう.
    最初に新しいフック- usetest ()を作成します.
    // useTests
    function useTests(projectID) {
        // react-table will send us the page index if user go back or next
      const [page, setPage] = React.useState({ index: 0, cursor: null, size: 25 });
    
      // we'll be using the GraphlClient to send requests
      const graphqlClient = useContext(GraphqlClientContext);
      const query = useQuery(
        [key, page.size, page.cursor, projectID],
        fetchProjects(graphqlClient)({ size: page.size, cursor: page.cursor, id: projectID })
      );
    
      return query
    }
    
    const fetchProjects = (client) => (variables) => async () => {
      const { tests } = await client.request(
        gql`
          query($id: ID, $size: Int, $cursor: String) {
            tests: getTestsByProject(id: $id, _size: $size, _cursor: $cursor) {
              data {
                id: _id
                name
                student {
                  name
                }
              }
              after
              before
            }
          }
        `,
        variables
      );
      return tests;
    };
    
    usequeryフックは、ページの状態を変更するたびに発生します.
    そして、使用されるメソッドのいくつかを加えた後に、ページネーションを扱います:
    // useTests.js
    function useTests(projectID) {
      ...
    
      // under query.data we have all the results from `tests` query
      // query.data -> { data, after, before }
        const tests = query.data?.data || [];
      const nextPageCursor = query.data?.after;
      const prevPageCursor = query.data?.before;
      const canNextPage = !!nextPageCursor;
    
      function nextPage() {
        if (!nextPageCursor) return;
        setPage((page) => ({
          ...page,
          index: page.index + 1,
          cursor: nextPageCursor,
        }));
      }
    
      const prevPageCursor = data?.before;
      function prevPage() {
        if (!prevPageCursor) return;
        setPage((page) => ({
          ...page,
          index: page.index - 1,
          cursor: prevPageCursor,
        }));
      }
    
      function changeSize(size) {
        if (size === page.size) return;
        setPage((page) => ({ index: page.index, cursor: null, size }));
      }
    
      function updateData({ pageIndex, pageSize }) {
        if (pageSize !== page.size) changeSize(pageSize);
        else if (pageIndex === page.index) return;
        else if (pageIndex > page.index) nextPage();
        else prevPage();
      }
    
      const canNextPage = !!nextPageCursor;
    
      return {
        ...query,
        data: tests,
        size: page.size,
        updateData,
    
        // page + 1 gives actual number of pages (page is an index started from 0)
        // Number(canNextPage) increase the pageCount by 1 if canNextPage == true
        pageCount: page.index + 1 + Number(canNextPage),
      };
    }
    
    ユーザーが次に行くことを決めた場合、nextPage ()メソッドを起動したい場合には、変更前のサイズ変更後にchangesize ()メソッドを返します.この論理はupdatedata ()の中で生きています.これはどんなページ/サイズ変更の後にも発射されます.
    「プロジェクト」コンポーネントで新しいメソッドを使用します
    // Project.js
    ...
    import { useParams } from "react-router-dom";
    
    export default function Project() {
      const { id } = useParams();
      const { data, isLoading, pageCount, size, updateData } = useTests(id);
    
      if (isLoading) {
        return <span>Loading...</span>;
      }
    
      return (
        <Table
          columns={columns}
          data={data}
          fetchData={updateData}
          pageCount={pageCount}
          initialPageSize={size}
        />
      );
    }
    
    const columns = [
      {
        Header: "ID",
        accessor: "_id",
      },
      {
        Header: "Name",
        accessor: "name",
      },
      {
        Header: "Student",
        accessor: "student.name",
      },
    ];
    
    // App.js
    ...
    <Router>
      <Switch>
        <Route path="/projects/:id">
          <Project />
        </Route>
        <Route path="/projects">
          <AllProjects />
        </Route>
      </Switch>
    </Router>
    ...
    
    これにより、各プロジェクトのページを入力することができます.ブラウザがヒットすると/project/<projectID> ページプロジェクトコンポーネントは、useParams ()フックを使用してURLからIDを取得することができます.
    最後の変更は、すべてのプロジェクトテーブルのid列を変更して、特定のプロジェクトページへのリンクを作成することです.
    // AllProjects.js
    import { Link } from "react-router-dom";
    ...
    
    const columns = [
      {
        Header: "ID",
        accessor: ({ _id }) => <Link to={`/projects/${_id}`}>{_id}</Link>,
      },
      {
        Header: "Name",
        accessor: "name",
      },
    ];
    
    そして、今ではすべてのように見えます-私たちは完全にページ化されたクエリーを使用してページ化されたテーブルを機能させました🚀
    最終的な解決策をチェックしたいなら、ここでのリンクですrepository
    でも.
    別のクエリを書く代わりに、さらに一歩を踏み出したいfilter_by あなたが使用したい、1つのクエリで複数のフィルタを受け入れる方法があります.
    複数の1つの目的質問の代わりに、あなたの質問にフィルタを使用したいと思う可能性が高いです.
    query {
        tests(filter: { 
        student: ["286712490662822407", "286712490702668289"], 
        project: ["286712490727835143"]
         }) {
          data {
            id: _id
            name
            student {
              id: _id
            }
          }
          after
          before
        }
      }
    }
    
    そのためには、各フィルタに対するインデックスを作成する必要があります.スキーマを使用した例
    # schema.graphql
    #...
    input TestFilters {
      project: [ID]
      student: [ID]
    }
    
    type Query {
      # ...
      tests(filter: TestFilters): [Test] @resolver(name: "get_tests", paginated: true)
      #...
    }
    
    // get_tests.fql
    Query(
      Lambda(
        ["filters", "size", "after", "before"],
        Let(
          {
            baseMatch: Match(Index("tests")),
            // creates match for every id in in filter.project array
            matchByProjects: Map(
              Select("project", Var("filters"), []),
              Lambda(
                "id",
                Match(
                  Index("project_tests_by_project"),
                  Ref(Collection("Project"), Var("id"))
                )
              )
            ),
            // creates match for every id in in filter.student array
            matchByStudents: Map(
              Select("student", Var("filters"), []),
              Lambda(
                "id",
                Match(
                  Index("student_tests_by_student"),
                  Ref(Collection("Student"), Var("id"))
                )
              )
            ),
            // combines all matches into one array
            // end up with [baseMatch, Union([projects]), Union([students])]
          match: Reduce(
              Lambda(
                ["acc", "curr"],
                If(
                  IsArray(Var("curr")),
                  If(
                    // skips if empty
                    IsEmpty(Var("curr")),
                    Var("acc"),
                    Append(Union(Var("curr")), Var("acc"))
                  ),
                  If(
                    IsNull(Var("curr")),
                    Var("acc"),
                    Append([Var("curr")], Var("acc")),
                  )
                )
              ),
              [],
              [
                Var("baseMatch"),
                Var("matchByProjects"), 
                Var("matchByStudents")
              ]
            ),
            intersectionMatch: Intersection(Var("match")),
            item: If(
              Equals(Var("before"), null),
              If(
                Equals(Var("after"), null),
                Paginate(Var("intersectionMatch"), { size: Var("size") }),
                Paginate(Var("intersectionMatch"), {
                  after: Var("after"),
                  size: Var("size")
                })
              ),
              Paginate(Var("intersectionMatch"), {
                before: Var("before"),
                size: Var("size")
              })
            )
          },
          Map(Var("item"), Lambda("ref", Get(Var("ref"))))
        )
      )
    )
    
    その場合は、同じクエリを使用して多くの要求をカバーすることができますし、以下の機能を維持する必要があります.
    私は個人的に1つの目的のリゾルバから起動し、マルチフィルタレゾルバには、同じコレクションの多くのフィルタリゾルバを持って切り替えます.