Open15

graphql-codegen のいろいろなプラグイン・オプション・プリセットを試す

Masayuki IzumiMasayuki Izumi

typescript-operations plugin の inlineFragmentTypes

@graphql-codegen/cli の2.0.0 から入った
fragment から生成した型定義で、他の fragment の型定義を参照するのではなく展開するようになる(inline

https://github.com/dotansimha/graphql-code-generator/pull/6184

Masayuki IzumiMasayuki Izumi

combine を指定する(2.0.0 以前の挙動)

--- src/__generated__/000-basic/nested.ts       2022-03-24 15:54:06.000000000 +0900
+++ src/__generated__/001-inlineFragmentTypes-combine/nested.ts 2022-03-24 15:54:06.000000000 +0900
@@ -11,7 +11,7 @@
 export type PostHeaderFragment = {
   __typename?: "Post";
   title: string;
-  author: { __typename?: "User"; username: string; avatarUrl?: string | null };
+  author: { __typename?: "User" } & PostHeaderUserFragment;
 };
 
 export type GetPostHeaderQueryVariables = Types.Exact<{
@@ -23,12 +23,6 @@
   postById: {
     __typename?: "Post";
     id: string;
-    title: string;
-    author: {
-      __typename?: "User";
-      id: string;
-      username: string;
-      avatarUrl?: string | null;
-    };
-  };
+    author: { __typename?: "User"; id: string };
+  } & PostHeaderFragment;
 };

https://github.com/izumin5210-sandbox/graphql-codegen-sandbox/tree/e0beacf で確認)

Masayuki IzumiMasayuki Izumi

mask

--- src/__generated__/000-basic/nested.ts       2022-03-24 15:54:06.000000000 +0900
+++ src/__generated__/002-inlineFragmentTypes-mask/nested.ts    2022-03-24 15:54:06.000000000 +0900
@@ -6,13 +6,15 @@
   __typename?: "User";
   username: string;
   avatarUrl?: string | null;
-};
+} & { " $fragmentName": "PostHeaderUserFragment" };
 
 export type PostHeaderFragment = {
   __typename?: "Post";
   title: string;
-  author: { __typename?: "User"; username: string; avatarUrl?: string | null };
-};
+  author: { __typename?: "User" } & {
+    " $fragmentRefs": { PostHeaderUserFragment: PostHeaderUserFragment };
+  };
+} & { " $fragmentName": "PostHeaderFragment" };
 
 export type GetPostHeaderQueryVariables = Types.Exact<{
   postId: Types.Scalars["String"];
@@ -23,12 +25,6 @@
   postById: {
     __typename?: "Post";
     id: string;
-    title: string;
-    author: {
-      __typename?: "User";
-      id: string;
-      username: string;
-      avatarUrl?: string | null;
-    };
-  };
+    author: { __typename?: "User"; id: string };
+  } & { " $fragmentRefs": { PostHeaderFragment: PostHeaderFragment } };
 };

https://github.com/izumin5210-sandbox/graphql-codegen-sandbox/tree/e0beacf で確認)

Masayuki IzumiMasayuki Izumi

mask を指定すると、そもそも子 fragment で参照されたフィールドに直接アクセスが不可能となる

Masayuki IzumiMasayuki Izumi

dedupeFragments

「データ転送量削減のための fragment 重複排除」
1つのクエリに同一のフラグメント定義が複数存在する場合に、その重複を排除する
いくつかのフラグメントでばらばらに同一フラグメントを参照すると起きる

Masayuki IzumiMasayuki Izumi

有効にすると、typed-document-node の出力が変わる

  • Fragment に別の Fragment の document を埋め込むのをやめる
  • 代わりに、Query や Mutaiton の document に依存 Fragment すべてを埋め込む

まあ大体正しく動くんだけど、Fragment の document が単体で利用できなくなるので困るケースもありそう
具体的には Apollo の readFragment / writeFragment だったり、graphql-anywhere のような fragment masking 実装は正常に動かなくなる気がする

--- src/__generated__/000-basic-with-typed-document-node/nested.ts      2022-03-24 16:18:13.000000000 +0900
+++ src/__generated__/003-typed-document-node-dedupe/nested.ts  2022-03-24 16:18:13.000000000 +0900
@@ -84,7 +84,6 @@
         ],
       },
     },
-    ...PostHeaderUserFragmentDoc.definitions,
   ],
 } as unknown as DocumentNode<PostHeaderFragment, unknown>;
 export const GetPostHeaderDocument = {
@@ -151,5 +150,6 @@
       },
     },
     ...PostHeaderFragmentDoc.definitions,
+    ...PostHeaderUserFragmentDoc.definitions,
   ],
 } as unknown as DocumentNode<GetPostHeaderQuery, GetPostHeaderQueryVariables>;

typescript-operations 自体には作用しない… そりゃそうなんだけど、じゃあなんで typescript-operations のおぷしょんになってるんだろう
→ operations だけでなく、(@graphql-codegen/visotor-plugin-common).Visitor を利用するすべてのプラグインが持つオプションかも

https://github.com/izumin5210-sandbox/graphql-codegen-sandbox/tree/e0beacf で確認)

Masayuki IzumiMasayuki Izumi
Masayuki IzumiMasayuki Izumi

Fragment 定義をそのまま文字列化したものをキーにし、値に Fragment の DocumentNode を持つ object を生成する
この object をもとにして激ヤバオーバーロードして gql 関数を生成することで、gql に Document 文字列を渡せば DocumentNode が返ってくる関数となる

Masayuki IzumiMasayuki Izumi

生成される DocumentNode は TypedDocumentNode になる
@graphql-codegen/typed-document-node@graphql-codegen/typescript-operations を内包しているイメージか

Masayuki IzumiMasayuki Izumi

gql-tag-operations preset の fragmentMasking

Masayuki IzumiMasayuki Izumi

useFragment 関数が生成される

// https://github.com/izumin5210-sandbox/graphql-codegen-sandbox/blob/f28b49b/src/__generated__/005-gql-tag-operations-preset-fragment-masking/fragment-masking.ts

export type FragmentType<TDocumentType extends DocumentNode<any, any>> =
  TDocumentType extends DocumentNode<infer TType, any>
    ? TType extends { " $fragmentName": infer TKey }
      ? TKey extends string
        ? { " $fragmentRefs": { [key in TKey]: TType } }
        : never
      : never
    : never;

export function useFragment<TType>(
  _documentNode: DocumentNode<TType, any>,
  fragmentType: FragmentType<DocumentNode<TType, any>>
): TType {
  return fragmentType as any;
}

型定義にも diff が発生する
typescript-operations plugin で inlineFragmentTypes: mask にしたのと同じ状態かな?

`fragmentMasking` の有無による生成物の違い
Only in src/__generated__/005-gql-tag-operations-preset-fragment-masking: fragment-masking.ts
diff -u src/__generated__/004-gql-tag-operations-preset/graphql.ts src/__generated__/005-gql-tag-operations-preset-fragment-masking/graphql.ts
--- src/__generated__/004-gql-tag-operations-preset/graphql.ts  2022-03-24 16:44:18.000000000 +0900
+++ src/__generated__/005-gql-tag-operations-preset-fragment-masking/graphql.ts 2022-03-24 16:44:18.000000000 +0900
@@ -85,7 +85,7 @@
   title: string;
   body: string;
   user: { __typename?: "User"; username: string; thumbnailUrl?: string | null };
-};
+} & { " $fragmentName": "PostSummaryFragment" };
 
 export type GetPostSummaryQueryVariables = Exact<{
   postId: Scalars["String"];
@@ -93,16 +93,8 @@
 
 export type GetPostSummaryQuery = {
   __typename?: "Query";
-  postById: {
-    __typename?: "Post";
-    id: string;
-    title: string;
-    body: string;
-    user: {
-      __typename?: "User";
-      username: string;
-      thumbnailUrl?: string | null;
-    };
+  postById: { __typename?: "Post"; id: string } & {
+    " $fragmentRefs": { PostSummaryFragment: PostSummaryFragment };
   };
 };
 
@@ -110,7 +102,7 @@
   __typename?: "Post";
   title: string;
   author: { __typename?: "User"; username: string };
-};
+} & { " $fragmentName": "PostWithAuthorFragment" };
 
 export type GetPostWithAuthorQueryVariables = Exact<{
   postId: Scalars["String"];
@@ -118,31 +110,31 @@
 
 export type GetPostWithAuthorQuery = {
   __typename?: "Query";
-  postById: {
-    __typename?: "Post";
-    id: string;
-    title: string;
-    author: { __typename?: "User"; username: string };
+  postById: { __typename?: "Post"; id: string } & {
+    " $fragmentRefs": { PostWithAuthorFragment: PostWithAuthorFragment };
   };
 };
 
 export type PostUserAvatarFragment = {
   __typename?: "Post";
   author: { __typename?: "User"; avatarUrl?: string | null };
-};
+} & { " $fragmentName": "PostUserAvatarFragment" };
 
-export type PostDetailHeaderFragment = {
+export type PostDetailHeaderFragment = ({
   __typename?: "Post";
   title: string;
-  author: { __typename?: "User"; avatarUrl?: string | null };
-};
+} & {
+  " $fragmentRefs": { PostUserAvatarFragment: PostUserAvatarFragment };
+}) & { " $fragmentName": "PostDetailHeaderFragment" };
 
-export type PostDetailFragment = {
+export type PostDetailFragment = ({
   __typename?: "Post";
   title: string;
   body: string;
-  author: { __typename?: "User"; username: string; avatarUrl?: string | null };
-};
+  author: { __typename?: "User"; username: string };
+} & {
+  " $fragmentRefs": { PostUserAvatarFragment: PostUserAvatarFragment };
+}) & { " $fragmentName": "PostDetailFragment" };
 
 export type GetPostDetailQueryVariables = Exact<{
   postId: Scalars["String"];
@@ -150,15 +142,10 @@
 
 export type GetPostDetailQuery = {
   __typename?: "Query";
-  postById: {
-    __typename?: "Post";
-    id: string;
-    title: string;
-    body: string;
-    author: {
-      __typename?: "User";
-      username: string;
-      avatarUrl?: string | null;
+  postById: { __typename?: "Post"; id: string } & {
+    " $fragmentRefs": {
+      PostDetailFragment: PostDetailFragment;
+      PostDetailHeaderFragment: PostDetailHeaderFragment;
     };
   };
 };
@@ -167,13 +154,15 @@
   __typename?: "User";
   username: string;
   avatarUrl?: string | null;
-};
+} & { " $fragmentName": "PostHeaderUserFragment" };
 
 export type PostHeaderFragment = {
   __typename?: "Post";
   title: string;
-  author: { __typename?: "User"; username: string; avatarUrl?: string | null };
-};
+  author: { __typename?: "User" } & {
+    " $fragmentRefs": { PostHeaderUserFragment: PostHeaderUserFragment };
+  };
+} & { " $fragmentName": "PostHeaderFragment" };
 
 export type GetPostHeaderQueryVariables = Exact<{
   postId: Scalars["String"];
@@ -184,14 +173,8 @@
   postById: {
     __typename?: "Post";
     id: string;
-    title: string;
-    author: {
-      __typename?: "User";
-      id: string;
-      username: string;
-      avatarUrl?: string | null;
-    };
-  };
+    author: { __typename?: "User"; id: string };
+  } & { " $fragmentRefs": { PostHeaderFragment: PostHeaderFragment } };
 };
 
 export type PostWithCommentsFragment = {
@@ -199,7 +182,7 @@
   title: string;
   body: string;
   comments: Array<{ __typename?: "Comment"; body: string }>;
-};
+} & { " $fragmentName": "PostWithCommentsFragment" };
 
 export type GetPostWithCommentsQueryVariables = Exact<{
   postId: Scalars["String"];
@@ -210,9 +193,9 @@
   postById: {
     __typename?: "Post";
     id: string;
-    title: string;
-    body: string;
-    comments: Array<{ __typename?: "Comment"; id: string; body: string }>;
+    comments: Array<{ __typename?: "Comment"; id: string }>;
+  } & {
+    " $fragmentRefs": { PostWithCommentsFragment: PostWithCommentsFragment };
   };
 };
 
@@ -225,7 +208,7 @@
     avatarUrl?: string | null;
     username: string;
   };
-};
+} & { " $fragmentName": "PostListItemFragment" };
 
 export type ListPostsQueryVariables = Exact<{
   userId: Scalars["String"];
@@ -233,24 +216,18 @@
 
 export type ListPostsQuery = {
   __typename?: "Query";
-  postsByUserId: Array<{
-    __typename?: "Post";
-    id: string;
-    title: string;
-    author: {
-      __typename?: "User";
-      id: string;
-      avatarUrl?: string | null;
-      username: string;
-    };
-  }>;
+  postsByUserId: Array<
+    { __typename?: "Post"; id: string } & {
+      " $fragmentRefs": { PostListItemFragment: PostListItemFragment };
+    }
+  >;
 };
 
 export type UserHeaderFragment = {
   __typename?: "User";
   username: string;
   avatarUrl?: string | null;
-};
+} & { " $fragmentName": "UserHeaderFragment" };
 
 export type GetUserHeaderQueryVariables = Exact<{
   userId: Scalars["String"];
@@ -258,26 +235,31 @@
 
 export type GetUserHeaderQuery = {
   __typename?: "Query";
-  userById: {
-    __typename?: "User";
-    id: string;
-    username: string;
-    avatarUrl?: string | null;
+  userById: { __typename?: "User"; id: string } & {
+    " $fragmentRefs": { UserHeaderFragment: UserHeaderFragment };
   };
 };
 
-export type PostImageFragment = { __typename?: "Image"; imageUrl: string };
+export type PostImageFragment = { __typename?: "Image"; imageUrl: string } & {
+  " $fragmentName": "PostImageFragment";
+};
 
-export type PostVideoFragment = { __typename?: "Video"; videoUrl: string };
+export type PostVideoFragment = { __typename?: "Video"; videoUrl: string } & {
+  " $fragmentName": "PostVideoFragment";
+};
 
 export type PostWithAttachmentsFragment = {
   __typename?: "Post";
   title: string;
   attachmentFiles: Array<
-    | { __typename?: "Image"; imageUrl: string }
-    | { __typename?: "Video"; videoUrl: string }
+    | ({ __typename?: "Image" } & {
+        " $fragmentRefs": { PostImageFragment: PostImageFragment };
+      })
+    | ({ __typename?: "Video" } & {
+        " $fragmentRefs": { PostVideoFragment: PostVideoFragment };
+      })
   >;
-};
+} & { " $fragmentName": "PostWithAttachmentsFragment" };
 
 export type GetPostWithAttachmentsQueryVariables = Exact<{
   postId: Scalars["String"];
@@ -285,13 +267,10 @@
 
 export type GetPostWithAttachmentsQuery = {
   __typename?: "Query";
-  postById: {
-    __typename?: "Post";
-    title: string;
-    attachmentFiles: Array<
-      | { __typename?: "Image"; imageUrl: string }
-      | { __typename?: "Video"; videoUrl: string }
-    >;
+  postById: { __typename?: "Post" } & {
+    " $fragmentRefs": {
+      PostWithAttachmentsFragment: PostWithAttachmentsFragment;
+    };
   };
 };
 
diff -u src/__generated__/004-gql-tag-operations-preset/index.ts src/__generated__/005-gql-tag-operations-preset-fragment-masking/index.ts
--- src/__generated__/004-gql-tag-operations-preset/index.ts    2022-03-24 16:44:18.000000000 +0900
+++ src/__generated__/005-gql-tag-operations-preset-fragment-masking/index.ts   2022-03-24 16:44:18.000000000 +0900
@@ -1 +1,2 @@
 export * from "./gql";
+export * from "./fragment-masking";
Masayuki IzumiMasayuki Izumi

useFragment 自体は何もしない(型変換してるだけ)
gql-tag-operations の生成コードにも依存してないので、typescript-operations と組み合わせても使えそう

Masayuki IzumiMasayuki Izumi

gql-tag-operations の augmentedModuleName

Masayuki IzumiMasayuki Izumi

presetConfig で augmentedModuleName および fragmentMasking.augmentedModuleName として設定できる
それぞれ gql.tsfragment-masking.ts のコード生成が declare module "<name>" { ... } の形に変化する

既存のツールを使いつつ gql-tag-operations の恩恵を受けられるようにするもの…かな?