[Keystone] フック

フック

Keystone は強力な CRUD GraphQL API を提供しており、データに対して基本的な操作を実行することができます。しかし、システムが進化するにつれて、これらの操作に加えてビジネスロジックを含める必要がある場合があります。
フックを使用して、コア操作をさまざまな方法で強化する方法を紹介します。関数のシグネチャの詳細については、Hooks API を確認してください。

フックとは

フックは、スキーマ構成の一部として定義された関数であり、GraphQL 操作が実行されたときに実行されます。基本的な例を見て、新しいユーザーが作成されるたびにコンソールにメッセージを記録する方法を確認しましょう。

import { config, list } from '@keystone-6/core';
import { text } from '@keystone-6/core/fields';

export default config({
  lists: {
    User: list({
      fields: {
        name: text(),
        email: text(),
       },
      hooks: {
        afterOperation: ({ operation, item }) => {
          if (operation === 'create') {
            console.log(`New user created. Name: ${item.name}, Email: ${item.email}`);
          }
        }
      },
    }),
  },
});

この関数は、GraphQL API の作成、更新、削除のミューテーションが実行されるたびにトリガーされます。操作対象の各アイテムごとに 1 回実行されます。ユーザーが作成されたときにのみログを取得するために、operation 引数の値をチェックします。次に、item 引数を使用して、新しく作成されたユーザーの値を取得します。
フックの概要がわかったところで、システムを作成する際に遭遇する一般的な問題を解決するために、フックをどのように使用できるかを見てみましょう。

受信データの変更

作成または更新の操作が呼び出された場合、データを保存する前にいくつかの前処理を適用したい場合があります。例えば、ブログ投稿がある場合、タイトルフィールドが常に大文字で始まるようにする必要があるかもしれません。resolveInput フックを使用すると、GraphQL ミューテーションに提供されたデータを取得して、保存する前に変更することができます。
ブログ投稿のデータを取り、最初の文字を大文字に変換するフックを書いてみましょう。

import { config, list } from '@keystone-6/core';
import { text } from '@keystone-6/core/fields';

export default config({
  lists: {
    Post: list({
      fields: {
        title: text({ validation: { isRequired: true } }),
        content: text({ validation: { isRequired: true } }),
       },
      hooks: {
        resolveInput: ({ resolvedData }) => {
          const { title } = resolvedData;
          if (title) {
            return {
              ...resolvedData,
              // Ensure the first letter of the title is capitalised
              title: title[0].toUpperCase() + title.slice(1)
            }
          }
          // We always return resolvedData from the resolveInput hook
          return resolvedData;
        }
      },
    }),
  },
});

resolveInput フックは、アイテムを更新または作成するたびに呼び出されます。resolvedData の値には、フィールドタイプの入力リゾルバが適用された入力自体が含まれます。たとえば、パスワードフィールドは、提供されたテキスト入力を暗号化されたハッシュ値に変換します。フィールドタイプのリゾルバが適用される前の元の入力を確認する場合は、inputData 引数を使用できます。
更新操作を実行する場合、データベースに格納されているアイテムの現在の値にアクセスすることもできます。これは item 引数として利用可能です。
最後に、すべてのフックには、完全なコンテキスト API にアクセスできる context 引数が提供されます。

入力の検証

データをデータベースに書き込む前に、アプリケーションのニーズに応じて、特定のルールに従うことを確認する必要があります。たとえば、ブログ投稿に空のタイトルがないことを確認したい場合があります。空の文字列、"" は GraphQL に渡すのに完全に有効な文字列値です。バリデーションフックを使用して、この値がデータベースに入らないようにします。

import { config, list } from '@keystone-6/core';
import { text } from '@keystone-6/core/fields';

export default config({
  lists: {
    Post: list({
      fields: {
        title: text({ validation: { isRequired: true } }),
        content: text({ validation: { isRequired: true } }),
       },
      hooks: {
        validateInput: ({ resolvedData, addValidationError }) => {
          const { title } = resolvedData;
          if (title === '') {
            // We call addValidationError to indicate an invalid value.
            addValidationError('The title of a blog post cannot be the empty string');
          }
        }
      },
    }),
  },
});

validateInput フックは、すべてのデフォルトと resolveInput フックが完了した後の resolvedData 値を受け取ります。これは、バリデーションエラーが見つからない場合にデータベースに書き込まれる値です。
このオブジェクトの値をチェックし、問題がある場合は、addValidationError 関数をエラーメッセージとともに呼び出します。入力には複数の問題がある可能性があるため、異なる問題すべてをキャプチャするために、addValidationError を複数回呼び出すことができます。
Keystone は操作を中止し、これらのエラーメッセージを GraphQL エラーに変換して呼び出し元に返します。
validateInput フックは、より高度なチェックを行いたい場合に、操作、inputData、item、context 引数を受け取ります。

サイドエフェクトをトリガーする

データがシステム内で変更されたときに、外部の副作用をトリガーしたい場合があります。例えば、ユーザーがアカウントを作成したときに、ウェルカムメールを送信したい場合があります。これを行うために、beforeOperation フックと afterOperation フックを使用することができます。ここでは、ユーザーが作成された後にメールを送信するフックを書いてみましょう。

import { config, list } from '@keystone-6/core';
import { text } from '@keystone-6/core/fields';
// Keystone leaves it up to you to decide how best to implement email in your system
import { sendWelcomeEmail } from './lib/welcomeEmail';

export default config({
  lists: {
    User: list({
      fields: {
        name: text(),
        email: text(),
       },
      hooks: {
        afterOperation: ({ operation, item }) => {
          if (operation === 'create') {
            sendWelcomeEmail(item.name, item.email);
          }
        }
      },
    }),
  },
});

beforeOperation と afterOperation のフックは非常に似ていますが、やや異なる目的に役立ちます。beforeOperation フックは item 引数を受け取り、これは操作前にデータベースに格納されている現在のデータを含みます。afterOperation フックでは、item はデータベース内で更新された新しいデータを表し、更新前の元のデータは originalItem として提供されます。create 操作では、事前に存在するアイテムはありません。削除操作では、afterOperation フックの item の値は null になります。
beforeOperation フックが例外をスローすると、操作はエラーを返し、データはデータベースに保存されません。afterOperation フックが例外をスローすると、データはデータベースに残ります。そのため、afterOperation フックは、実行の失敗が致命的な問題でない場合に使用する必要があります。

リストフックとフィールドフック

これまでの例では、特定のリストに関連付けられたフックについて説明しました。Keystone では、特定のフィールドに関連付けられたフックの設定もサポートしています。すべてのフックが利用可能で、すべて同じ引数が渡されますが、追加の fieldKey 引数があります。
フィールドフックは、フィールド固有のルールが必要な場合に役立ちます。例えば、システムで使用する電子メールのバリデーション関数がある場合、常にリストフックとして書くことができますが、フィールドフックとして書いた方がコードがより明確になります。

import { config, list } from '@keystone-6/core';
import { text } from '@keystone-6/core/fields';

export default config({
  lists: {
    User: list({
      fields: {
        name: text(),
        email: text({
          validation: { isRequired: true },
          hooks: {
            validateInput: ({ addValidationError, resolvedData, fieldKey }) => {
              const email = resolvedData[fieldKey];
              if (email !== undefined && email !== null && !email.includes('@')) {
                addValidationError(`The email address ${email} provided for the field ${fieldKey} must contain an '@' character`);
              }
            },
          },
        }),
       },
    }),
  },
});

すべての異なるフック関数に利用可能な引数の詳細については、Hooks API を参照してください。