GraphSQLツールを使用したGraphSQL Liveクエリリソース識別子の収集


写真で Łukasz Nieścioruk on Unsplash
GraphSQLライブクエリは、GraphSQLのサブスクリプションよりもエレガントな方法でリアルタイム更新を解決することができます.
イベントライブクエリを購読する代わりに、主にデータ変更を購読します.
クライアント・ストアを手動で更新する代わりに、ライブ・クエリは任意の冗長キャッシュ更新ロジックなしで魔法のようにクライアント・ストアを更新します.
You can learn more about the differences here
しかしながら、すべてのそれらの利点は、クライアントが、クライアントの操作がすべてのデータを意識していて、特定のクライアントのためにそれらの問合せ操作を再実行して、再実行するすべてのデータを意識しなければならないサーバーの欠点とともに来ます.
私が最初にGraphSQL Liveクエリを使って実験を始めたとき、最も簡単な解決策はQuery オブジェクト型ルートフィールド.例えば、SELECT SELECT SELECTを選択したクエリQuery.viewer フィールドは、Query.viewer ライブクエリストアイベントエミッタを介してイベント.しかしながら、ビューアーは与えられたクエリー操作を消費する各々のクライアントのための完全に異なる記録/リソースでありえる.
より明確にするには、対応するスキーマがあります.
type User {
  id: ID!
  login: String!
}

type Query {
  """
  Returns the authenticated user. Returns null in case the user is not authenticated.
  """
  viewer: User
  """
  List of the users that are currently online.
  """
  onlineUsers: [User!]!
}

type Mutation {
  updateLogin(newLogin: String!): Boolean!
}

query viewer @live {
  viewer {
    id
    login
  }
}
この実装がどのように見えるかを見てみましょう.
const Query = {
  viewer: (source, args, context) => {
    return context.viewer;
  },
};

const Mutation = {
  updateLogin: async (source, args, context) => {
    await context.db.updateUser(
      context.viewer.id,
      args.newLogin
    );

    context.liveQueryStore.invalidate(
      `Query.viewer`
    );
    return true;
  },
};
特定のユーザーが彼のログインをアップデートするならば、我々は無効にして、その変化によって影響を受けさえしないかもしれないどんな接続されたユーザーのためにでもセットされる視聴者選択を持っているどんなライブ質問操作も再実行してはいけません!

同時に、他の操作(例えば利用可能な全てのユーザーのリスト)で、ユーザーも参照されることができたQuery.onlineUsers ). The Query.viewer イベントは、そのフィールドを介してユーザーを選択する操作の再実行をカバーし、スケジュールしません.

選択セットデータを一意に識別するためのより良い解決策がなければなりません


おそらく、ユーザーが気づいたようにid フィールドオブザID! (非NULLのID )型.クライアント側のリソースを一意に識別するための一般的なフィールドです.アポロクライアントは__typename と組み合わせてフィールドid フィールドリソースキャッシュキーUser:1 ), リレーはさらに一歩進んで、すでにリソースタイプがコード化されていると仮定します.base64("User:1") 注意: idの内部でのみidフィールドを使用します.
私たちのライブクエリストアの実装でサーバー側にそのような識別子を使用することができればどうでしょうか?
私の現在の実装は、クエリ操作のASTをちょうど横断し、schema coordinates rootクエリ型で.例えばQuery.viewer のためにviewer 上記からのライブクエリ操作.

しかし、IDを介してユーザを識別したい場合は、次のように追加しなければなりませんUser:1 リソースの集合には、ライブクエリ操作が選択されます.これは、どのタイプがIDフィールドを持っているかを知っている必要があり、選択セットに含まれている場合、対応するリソース識別子を収集するために、スキーマの知識が必要です.

前述のように、これはより粒状のクエリ無効化を可能にします.

私が考えた最初の欠点は、操作がid 選択セットのフィールドは、リソースクエリストアで追跡できません.
しかし、ほとんどの操作はおそらくid フィールドがキャッシュキーのクライアントで使用されます.
さらに、このような方法で簡単にクエリを変換することができますid フィールドはセレクションセットに追加されます__typename 各オブジェクトの型への選択.
単純なものを保つためには、LIVEクエリ操作を送信するクライアントにIDフィールドを選択するための責任を押すことにしました.私は、存在しなかった私の既存のアプリケーションのユースケースも見つけることができませんでしたid リソースの選択👍.

リソース識別子コレクタの実装


次の障害はIDがどのように抽出されるかを決定することであり、私は2つのオプションを念頭に置いていました.

GraphSQL実行結果ツリーを横切って


操作のASTとスキーマに基づいて、それぞれの葉のタイプを推測/チェックしている間、私は結果全体を横断する必要があるので、これは私にとって複雑に思えました.私はすぐにその考えを落とした.

コンテキストを通じて注入される関数を呼び出すことで、リソース識別子を手動で登録する


私のライブクエリストアの実装の目標は、最小限の労力で任意のスキーマにライブクエリのサポートを追加することです.ライブラリのユーザーがクエリリゾルバ内で呼び出す必要があるコンテキストに沿って何かを渡すことは間違っているように見えました.
オブジェクトタイプを返す各リゾルバに手動でリソースを登録しなければならないかを想像してください.
const Query = {
  viewer: (source, args, context) => {
    const viewer = context.viewer;
    context.registerResource(`User:${viewer.id}`);
    return viewer;
  },
};
これは、単一のリゾルバのために非常に簡単に見えるかもしれませんが、それはすぐに混乱することができますし、バグを引き起こす場合は、手動で任意のリゾルバの任意のリソースを行う必要があります.
理想的には、ライブラリのユーザはcontext.liveQueryStore.invalidate("User:1") 行をupdateLogin 突然変異フィールドレゾルバは、各リゾルバに追加の関数呼び出しを追加するオーバーヘッドなしで、操作再実行を魔法的にスケジュールするために.
const Query = {
  viewer: (source, args, context) => {
    // No tracking registration code here.
    return context.viewer;
  },
};

const Mutation = {
  updateLogin: async (source, args, context) => {
    await context.db.updateUser(
      context.viewer.id,
      args.newLogin
    );

    context.liveQueryStore.invalidate(
      `User:${context.viewer.id}`
    );
    return true;
  },
};
それで、私は、これがより冗長な方法でどのように実装されることができるかについてもっと考えました.
他のフィールドとしてid フィールドはレゾルバ( Graphqlまたはユーザ定義のリゾルバによって提供されるデフォルトリゾルバのいずれか)を持っているので、id 問題を解決できる機能を持つフィールドリゾルバ.ラッパーは、実際のレゾルバを呼び出すことができますリソースを登録し、値を返します.ユーザーは何も気にする必要はありませんid フィールドを指定します.
GraphSQLスキーマの変換と修正のための最良のライブラリです graphql-tools . 幸いにも、それは現在ギルドによって維持されます.
だから私は少し派手なドキュメントに掘り下げ、すぐに必要なものを見つけました.@graphql-tools/wrap .
ドキュメントからの迅速な抜粋

Schema wrapping is a method of making modified copies of GraphQLSchema objects, without changing the original schema implementation.


スキーマは、“通常の”クエリ/突然変異/サブスクリプション操作に使用されるように.ラッピングのオーバーヘッドが欲しくなかったid 非ライブクエリ操作のフィールド.
TransformObjectFields 変換は、スキーマフィールドをラップするのはかなりまっすぐです.
import {
  GraphQLSchema,
  isScalarType,
  isNonNullType,
  GraphQLOutputType,
  GraphQLScalarType,
  execute,
} from "graphql";
import { wrapSchema, TransformObjectFields } from "@graphql-tools/wrap";

const isNonNullIDScalarType = (
  type: GraphQLOutputType
): type is GraphQLScalarType => {
  if (isNonNullType(type)) {
    return isScalarType(type.ofType) && type.ofType.name === "ID";
  }
  return false;
};

const addResourceIdentifierCollectorToSchema = (
  schema: GraphQLSchema
): GraphQLSchema =>
  wrapSchema(schema, [
    new TransformObjectFields((typename, fieldName, fieldConfig) => {
      let isIDField = fieldName === "id" && isNonNullIDScalarType(fieldConfig.type);

      let resolve = fieldConfig.resolve;
      fieldConfig.resolve = (src, args, context, info) => {
        if (!context || !context[ORIGINAL_CONTEXT_SYMBOL]) {
          return resolve(src, args, context, info);
        }

        const collectResourceIdentifier = context.collectResourceIdentifier;
        context = context[ORIGINAL_CONTEXT_SYMBOL];
        const result = resolve(src, args, context, info);
        if (isIDField) {
          if (isPromise(result)) {
            result.then(
              (value) => collectResourceIdentifier({ typename, value }),
              () => undefined
            );
          } else {
            collectResourceIdentifier({ typename, result });
          }
        }
        return result;
      };

      return fieldConfig;
    }),
  ]);
操作を実行するための実装は次のようになります.
const newIdentifier = new Set(rootFieldIdentifier);
const collectResourceIdentifier: ResourceGatherFunction = ({ typename, id }) =>
  // for a relay spec conform server the typename could even be omitted :)
  newIdentifier.add(`${typename}:${id}`);

// You definitely wanna cache the wrapped schema as you don't want to re-create it for each operation :)
const wrappedSchema = addResourceIdentifierCollectorToSchema(schema);

const result = execute({
  schema: wrappedSchema,
  document: operationDocument,
  operationName,
  rootValue,
  contextValue: {
    [ORIGINAL_CONTEXT_SYMBOL]: contextValue,
    collectResourceIdentifier,
  },
  variableValues: operationVariables,
});
文脈における「ユーザ」コンテキストをラップしなければならなかった🤯) リソース識別子をリソース識別子セットに追加する関数も添付しました.私はそれがResolver実行時間を測定する方法を持っていることを知っていたので、アポロサーバのソースコードによってこれにインスパイアされました.このメソッドは、各実行のための新しい関数/コンテキストを使用できます.フィールドリゾルバの中で、正しいユーザーコンテキストは、実際の(ユーザ)フィールドリゾルバに渡されます.
スキーマに対する操作を実行した後にnewIdentifier setは、操作中に解決されたすべてのリソースの識別子を含むべきです.
ライブクエリストアは、リソース識別子のイベントが発行されるとクエリを再実行するための情報を使用することができます👌.

結論


リソースを識別し、クエリルートフィールドベースではなくリソースベースに基づいてクエリを無効にすると、より効率的なクエリの再実行を可能にし、クライアントに不要な更新を押すことを避けることができます.
Graphicsのツールは、問題の巨大な様々な解決に使用できる超便利なライブラリです.私はそれがこのような巨大な更新プログラムと良いドキュメントを得たうれしいです!
実装はおそらくすべてのユースケースをカバーしません.クライアントが認証されていない場合、およびQuery.viewer リゾルバnull . ないUser:ID ユーザーが認証した後、ライブクエリストア操作コンテキストで使用可能な文字列.どちらかQuery.viewer 更新は、ライブクエリストアのエミッタを介して送信される必要がありますviewer ), クライアントは、ログイン後の操作を再実行しなければならず、あるいは、ライブクエリストアは、単に認証されたユーザのすべての操作を再実行するように通知する必要があります.
実装チェックアウトのソースコードに興味がある場合はhttps://github.com/n1ru4l/graphql-live-queries/pull/94
まだ発見し、ライブクエリの土地で構築するより多くです!
我々はまだ、リソースが無効にされなければならないことをライブクエリストアに通知する必要があります.舞台裏でこれをするための抽象化は、異なるスタックのために非常に異なることができました.
たぶんORM/データベースストア層はイベントを放出するかもしれません、あるいは、プロキシはデータベース操作に基づくそれらの出来事を放出することができましたINSERT , DELETE , and UPDATE .
クエリ操作を再実行するのは、素晴らしいですが、最も効率的な解決策ではありません.私たちが特定のレゾルバを再実行することができるならば、どうですか?私はすでに心にいくつかのアイデアを持っていると私はおそらくそれについても書くでしょう!
あなたが一般的にライブクエリまたはGraphSQLについて議論するのに興味があるならば、さえずりの上で、または、以下のコメントを書くことによって私に連絡してください🙂. また、興味があるかもしれない人々と記事を共有することを考えてください😉.