From 3848a2b73339fe9f474b31647b71e75b9ca52a96 Mon Sep 17 00:00:00 2001 From: Aleksandra Date: Tue, 2 May 2023 10:20:38 +0200 Subject: [PATCH] Add `@defer` directive support (#9196) --- .changeset/eleven-news-lay.md | 10 + .changeset/modern-clouds-prove.md | 5 + dev-test/githunt/typed-document-nodes.ts | 2 + dev-test/githunt/types.avoidOptionals.ts | 2 + dev-test/githunt/types.d.ts | 2 + dev-test/githunt/types.enumsAsTypes.ts | 2 + .../githunt/types.flatten.preResolveTypes.ts | 2 + dev-test/githunt/types.immutableTypes.ts | 2 + ...ypes.preResolveTypes.onlyOperationTypes.ts | 2 + dev-test/githunt/types.preResolveTypes.ts | 2 + dev-test/githunt/types.ts | 2 + .../gql/fragment-masking.ts | 22 +- .../gql-tag-operations-masking/gql/graphql.ts | 2 + .../gql/fragment-masking.ts | 22 +- .../gql-tag-operations-urql/gql/graphql.ts | 2 + .../gql/fragment-masking.ts | 22 +- dev-test/gql-tag-operations/gql/graphql.ts | 2 + .../graphql/fragment-masking.ts | 22 +- .../gql-tag-operations/graphql/graphql.ts | 2 + dev-test/modules/types.ts | 2 + dev-test/star-wars/types.avoidOptionals.ts | 2 + dev-test/star-wars/types.d.ts | 2 + .../star-wars/types.globallyAvailable.d.ts | 2 + dev-test/star-wars/types.immutableTypes.ts | 2 + ...ypes.preResolveTypes.onlyOperationTypes.ts | 2 + dev-test/star-wars/types.preResolveTypes.ts | 2 + dev-test/star-wars/types.skipSchema.ts | 2 + dev-test/star-wars/types.ts | 2 + dev-test/test-schema/env.types.ts | 2 + dev-test/test-schema/resolvers-federation.ts | 2 + dev-test/test-schema/resolvers-root.ts | 2 + dev-test/test-schema/resolvers-stitching.ts | 2 + dev-test/test-schema/resolvers-types.ts | 2 + ...ypes.preResolveTypes.onlyOperationTypes.ts | 2 + dev-test/test-schema/types.preResolveTypes.ts | 2 + .../test-schema/typings.avoidOptionals.ts | 2 + dev-test/test-schema/typings.enum.ts | 2 + .../test-schema/typings.immutableTypes.ts | 2 + dev-test/test-schema/typings.ts | 2 + dev-test/test-schema/typings.wrapped.ts | 2 + .../src/gql/fragment-masking.ts | 17 +- .../src/gql/graphql.ts | 4 +- .../src/gql/fragment-masking.ts | 22 +- .../persisted-documents/src/gql/graphql.ts | 2 + examples/react/apollo-client-defer/.gitignore | 23 + examples/react/apollo-client-defer/README.md | 17 + examples/react/apollo-client-defer/codegen.ts | 15 + .../apollo-client-defer/cypress.config.ts | 10 + .../cypress/e2e/end2end.cy.ts | 11 + .../cypress/support/commands.ts | 1 + .../cypress/support/e2e.ts | 2 + examples/react/apollo-client-defer/index.html | 13 + .../react/apollo-client-defer/package.json | 36 + .../react/apollo-client-defer/src/App.tsx | 55 ++ .../src/gql/fragment-masking.ts | 66 ++ .../react/apollo-client-defer/src/gql/gql.ts | 57 ++ .../apollo-client-defer/src/gql/graphql.ts | 138 ++++ .../apollo-client-defer/src/gql/index.ts | 2 + .../react/apollo-client-defer/src/main.tsx | 18 + .../react/apollo-client-defer/src/yoga.mjs | 55 ++ .../react/apollo-client-defer/tsconfig.json | 18 + .../apollo-client-defer/tsconfig.node.json | 9 + .../react/apollo-client-defer/vite.config.ts | 8 + .../apollo-client-swc-plugin/package.json | 2 +- .../src/gql/fragment-masking.ts | 22 +- .../src/gql/graphql.ts | 2 + examples/react/apollo-client/package.json | 2 +- .../apollo-client/src/gql/fragment-masking.ts | 22 +- .../react/apollo-client/src/gql/graphql.ts | 2 + .../http-executor/src/gql/fragment-masking.ts | 22 +- .../react/http-executor/src/gql/graphql.ts | 2 + .../react/nextjs-swr/gql/fragment-masking.ts | 22 +- examples/react/nextjs-swr/gql/graphql.ts | 2 + .../src/gql/fragment-masking.ts | 17 +- .../tanstack-react-query/src/gql/graphql.ts | 11 +- .../react/urql/src/gql/fragment-masking.ts | 17 +- examples/react/urql/src/gql/graphql.ts | 11 +- .../src/gql/fragment-masking.ts | 22 +- examples/typescript-esm/src/gql/graphql.ts | 2 + .../src/gql/fragment-masking.ts | 17 +- .../src/gql/graphql.ts | 4 +- .../typescript-resolvers/src/type-defs.d.ts | 2 + examples/vite/vite-react-cts/package.json | 2 +- .../src/gql/fragment-masking.ts | 22 +- .../vite/vite-react-cts/src/gql/graphql.ts | 2 + examples/vite/vite-react-mts/package.json | 2 +- .../src/gql/fragment-masking.ts | 22 +- .../vite/vite-react-mts/src/gql/graphql.ts | 2 + examples/vite/vite-react-ts/package.json | 2 +- .../vite-react-ts/src/gql/fragment-masking.ts | 22 +- .../vite/vite-react-ts/src/gql/graphql.ts | 2 + examples/vue/apollo-composable/package.json | 2 +- .../src/gql/fragment-masking.ts | 22 +- .../vue/apollo-composable/src/gql/graphql.ts | 2 + examples/vue/urql/src/gql/fragment-masking.ts | 22 +- examples/vue/urql/src/gql/graphql.ts | 2 + .../vue/villus/src/gql/fragment-masking.ts | 22 +- examples/vue/villus/src/gql/graphql.ts | 2 + .../yoga-tests/src/gql/fragment-masking.ts | 22 +- examples/yoga-tests/src/gql/graphql.ts | 2 + .../src/client-side-base-visitor.ts | 84 +- .../src/selection-set-processor/base.ts | 15 +- .../pre-resolve-types.ts | 38 +- .../src/selection-set-to-object.ts | 122 ++- .../other/visitor-plugin-common/src/types.ts | 6 +- .../other/visitor-plugin-common/src/utils.ts | 10 +- .../src/ts-selection-set-processor.ts | 7 +- .../typescript/operations/src/visitor.ts | 6 +- .../__snapshots__/ts-documents.spec.ts.snap | 8 + .../operations/tests/ts-documents.spec.ts | 717 ++++++++++++++++++ .../__snapshots__/ts-resolvers.spec.ts.snap | 4 + .../typed-document-node/src/index.ts | 2 +- .../typescript/typescript/src/visitor.ts | 12 + .../client/src/fragment-masking-plugin.ts | 82 +- packages/presets/client/src/index.ts | 4 +- .../client/tests/client-preset.spec.ts | 439 ++++++++++- .../tests/fixtures/with-deferred-fragment.ts | 26 + .../client/tests/fixtures/with-fragment.ts | 2 +- .../pages/plugins/presets/preset-client.mdx | 55 ++ yarn.lock | 11 +- 120 files changed, 2665 insertions(+), 134 deletions(-) create mode 100644 .changeset/eleven-news-lay.md create mode 100644 .changeset/modern-clouds-prove.md create mode 100644 examples/react/apollo-client-defer/.gitignore create mode 100644 examples/react/apollo-client-defer/README.md create mode 100644 examples/react/apollo-client-defer/codegen.ts create mode 100644 examples/react/apollo-client-defer/cypress.config.ts create mode 100644 examples/react/apollo-client-defer/cypress/e2e/end2end.cy.ts create mode 100644 examples/react/apollo-client-defer/cypress/support/commands.ts create mode 100644 examples/react/apollo-client-defer/cypress/support/e2e.ts create mode 100644 examples/react/apollo-client-defer/index.html create mode 100644 examples/react/apollo-client-defer/package.json create mode 100644 examples/react/apollo-client-defer/src/App.tsx create mode 100644 examples/react/apollo-client-defer/src/gql/fragment-masking.ts create mode 100644 examples/react/apollo-client-defer/src/gql/gql.ts create mode 100644 examples/react/apollo-client-defer/src/gql/graphql.ts create mode 100644 examples/react/apollo-client-defer/src/gql/index.ts create mode 100644 examples/react/apollo-client-defer/src/main.tsx create mode 100644 examples/react/apollo-client-defer/src/yoga.mjs create mode 100644 examples/react/apollo-client-defer/tsconfig.json create mode 100644 examples/react/apollo-client-defer/tsconfig.node.json create mode 100644 examples/react/apollo-client-defer/vite.config.ts create mode 100644 packages/presets/client/tests/fixtures/with-deferred-fragment.ts diff --git a/.changeset/eleven-news-lay.md b/.changeset/eleven-news-lay.md new file mode 100644 index 00000000000..90faab5de99 --- /dev/null +++ b/.changeset/eleven-news-lay.md @@ -0,0 +1,10 @@ +--- +'@graphql-codegen/typed-document-node': minor +'@graphql-codegen/visitor-plugin-common': minor +'@graphql-codegen/typescript-operations': minor +'@graphql-codegen/typescript': minor +'@graphql-codegen/typescript-resolvers': minor +'@graphql-codegen/client-preset': minor +--- + +Add `@defer` directive support diff --git a/.changeset/modern-clouds-prove.md b/.changeset/modern-clouds-prove.md new file mode 100644 index 00000000000..b4dfdd864fd --- /dev/null +++ b/.changeset/modern-clouds-prove.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/client-preset': patch +--- + +Pass `emitLegacyCommonJSImports` and `isStringDocumentMode` to the client preset config diff --git a/dev-test/githunt/typed-document-nodes.ts b/dev-test/githunt/typed-document-nodes.ts index 83d0a671e0c..8024a2d04e0 100644 --- a/dev-test/githunt/typed-document-nodes.ts +++ b/dev-test/githunt/typed-document-nodes.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.avoidOptionals.ts b/dev-test/githunt/types.avoidOptionals.ts index d10a57f2f7d..87fd3a832b0 100644 --- a/dev-test/githunt/types.avoidOptionals.ts +++ b/dev-test/githunt/types.avoidOptionals.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.d.ts b/dev-test/githunt/types.d.ts index c9fc2f34a23..918c2881788 100644 --- a/dev-test/githunt/types.d.ts +++ b/dev-test/githunt/types.d.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.enumsAsTypes.ts b/dev-test/githunt/types.enumsAsTypes.ts index c9fc2f34a23..918c2881788 100644 --- a/dev-test/githunt/types.enumsAsTypes.ts +++ b/dev-test/githunt/types.enumsAsTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.flatten.preResolveTypes.ts b/dev-test/githunt/types.flatten.preResolveTypes.ts index a05c999876e..60c6c2c8a2c 100644 --- a/dev-test/githunt/types.flatten.preResolveTypes.ts +++ b/dev-test/githunt/types.flatten.preResolveTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.immutableTypes.ts b/dev-test/githunt/types.immutableTypes.ts index 1da626f2b57..a138437caad 100644 --- a/dev-test/githunt/types.immutableTypes.ts +++ b/dev-test/githunt/types.immutableTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts index 118c86bfab5..ad963fff007 100644 --- a/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.preResolveTypes.ts b/dev-test/githunt/types.preResolveTypes.ts index 312db844b11..9196055488c 100644 --- a/dev-test/githunt/types.preResolveTypes.ts +++ b/dev-test/githunt/types.preResolveTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/githunt/types.ts b/dev-test/githunt/types.ts index 312db844b11..9196055488c 100644 --- a/dev-test/githunt/types.ts +++ b/dev-test/githunt/types.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/gql-tag-operations-masking/gql/fragment-masking.ts b/dev-test/gql-tag-operations-masking/gql/fragment-masking.ts index dc2836d43e3..0cedfd53e94 100644 --- a/dev-test/gql-tag-operations-masking/gql/fragment-masking.ts +++ b/dev-test/gql-tag-operations-masking/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql.js'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/dev-test/gql-tag-operations-masking/gql/graphql.ts b/dev-test/gql-tag-operations-masking/gql/graphql.ts index cf80a16e14c..20b8154dd3d 100644 --- a/dev-test/gql-tag-operations-masking/gql/graphql.ts +++ b/dev-test/gql-tag-operations-masking/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/gql-tag-operations-urql/gql/fragment-masking.ts b/dev-test/gql-tag-operations-urql/gql/fragment-masking.ts index dc2836d43e3..0cedfd53e94 100644 --- a/dev-test/gql-tag-operations-urql/gql/fragment-masking.ts +++ b/dev-test/gql-tag-operations-urql/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql.js'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/dev-test/gql-tag-operations-urql/gql/graphql.ts b/dev-test/gql-tag-operations-urql/gql/graphql.ts index 67bfe67faf7..6992bb76696 100644 --- a/dev-test/gql-tag-operations-urql/gql/graphql.ts +++ b/dev-test/gql-tag-operations-urql/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/gql-tag-operations/gql/fragment-masking.ts b/dev-test/gql-tag-operations/gql/fragment-masking.ts index dc2836d43e3..0cedfd53e94 100644 --- a/dev-test/gql-tag-operations/gql/fragment-masking.ts +++ b/dev-test/gql-tag-operations/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql.js'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/dev-test/gql-tag-operations/gql/graphql.ts b/dev-test/gql-tag-operations/gql/graphql.ts index 67bfe67faf7..6992bb76696 100644 --- a/dev-test/gql-tag-operations/gql/graphql.ts +++ b/dev-test/gql-tag-operations/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/gql-tag-operations/graphql/fragment-masking.ts b/dev-test/gql-tag-operations/graphql/fragment-masking.ts index dc2836d43e3..0cedfd53e94 100644 --- a/dev-test/gql-tag-operations/graphql/fragment-masking.ts +++ b/dev-test/gql-tag-operations/graphql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql.js'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/dev-test/gql-tag-operations/graphql/graphql.ts b/dev-test/gql-tag-operations/graphql/graphql.ts index 67bfe67faf7..6992bb76696 100644 --- a/dev-test/gql-tag-operations/graphql/graphql.ts +++ b/dev-test/gql-tag-operations/graphql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/modules/types.ts b/dev-test/modules/types.ts index 05c9718ccae..a6833e1cfbe 100644 --- a/dev-test/modules/types.ts +++ b/dev-test/modules/types.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; export type Omit = Pick>; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ diff --git a/dev-test/star-wars/types.avoidOptionals.ts b/dev-test/star-wars/types.avoidOptionals.ts index 79cc40d04b2..354473aa3b5 100644 --- a/dev-test/star-wars/types.avoidOptionals.ts +++ b/dev-test/star-wars/types.avoidOptionals.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.d.ts b/dev-test/star-wars/types.d.ts index 7f7577a548a..8242f8b5c3b 100644 --- a/dev-test/star-wars/types.d.ts +++ b/dev-test/star-wars/types.d.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.globallyAvailable.d.ts b/dev-test/star-wars/types.globallyAvailable.d.ts index 097c066666a..a3feed5792c 100644 --- a/dev-test/star-wars/types.globallyAvailable.d.ts +++ b/dev-test/star-wars/types.globallyAvailable.d.ts @@ -3,6 +3,8 @@ type InputMaybe = Maybe; type Exact = { [K in keyof T]: T[K] }; type MakeOptional = Omit & { [SubKey in K]?: Maybe }; type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +type MakeEmpty = { [_ in K]?: never }; +type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.immutableTypes.ts b/dev-test/star-wars/types.immutableTypes.ts index 308c916179d..aebbd6f24c4 100644 --- a/dev-test/star-wars/types.immutableTypes.ts +++ b/dev-test/star-wars/types.immutableTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts index 58104b680ba..4fddfe57b71 100644 --- a/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.preResolveTypes.ts b/dev-test/star-wars/types.preResolveTypes.ts index 16eaefea148..894ebd04e37 100644 --- a/dev-test/star-wars/types.preResolveTypes.ts +++ b/dev-test/star-wars/types.preResolveTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.skipSchema.ts b/dev-test/star-wars/types.skipSchema.ts index 16eaefea148..894ebd04e37 100644 --- a/dev-test/star-wars/types.skipSchema.ts +++ b/dev-test/star-wars/types.skipSchema.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/star-wars/types.ts b/dev-test/star-wars/types.ts index 16eaefea148..894ebd04e37 100644 --- a/dev-test/star-wars/types.ts +++ b/dev-test/star-wars/types.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/env.types.ts b/dev-test/test-schema/env.types.ts index 8f8f1b5b588..d10823f189d 100644 --- a/dev-test/test-schema/env.types.ts +++ b/dev-test/test-schema/env.types.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index 5a33b696555..256d3146f9d 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/resolvers-root.ts b/dev-test/test-schema/resolvers-root.ts index 384d4c43bff..6d0f385db1d 100644 --- a/dev-test/test-schema/resolvers-root.ts +++ b/dev-test/test-schema/resolvers-root.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { diff --git a/dev-test/test-schema/resolvers-stitching.ts b/dev-test/test-schema/resolvers-stitching.ts index 0221fe70e4f..fd588e89449 100644 --- a/dev-test/test-schema/resolvers-stitching.ts +++ b/dev-test/test-schema/resolvers-stitching.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { diff --git a/dev-test/test-schema/resolvers-types.ts b/dev-test/test-schema/resolvers-types.ts index 751529eae6a..4e1754d6976 100644 --- a/dev-test/test-schema/resolvers-types.ts +++ b/dev-test/test-schema/resolvers-types.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { diff --git a/dev-test/test-schema/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/test-schema/types.preResolveTypes.onlyOperationTypes.ts index ff64cdd6408..e8f4b62fbb0 100644 --- a/dev-test/test-schema/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/test-schema/types.preResolveTypes.onlyOperationTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/types.preResolveTypes.ts b/dev-test/test-schema/types.preResolveTypes.ts index c2611ac2c97..78e0a4e4705 100644 --- a/dev-test/test-schema/types.preResolveTypes.ts +++ b/dev-test/test-schema/types.preResolveTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/typings.avoidOptionals.ts b/dev-test/test-schema/typings.avoidOptionals.ts index c4da945f8d3..39eb13a5346 100644 --- a/dev-test/test-schema/typings.avoidOptionals.ts +++ b/dev-test/test-schema/typings.avoidOptionals.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/typings.enum.ts b/dev-test/test-schema/typings.enum.ts index 4d27a6aa56b..c4f33e44bcb 100644 --- a/dev-test/test-schema/typings.enum.ts +++ b/dev-test/test-schema/typings.enum.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/typings.immutableTypes.ts b/dev-test/test-schema/typings.immutableTypes.ts index 01f29495fee..68de80f79df 100644 --- a/dev-test/test-schema/typings.immutableTypes.ts +++ b/dev-test/test-schema/typings.immutableTypes.ts @@ -3,6 +3,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/dev-test/test-schema/typings.ts b/dev-test/test-schema/typings.ts index 9f1bf80ffa5..6f5eed9dc47 100644 --- a/dev-test/test-schema/typings.ts +++ b/dev-test/test-schema/typings.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { diff --git a/dev-test/test-schema/typings.wrapped.ts b/dev-test/test-schema/typings.wrapped.ts index 4229727530a..67ef1c7b161 100644 --- a/dev-test/test-schema/typings.wrapped.ts +++ b/dev-test/test-schema/typings.wrapped.ts @@ -4,6 +4,8 @@ declare namespace GraphQL { export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts b/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts index dc2836d43e3..f86d241c2c2 100644 --- a/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts +++ b/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts @@ -1,8 +1,9 @@ import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { Incremental, TypedDocumentString } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +47,17 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: TypedDocumentString, + fragmentNode: TypedDocumentString, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = queryNode.__meta__?.deferredFields as Record; + + if (!deferredFields) return true; + + const fragName = fragmentNode.__meta__?.fragmentName; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/persisted-documents-string-mode/src/gql/graphql.ts b/examples/persisted-documents-string-mode/src/gql/graphql.ts index eec941afa44..89fb697c109 100644 --- a/examples/persisted-documents-string-mode/src/gql/graphql.ts +++ b/examples/persisted-documents-string-mode/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -38,7 +40,7 @@ export class TypedDocumentString { __apiType?: DocumentTypeDecoration['__apiType']; - constructor(private value: string, public __meta__?: { hash: string }) { + constructor(private value: string, public __meta__?: Record) { super(value); } diff --git a/examples/persisted-documents/src/gql/fragment-masking.ts b/examples/persisted-documents/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/persisted-documents/src/gql/fragment-masking.ts +++ b/examples/persisted-documents/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/persisted-documents/src/gql/graphql.ts b/examples/persisted-documents/src/gql/graphql.ts index a85407d9609..0f99afca549 100644 --- a/examples/persisted-documents/src/gql/graphql.ts +++ b/examples/persisted-documents/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/react/apollo-client-defer/.gitignore b/examples/react/apollo-client-defer/.gitignore new file mode 100644 index 00000000000..4d29575de80 --- /dev/null +++ b/examples/react/apollo-client-defer/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/apollo-client-defer/README.md b/examples/react/apollo-client-defer/README.md new file mode 100644 index 00000000000..e808d30aa8a --- /dev/null +++ b/examples/react/apollo-client-defer/README.md @@ -0,0 +1,17 @@ +# Using GraphQL Code Generator with Apollo Client and React + +This example illustrates how to use GraphQL Code Generator in a React application using the Apollo React GraphQL Client. + +You will find the TypeScript-based codegen configuration in [`codegen.ts`](./codegen.ts). + +This simple codegen configuration generates types and helpers in the [`src/gql`](./src/gql/) folder that help you to get typed GraphQL Queries and Mutations seamlessly ⚡️ + +
+ +For a step-by-step implementation tutorial, please refer to the related guide: + +https://www.the-guild.dev/graphql/codegen/docs/guides/react-vue-angular + +-- + +Please note that the `client` preset used in this example is compatible with `@apollo/client` (since `3.2.0`, not when using React Components (``)). diff --git a/examples/react/apollo-client-defer/codegen.ts b/examples/react/apollo-client-defer/codegen.ts new file mode 100644 index 00000000000..117250afa81 --- /dev/null +++ b/examples/react/apollo-client-defer/codegen.ts @@ -0,0 +1,15 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { type CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + schema: './src/yoga.mjs', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + }, + }, + hooks: { afterAllFileWrite: ['prettier --write'] }, +}; + +export default config; diff --git a/examples/react/apollo-client-defer/cypress.config.ts b/examples/react/apollo-client-defer/cypress.config.ts new file mode 100644 index 00000000000..b1c137b9e05 --- /dev/null +++ b/examples/react/apollo-client-defer/cypress.config.ts @@ -0,0 +1,10 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + setupNodeEvents(_on, _config) { + // implement node event listeners here + }, + }, +}); diff --git a/examples/react/apollo-client-defer/cypress/e2e/end2end.cy.ts b/examples/react/apollo-client-defer/cypress/e2e/end2end.cy.ts new file mode 100644 index 00000000000..312e7b4546b --- /dev/null +++ b/examples/react/apollo-client-defer/cypress/e2e/end2end.cy.ts @@ -0,0 +1,11 @@ +describe('template spec', () => { + it('renders everything correctly', () => { + cy.visit('http://localhost:3000'); + cy.get('.App').should('contain', 'I am speed'); + cy.get('.App').should('not.contain', 'I am slow'); + + cy.wait(5000); + cy.get('.App').should('contain', 'I am slow'); + cy.get('.App').should('contain', 'I am slow inlined fragment'); + }); +}); diff --git a/examples/react/apollo-client-defer/cypress/support/commands.ts b/examples/react/apollo-client-defer/cypress/support/commands.ts new file mode 100644 index 00000000000..6d4cbd5a68a --- /dev/null +++ b/examples/react/apollo-client-defer/cypress/support/commands.ts @@ -0,0 +1 @@ +/// diff --git a/examples/react/apollo-client-defer/cypress/support/e2e.ts b/examples/react/apollo-client-defer/cypress/support/e2e.ts new file mode 100644 index 00000000000..663a7b21f51 --- /dev/null +++ b/examples/react/apollo-client-defer/cypress/support/e2e.ts @@ -0,0 +1,2 @@ +// Import commands.js using ES2015 syntax: +import './commands'; diff --git a/examples/react/apollo-client-defer/index.html b/examples/react/apollo-client-defer/index.html new file mode 100644 index 00000000000..ab66da90506 --- /dev/null +++ b/examples/react/apollo-client-defer/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/examples/react/apollo-client-defer/package.json b/examples/react/apollo-client-defer/package.json new file mode 100644 index 00000000000..3bf156bd1eb --- /dev/null +++ b/examples/react/apollo-client-defer/package.json @@ -0,0 +1,36 @@ +{ + "name": "example-react-apollo-client-defer", + "version": "0.1.0", + "private": true, + "dependencies": { + "@apollo/client": "^3.7.10", + "@graphql-yoga/plugin-defer-stream": "^1.7.3", + "graphql": "^16.6.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "graphql-yoga": "3.7.3" + }, + "devDependencies": { + "@graphql-codegen/cli": "^3.3.0", + "@graphql-codegen/client-preset": "^3.0.0", + "@types/jest": "^27.5.2", + "@types/node": "^18.11.18", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "cypress": "12.9.0", + "serve": "14.2.0", + "start-server-and-test": "2.0.0", + "typescript": "4.9.5", + "vite": "^4.1.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "start:yoga": "node src/yoga.mjs", + "start": "yarn start:yoga & serve -s dist", + "test": "cypress run", + "test:end2end": "start-server-and-test start http://localhost:3000 test", + "codegen": "graphql-codegen --config codegen.ts" + } +} diff --git a/examples/react/apollo-client-defer/src/App.tsx b/examples/react/apollo-client-defer/src/App.tsx new file mode 100644 index 00000000000..e836b55fa9a --- /dev/null +++ b/examples/react/apollo-client-defer/src/App.tsx @@ -0,0 +1,55 @@ +import { useQuery } from '@apollo/client'; + +import { useFragment, graphql, FragmentType, isFragmentReady, DocumentType } from './gql'; + +export const slowFieldFragment = graphql(/* GraphQL */ ` + fragment SlowFieldFragment on Query { + slowField(waitFor: 5000) + } +`); + +const alphabetQuery = graphql(/* GraphQL */ ` + query SlowAndFastFieldWithDefer { + fastField + ...SlowFieldFragment @defer + + ... @defer { + inlinedSlowField: slowField(waitFor: 5000) + } + } +`); + +const SlowDataField = (props: { data: FragmentType }) => { + const data = useFragment(slowFieldFragment, props.data); + return

{data.slowField}

; +}; + +const InlinedSlowDataField = (props: { data: DocumentType }) => { + try { + // @ts-expect-error - this field should be either undefined or a string + const _ = props.data.inlinedSlowField.toLowerCase(); + } catch (e) {} + + if (!props.data.inlinedSlowField) { + return null; + } + return

{props.data.inlinedSlowField} inlined fragment

; +}; + +function App() { + const { data } = useQuery(alphabetQuery); + + return ( +
+ {data && ( + <> +

{data.fastField}

+ {isFragmentReady(alphabetQuery, slowFieldFragment, data) && } + + + )} +
+ ); +} + +export default App; diff --git a/examples/react/apollo-client-defer/src/gql/fragment-masking.ts b/examples/react/apollo-client-defer/src/gql/fragment-masking.ts new file mode 100644 index 00000000000..f895cdecd67 --- /dev/null +++ b/examples/react/apollo-client-defer/src/gql/fragment-masking.ts @@ -0,0 +1,66 @@ +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; + +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never + : never + : never; + +// return non-nullable if `fragmentType` is non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> +): TType; +// return nullable if `fragmentType` is nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined +): TType | null | undefined; +// return array of non-nullable if `fragmentType` is array of non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> +): ReadonlyArray; +// return array of nullable if `fragmentType` is array of nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined; +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: + | FragmentType> + | ReadonlyArray>> + | null + | undefined +): TType | ReadonlyArray | null | undefined { + return fragmentType as any; +} + +export function makeFragmentData, FT extends ResultOf>( + data: FT, + _fragment: F +): FragmentType { + return data as FragmentType; +} +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/apollo-client-defer/src/gql/gql.ts b/examples/react/apollo-client-defer/src/gql/gql.ts new file mode 100644 index 00000000000..f7b35ee845d --- /dev/null +++ b/examples/react/apollo-client-defer/src/gql/gql.ts @@ -0,0 +1,57 @@ +/* eslint-disable */ +import * as types from './graphql'; +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + */ +const documents = { + '\n fragment SlowFieldFragment on Query {\n slowField(waitFor: 5000)\n }\n': types.SlowFieldFragmentFragmentDoc, + '\n query SlowAndFastFieldWithDefer {\n fastField\n ...SlowFieldFragment @defer\n\n ... @defer {\n inlinedSlowField: slowField(waitFor: 5000)\n }\n }\n': + types.SlowAndFastFieldWithDeferDocument, +}; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + * + * + * @example + * ```ts + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * ``` + * + * The query argument is unknown! + * Please regenerate the types. + */ +export function graphql(source: string): unknown; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: '\n fragment SlowFieldFragment on Query {\n slowField(waitFor: 5000)\n }\n' +): (typeof documents)['\n fragment SlowFieldFragment on Query {\n slowField(waitFor: 5000)\n }\n']; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: '\n query SlowAndFastFieldWithDefer {\n fastField\n ...SlowFieldFragment @defer\n\n ... @defer {\n inlinedSlowField: slowField(waitFor: 5000)\n }\n }\n' +): (typeof documents)['\n query SlowAndFastFieldWithDefer {\n fastField\n ...SlowFieldFragment @defer\n\n ... @defer {\n inlinedSlowField: slowField(waitFor: 5000)\n }\n }\n']; + +export function graphql(source: string) { + return (documents as any)[source] ?? {}; +} + +export type DocumentType> = TDocumentNode extends DocumentNode< + infer TType, + any +> + ? TType + : never; diff --git a/examples/react/apollo-client-defer/src/gql/graphql.ts b/examples/react/apollo-client-defer/src/gql/graphql.ts new file mode 100644 index 00000000000..fd16b6a7dd4 --- /dev/null +++ b/examples/react/apollo-client-defer/src/gql/graphql.ts @@ -0,0 +1,138 @@ +/* eslint-disable */ +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; +}; + +export type Query = { + __typename?: 'Query'; + alphabet: Array; + /** A field that resolves fast. */ + fastField: Scalars['String']; + /** + * A field that resolves slowly. + * Maybe you want to @defer this field ;) + */ + slowField: Scalars['String']; +}; + +export type QuerySlowFieldArgs = { + waitFor?: Scalars['Int']; +}; + +export type SlowFieldFragmentFragment = { __typename?: 'Query'; slowField: string } & { + ' $fragmentName'?: 'SlowFieldFragmentFragment'; +}; + +export type SlowAndFastFieldWithDeferQueryVariables = Exact<{ [key: string]: never }>; + +export type SlowAndFastFieldWithDeferQuery = { __typename?: 'Query'; fastField: string } & ( + | { __typename?: 'Query'; inlinedSlowField: string } + | { __typename?: 'Query'; inlinedSlowField?: never } +) & + ({ __typename?: 'Query' } & { + ' $fragmentRefs'?: { SlowFieldFragmentFragment: Incremental }; + }); + +export const SlowFieldFragmentFragmentDoc = { + kind: 'Document', + definitions: [ + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'SlowFieldFragment' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Query' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'slowField' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'waitFor' }, + value: { kind: 'IntValue', value: '5000' }, + }, + ], + }, + ], + }, + }, + ], +} as unknown as DocumentNode; +export const SlowAndFastFieldWithDeferDocument = { + __meta__: { deferredFields: { SlowFieldFragment: ['slowField'] } }, + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'SlowAndFastFieldWithDefer' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'fastField' } }, + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'SlowFieldFragment' }, + directives: [{ kind: 'Directive', name: { kind: 'Name', value: 'defer' } }], + }, + { + kind: 'InlineFragment', + directives: [{ kind: 'Directive', name: { kind: 'Name', value: 'defer' } }], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + alias: { kind: 'Name', value: 'inlinedSlowField' }, + name: { kind: 'Name', value: 'slowField' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'waitFor' }, + value: { kind: 'IntValue', value: '5000' }, + }, + ], + }, + ], + }, + }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'SlowFieldFragment' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Query' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'slowField' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'waitFor' }, + value: { kind: 'IntValue', value: '5000' }, + }, + ], + }, + ], + }, + }, + ], +} as unknown as DocumentNode; diff --git a/examples/react/apollo-client-defer/src/gql/index.ts b/examples/react/apollo-client-defer/src/gql/index.ts new file mode 100644 index 00000000000..c682b1e2f99 --- /dev/null +++ b/examples/react/apollo-client-defer/src/gql/index.ts @@ -0,0 +1,2 @@ +export * from './fragment-masking'; +export * from './gql'; diff --git a/examples/react/apollo-client-defer/src/main.tsx b/examples/react/apollo-client-defer/src/main.tsx new file mode 100644 index 00000000000..21031c42f1c --- /dev/null +++ b/examples/react/apollo-client-defer/src/main.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; + +const client = new ApolloClient({ + uri: 'http://localhost:4000/graphql', + cache: new InMemoryCache(), +}); + +const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement); +root.render( + + + + + +); diff --git a/examples/react/apollo-client-defer/src/yoga.mjs b/examples/react/apollo-client-defer/src/yoga.mjs new file mode 100644 index 00000000000..ce0a710ca02 --- /dev/null +++ b/examples/react/apollo-client-defer/src/yoga.mjs @@ -0,0 +1,55 @@ +import { createSchema, createYoga } from 'graphql-yoga'; +import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'; +import { createServer } from 'node:http'; + +const typeDefs = /* GraphQL */ ` + type Query { + alphabet: [String!]! + """ + A field that resolves fast. + """ + fastField: String! + + """ + A field that resolves slowly. + Maybe you want to @defer this field ;) + """ + slowField(waitFor: Int! = 5000): String! + } +`; + +const wait = time => new Promise(resolve => setTimeout(resolve, time)); + +const resolvers = { + Query: { + async *alphabet() { + for (const character of ['a', 'b', 'c', 'd', 'e', 'f', 'g']) { + yield character; + await wait(1000); + } + }, + fastField: async () => { + await wait(100); + return 'I am speed'; + }, + slowField: async (_, { waitFor }) => { + await wait(waitFor); + return 'I am slow'; + }, + }, +}; + +export const yoga = createYoga({ + schema: createSchema({ + typeDefs, + resolvers, + }), + plugins: [useDeferStream()], +}); + +const server = createServer(yoga); + +server.listen(4000, () => { + // eslint-disable-next-line no-console + console.info('Server is running on http://localhost:4000/graphql'); +}); diff --git a/examples/react/apollo-client-defer/tsconfig.json b/examples/react/apollo-client-defer/tsconfig.json new file mode 100644 index 00000000000..b557c4047ca --- /dev/null +++ b/examples/react/apollo-client-defer/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/react/apollo-client-defer/tsconfig.node.json b/examples/react/apollo-client-defer/tsconfig.node.json new file mode 100644 index 00000000000..9d31e2aed93 --- /dev/null +++ b/examples/react/apollo-client-defer/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/react/apollo-client-defer/vite.config.ts b/examples/react/apollo-client-defer/vite.config.ts new file mode 100644 index 00000000000..779543405fa --- /dev/null +++ b/examples/react/apollo-client-defer/vite.config.ts @@ -0,0 +1,8 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/examples/react/apollo-client-swc-plugin/package.json b/examples/react/apollo-client-swc-plugin/package.json index e42c79d5924..8f33b209355 100644 --- a/examples/react/apollo-client-swc-plugin/package.json +++ b/examples/react/apollo-client-swc-plugin/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@apollo/client": "^3.6.9", + "@apollo/client": "^3.7.10", "react": "18.2.0", "react-dom": "18.2.0" }, diff --git a/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts b/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts +++ b/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/apollo-client-swc-plugin/src/gql/graphql.ts b/examples/react/apollo-client-swc-plugin/src/gql/graphql.ts index 9ca792fa77d..100bcd7ccd7 100644 --- a/examples/react/apollo-client-swc-plugin/src/gql/graphql.ts +++ b/examples/react/apollo-client-swc-plugin/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/react/apollo-client/package.json b/examples/react/apollo-client/package.json index caf63f93ca2..85f2f7dfc28 100644 --- a/examples/react/apollo-client/package.json +++ b/examples/react/apollo-client/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@apollo/client": "^3.6.9", + "@apollo/client": "^3.7.10", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/apollo-client/src/gql/fragment-masking.ts b/examples/react/apollo-client/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/react/apollo-client/src/gql/fragment-masking.ts +++ b/examples/react/apollo-client/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/apollo-client/src/gql/graphql.ts b/examples/react/apollo-client/src/gql/graphql.ts index 9ca792fa77d..100bcd7ccd7 100644 --- a/examples/react/apollo-client/src/gql/graphql.ts +++ b/examples/react/apollo-client/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/react/http-executor/src/gql/fragment-masking.ts b/examples/react/http-executor/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/react/http-executor/src/gql/fragment-masking.ts +++ b/examples/react/http-executor/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/http-executor/src/gql/graphql.ts b/examples/react/http-executor/src/gql/graphql.ts index 9ca792fa77d..100bcd7ccd7 100644 --- a/examples/react/http-executor/src/gql/graphql.ts +++ b/examples/react/http-executor/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/react/nextjs-swr/gql/fragment-masking.ts b/examples/react/nextjs-swr/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/react/nextjs-swr/gql/fragment-masking.ts +++ b/examples/react/nextjs-swr/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/nextjs-swr/gql/graphql.ts b/examples/react/nextjs-swr/gql/graphql.ts index 1b439fa77dd..5b8106ff6f1 100644 --- a/examples/react/nextjs-swr/gql/graphql.ts +++ b/examples/react/nextjs-swr/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/react/tanstack-react-query/src/gql/fragment-masking.ts b/examples/react/tanstack-react-query/src/gql/fragment-masking.ts index dc2836d43e3..f86d241c2c2 100644 --- a/examples/react/tanstack-react-query/src/gql/fragment-masking.ts +++ b/examples/react/tanstack-react-query/src/gql/fragment-masking.ts @@ -1,8 +1,9 @@ import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { Incremental, TypedDocumentString } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +47,17 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: TypedDocumentString, + fragmentNode: TypedDocumentString, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = queryNode.__meta__?.deferredFields as Record; + + if (!deferredFields) return true; + + const fragName = fragmentNode.__meta__?.fragmentName; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/tanstack-react-query/src/gql/graphql.ts b/examples/react/tanstack-react-query/src/gql/graphql.ts index 239024a493c..b5e81ccaa97 100644 --- a/examples/react/tanstack-react-query/src/gql/graphql.ts +++ b/examples/react/tanstack-react-query/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1302,7 +1304,7 @@ export class TypedDocumentString { __apiType?: DocumentTypeDecoration['__apiType']; - constructor(private value: string, public __meta__?: { hash: string }) { + constructor(private value: string, public __meta__?: Record) { super(value); } @@ -1310,14 +1312,17 @@ export class TypedDocumentString return this.value; } } -export const FilmItemFragmentDoc = new TypedDocumentString(` +export const FilmItemFragmentDoc = new TypedDocumentString( + ` fragment FilmItem on Film { id title releaseDate producers } - `) as unknown as TypedDocumentString; + `, + { fragmentName: 'FilmItem' } +) as unknown as TypedDocumentString; export const AllFilmsWithVariablesQueryDocument = new TypedDocumentString(` query allFilmsWithVariablesQuery($first: Int!) { allFilms(first: $first) { diff --git a/examples/react/urql/src/gql/fragment-masking.ts b/examples/react/urql/src/gql/fragment-masking.ts index dc2836d43e3..f86d241c2c2 100644 --- a/examples/react/urql/src/gql/fragment-masking.ts +++ b/examples/react/urql/src/gql/fragment-masking.ts @@ -1,8 +1,9 @@ import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { Incremental, TypedDocumentString } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +47,17 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: TypedDocumentString, + fragmentNode: TypedDocumentString, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = queryNode.__meta__?.deferredFields as Record; + + if (!deferredFields) return true; + + const fragName = fragmentNode.__meta__?.fragmentName; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/react/urql/src/gql/graphql.ts b/examples/react/urql/src/gql/graphql.ts index b62e6096cf6..ee56c52ce1b 100644 --- a/examples/react/urql/src/gql/graphql.ts +++ b/examples/react/urql/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1302,7 +1304,7 @@ export class TypedDocumentString { __apiType?: DocumentTypeDecoration['__apiType']; - constructor(private value: string, public __meta__?: { hash: string }) { + constructor(private value: string, public __meta__?: Record) { super(value); } @@ -1310,14 +1312,17 @@ export class TypedDocumentString return this.value; } } -export const FilmItemFragmentDoc = new TypedDocumentString(` +export const FilmItemFragmentDoc = new TypedDocumentString( + ` fragment FilmItem on Film { id title releaseDate producers } - `) as unknown as TypedDocumentString; + `, + { fragmentName: 'FilmItem' } +) as unknown as TypedDocumentString; export const AllFilmsWithVariablesQuery199Document = new TypedDocumentString(` query allFilmsWithVariablesQuery199($first: Int!) { allFilms(first: $first) { diff --git a/examples/typescript-esm/src/gql/fragment-masking.ts b/examples/typescript-esm/src/gql/fragment-masking.ts index dc2836d43e3..0cedfd53e94 100644 --- a/examples/typescript-esm/src/gql/fragment-masking.ts +++ b/examples/typescript-esm/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql.js'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/typescript-esm/src/gql/graphql.ts b/examples/typescript-esm/src/gql/graphql.ts index a689c8a2080..24283c0e2d9 100644 --- a/examples/typescript-esm/src/gql/graphql.ts +++ b/examples/typescript-esm/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/typescript-graphql-request/src/gql/fragment-masking.ts b/examples/typescript-graphql-request/src/gql/fragment-masking.ts index dc2836d43e3..f86d241c2c2 100644 --- a/examples/typescript-graphql-request/src/gql/fragment-masking.ts +++ b/examples/typescript-graphql-request/src/gql/fragment-masking.ts @@ -1,8 +1,9 @@ import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { Incremental, TypedDocumentString } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +47,17 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: TypedDocumentString, + fragmentNode: TypedDocumentString, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = queryNode.__meta__?.deferredFields as Record; + + if (!deferredFields) return true; + + const fragName = fragmentNode.__meta__?.fragmentName; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/typescript-graphql-request/src/gql/graphql.ts b/examples/typescript-graphql-request/src/gql/graphql.ts index 1b9d64da87e..a849c32238d 100644 --- a/examples/typescript-graphql-request/src/gql/graphql.ts +++ b/examples/typescript-graphql-request/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1315,7 +1317,7 @@ export class TypedDocumentString { __apiType?: DocumentTypeDecoration['__apiType']; - constructor(private value: string, public __meta__?: { hash: string }) { + constructor(private value: string, public __meta__?: Record) { super(value); } diff --git a/examples/typescript-resolvers/src/type-defs.d.ts b/examples/typescript-resolvers/src/type-defs.d.ts index 160b23311fb..3efc5d2730b 100644 --- a/examples/typescript-resolvers/src/type-defs.d.ts +++ b/examples/typescript-resolvers/src/type-defs.d.ts @@ -4,6 +4,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { diff --git a/examples/vite/vite-react-cts/package.json b/examples/vite/vite-react-cts/package.json index 0aaf7e0d194..630a8e1dc37 100644 --- a/examples/vite/vite-react-cts/package.json +++ b/examples/vite/vite-react-cts/package.json @@ -12,7 +12,7 @@ "test:end2end": "start-server-and-test start http://localhost:3000 test" }, "dependencies": { - "@apollo/client": "^3.6.9", + "@apollo/client": "^3.7.10", "@graphql-typed-document-node/core": "3.2.0", "@vitejs/plugin-react-swc": "^3.0.0", "graphql": "16.6.0", diff --git a/examples/vite/vite-react-cts/src/gql/fragment-masking.ts b/examples/vite/vite-react-cts/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/vite/vite-react-cts/src/gql/fragment-masking.ts +++ b/examples/vite/vite-react-cts/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/vite/vite-react-cts/src/gql/graphql.ts b/examples/vite/vite-react-cts/src/gql/graphql.ts index 1b439fa77dd..5b8106ff6f1 100644 --- a/examples/vite/vite-react-cts/src/gql/graphql.ts +++ b/examples/vite/vite-react-cts/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/vite/vite-react-mts/package.json b/examples/vite/vite-react-mts/package.json index cc5fc486f4e..252f2f52cd9 100644 --- a/examples/vite/vite-react-mts/package.json +++ b/examples/vite/vite-react-mts/package.json @@ -12,7 +12,7 @@ "test:end2end": "start-server-and-test start http://localhost:3000 test" }, "dependencies": { - "@apollo/client": "^3.6.9", + "@apollo/client": "^3.7.10", "@graphql-typed-document-node/core": "3.2.0", "@vitejs/plugin-react-swc": "^3.0.0", "graphql": "16.6.0", diff --git a/examples/vite/vite-react-mts/src/gql/fragment-masking.ts b/examples/vite/vite-react-mts/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/vite/vite-react-mts/src/gql/fragment-masking.ts +++ b/examples/vite/vite-react-mts/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/vite/vite-react-mts/src/gql/graphql.ts b/examples/vite/vite-react-mts/src/gql/graphql.ts index 1b439fa77dd..5b8106ff6f1 100644 --- a/examples/vite/vite-react-mts/src/gql/graphql.ts +++ b/examples/vite/vite-react-mts/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/vite/vite-react-ts/package.json b/examples/vite/vite-react-ts/package.json index 94ae01462c0..b447c74b9c6 100644 --- a/examples/vite/vite-react-ts/package.json +++ b/examples/vite/vite-react-ts/package.json @@ -12,7 +12,7 @@ "test:end2end": "start-server-and-test start http://localhost:3000 test" }, "dependencies": { - "@apollo/client": "^3.6.9", + "@apollo/client": "^3.7.10", "@graphql-typed-document-node/core": "3.2.0", "@vitejs/plugin-react-swc": "^3.0.0", "graphql": "16.6.0", diff --git a/examples/vite/vite-react-ts/src/gql/fragment-masking.ts b/examples/vite/vite-react-ts/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/vite/vite-react-ts/src/gql/fragment-masking.ts +++ b/examples/vite/vite-react-ts/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/vite/vite-react-ts/src/gql/graphql.ts b/examples/vite/vite-react-ts/src/gql/graphql.ts index 1b439fa77dd..5b8106ff6f1 100644 --- a/examples/vite/vite-react-ts/src/gql/graphql.ts +++ b/examples/vite/vite-react-ts/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/vue/apollo-composable/package.json b/examples/vue/apollo-composable/package.json index 53fb99cb194..5a75d673ffd 100644 --- a/examples/vue/apollo-composable/package.json +++ b/examples/vue/apollo-composable/package.json @@ -11,7 +11,7 @@ "test:end2end": "start-server-and-test start http://localhost:3000 test" }, "dependencies": { - "@apollo/client": "^3.7.7", + "@apollo/client": "^3.7.10", "@vue/apollo-composable": "4.0.0-beta.4", "graphql": "^16.6.0", "vue": "^3.2.37" diff --git a/examples/vue/apollo-composable/src/gql/fragment-masking.ts b/examples/vue/apollo-composable/src/gql/fragment-masking.ts index bb601eea66b..7fdcdff8304 100644 --- a/examples/vue/apollo-composable/src/gql/fragment-masking.ts +++ b/examples/vue/apollo-composable/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/vue/apollo-composable/src/gql/graphql.ts b/examples/vue/apollo-composable/src/gql/graphql.ts index 2f497291a3c..a70aaf50ae7 100644 --- a/examples/vue/apollo-composable/src/gql/graphql.ts +++ b/examples/vue/apollo-composable/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/vue/urql/src/gql/fragment-masking.ts b/examples/vue/urql/src/gql/fragment-masking.ts index bb601eea66b..7fdcdff8304 100644 --- a/examples/vue/urql/src/gql/fragment-masking.ts +++ b/examples/vue/urql/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/vue/urql/src/gql/graphql.ts b/examples/vue/urql/src/gql/graphql.ts index 2f497291a3c..a70aaf50ae7 100644 --- a/examples/vue/urql/src/gql/graphql.ts +++ b/examples/vue/urql/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/vue/villus/src/gql/fragment-masking.ts b/examples/vue/villus/src/gql/fragment-masking.ts index bb601eea66b..7fdcdff8304 100644 --- a/examples/vue/villus/src/gql/fragment-masking.ts +++ b/examples/vue/villus/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/vue/villus/src/gql/graphql.ts b/examples/vue/villus/src/gql/graphql.ts index 2f497291a3c..a70aaf50ae7 100644 --- a/examples/vue/villus/src/gql/graphql.ts +++ b/examples/vue/villus/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/examples/yoga-tests/src/gql/fragment-masking.ts b/examples/yoga-tests/src/gql/fragment-masking.ts index dc2836d43e3..f895cdecd67 100644 --- a/examples/yoga-tests/src/gql/fragment-masking.ts +++ b/examples/yoga-tests/src/gql/fragment-masking.ts @@ -1,8 +1,10 @@ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -46,3 +48,19 @@ export function makeFragmentData, FT ): FragmentType { return data as FragmentType; } +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/examples/yoga-tests/src/gql/graphql.ts b/examples/yoga-tests/src/gql/graphql.ts index 51c26c9b9bf..cea15891bec 100644 --- a/examples/yoga-tests/src/gql/graphql.ts +++ b/examples/yoga-tests/src/gql/graphql.ts @@ -5,6 +5,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts index 2e15884b292..423cc70a127 100644 --- a/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts @@ -5,12 +5,15 @@ import autoBind from 'auto-bind'; import { pascalCase } from 'change-case-all'; import { DepGraph } from 'dependency-graph'; import { + DefinitionNode, + DirectiveNode, DocumentNode, FragmentDefinitionNode, FragmentSpreadNode, GraphQLSchema, Kind, OperationDefinitionNode, + SelectionNode, print, } from 'graphql'; import gqlTag from 'graphql-tag'; @@ -407,19 +410,15 @@ export class ClientSideBaseVisitor< } let metaString = ''; - if (this._onExecutableDocumentNode && node.kind === Kind.OPERATION_DEFINITION) { - const meta = this._onExecutableDocumentNode({ - kind: Kind.DOCUMENT, - definitions, - }); + const meta = this._getGraphQLCodegenMetadata(node, definitions); if (meta) { - metaString = `"__meta__":${JSON.stringify(meta)},`; - if (this._omitDefinitions === true) { - return `{${metaString.slice(0, -1)}}`; + return `{${`"__meta__":${JSON.stringify(meta)},`.slice(0, -1)}}`; } + + metaString = `"__meta__":${JSON.stringify(meta)},`; } } @@ -427,21 +426,20 @@ export class ClientSideBaseVisitor< } if (this.config.documentMode === DocumentMode.string) { - let meta: ExecutableDocumentNodeMeta | void; + if (node.kind === Kind.FRAGMENT_DEFINITION) { + return `new TypedDocumentString(\`${doc}\`, ${JSON.stringify({ fragmentName: node.name.value })})`; + } if (this._onExecutableDocumentNode && node.kind === Kind.OPERATION_DEFINITION) { - meta = this._onExecutableDocumentNode({ - kind: Kind.DOCUMENT, - definitions: gqlTag([doc]).definitions, - }); + const meta = this._getGraphQLCodegenMetadata(node, gqlTag([doc]).definitions); - if (meta && this._omitDefinitions === true) { - return `{${`"__meta__":${JSON.stringify(meta)},`.slice(0, -1)}}`; + if (meta) { + if (this._omitDefinitions === true) { + return `{${`"__meta__":${JSON.stringify(meta)},`.slice(0, -1)}}`; + } + return `new TypedDocumentString(\`${doc}\`, ${JSON.stringify(meta)})`; } } - if (meta) { - return `new TypedDocumentString(\`${doc}\`, ${JSON.stringify(meta)})`; - } return `new TypedDocumentString(\`${doc}\`)`; } @@ -451,6 +449,56 @@ export class ClientSideBaseVisitor< return (gqlImport.propName || 'gql') + '`' + doc + '`'; } + protected _getGraphQLCodegenMetadata( + node: OperationDefinitionNode, + definitions?: ReadonlyArray + ): Record | void | undefined { + let meta: Record | void | undefined; + + meta = this._onExecutableDocumentNode({ + kind: Kind.DOCUMENT, + definitions, + }); + + const deferredFields = this._findDeferredFields(node); + if (Object.keys(deferredFields).length) { + meta = { + ...meta, + deferredFields, + }; + } + + return meta; + } + + protected _findDeferredFields(node: OperationDefinitionNode): { [fargmentName: string]: string[] } { + const deferredFields: { [fargmentName: string]: string[] } = {}; + const queue: SelectionNode[] = [...node.selectionSet.selections]; + while (queue.length) { + const selection = queue.shift(); + if ( + selection.kind === Kind.FRAGMENT_SPREAD && + selection.directives.some((d: DirectiveNode) => d.name.value === 'defer') + ) { + const fragmentName = selection.name.value; + const fragment = this.fragmentsGraph.getNodeData(fragmentName); + if (fragment) { + const fields = fragment.node.selectionSet.selections.reduce((acc, selection) => { + if (selection.kind === Kind.FIELD) { + acc.push(selection.name.value); + } + return acc; + }, []); + + deferredFields[fragmentName] = fields; + } + } else if (selection.kind === Kind.FIELD && selection.selectionSet) { + queue.push(...selection.selectionSet.selections); + } + } + return deferredFields; + } + protected _generateFragment(fragmentDocument: FragmentDefinitionNode): string | void { const name = this.getFragmentVariableName(fragmentDocument); const fragmentTypeSuffix = this.getFragmentSuffix(fragmentDocument); diff --git a/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts b/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts index 9ac00ff1e0f..7441b5cdbfc 100644 --- a/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts +++ b/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts @@ -12,7 +12,12 @@ export type SelectionSetProcessorConfig = { convertName: ConvertNameFn; enumPrefix: boolean | null; scalars: ScalarsMap; - formatNamedField(name: string, type?: GraphQLOutputType | GraphQLNamedType | null, isConditional?: boolean): string; + formatNamedField( + name: string, + type?: GraphQLOutputType | GraphQLNamedType | null, + isConditional?: boolean, + isOptional?: boolean + ): string; wrapTypeWithModifiers(baseType: string, type: GraphQLOutputType | GraphQLNamedType): string; avoidOptionals?: AvoidOptionalsConfig | boolean; }; @@ -36,7 +41,8 @@ export class BaseSelectionSetProcessor ({ name: field.alias || field.name, - type: field.selectionSet, + type: unsetTypes ? 'never' : field.selectionSet, })); } } diff --git a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts index 431eb16c5c9..3ec5bd632ec 100644 --- a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts +++ b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts @@ -34,12 +34,19 @@ import { PrimitiveField, ProcessResult, } from './selection-set-processor/base.js'; -import { ConvertNameFn, GetFragmentSuffixFn, LoadedFragment, NormalizedScalarsMap } from './types.js'; +import { + ConvertNameFn, + FragmentDirectives, + GetFragmentSuffixFn, + LoadedFragment, + NormalizedScalarsMap, +} from './types.js'; import { DeclarationBlock, getFieldNodeNameValue, getPossibleTypes, hasConditionalDirectives, + hasIncrementalDeliveryDirectives, mergeSelectionSets, separateSelectionSet, } from './utils.js'; @@ -49,8 +56,12 @@ type FragmentSpreadUsage = { typeName: string; onType: string; selectionNodes: Array; + fragmentDirectives?: DirectiveNode[]; }; +type CollectedFragmentNode = (SelectionNode | FragmentSpreadUsage | DirectiveNode) & FragmentDirectives; +type GroupedStringifiedTypes = Record>; + function isMetadataFieldName(name: string) { return ['__schema', '__type'].includes(name); } @@ -99,8 +110,8 @@ export class SelectionSetToObject> + nodes: Array, + types: Map> ) { if (isListType(parentType) || isNonNullType(parentType)) { return this._collectInlineFragments(parentType.ofType as GraphQLNamedType, nodes, types); @@ -112,8 +123,18 @@ export class SelectionSetToObject ({ + ...field, + fragmentDirectives: field.fragmentDirectives || directives, + })); + if (isObjectType(typeOnSchema)) { - this._appendToTypeMap(types, typeOnSchema.name, fields); + this._appendToTypeMap(types, typeOnSchema.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, typeOnSchema.name, spreadsUsage[typeOnSchema.name]); this._appendToTypeMap(types, typeOnSchema.name, directives); this._collectInlineFragments(typeOnSchema, inlines, types); @@ -237,6 +258,7 @@ export class SelectionSetToObject(types: Map>, typeName: string, nodes: Array): void { + private _appendToTypeMap( + types: Map>, + typeName: string, + nodes: Array + ): void { if (!types.has(typeName)) { types.set(typeName, []); } @@ -301,7 +326,7 @@ export class SelectionSetToObject; mustAddEmptyObject: boolean } { + protected _buildGroupedSelections(): { grouped: GroupedStringifiedTypes; mustAddEmptyObject: boolean } { if (!this._selectionSet?.selections || this._selectionSet.selections.length === 0) { return { grouped: {}, mustAddEmptyObject: true }; } @@ -314,7 +339,7 @@ export class SelectionSetToObject { + const grouped = possibleTypes.reduce((prev, type) => { const typeName = type.name; const schemaType = this._schema.getType(typeName); @@ -322,21 +347,56 @@ export class SelectionSetToObject( + (acc, node) => { + if ('fragmentDirectives' in node && hasIncrementalDeliveryDirectives(node.fragmentDirectives)) { + acc.incrementalNodes.push(node); + } else { + acc.selectionNodes.push(node); + } + return acc; + }, + { selectionNodes: [], incrementalNodes: [] } + ); + const { fields } = this.buildSelectionSet(schemaType, selectionNodes); - const transformedSet = this.selectionSetStringFromFields(fields); + const transformedSet = this.selectionSetStringFromFields(fields); if (transformedSet) { prev[typeName].push(transformedSet); } else { mustAddEmptyObject = true; } + for (const incrementalNode of incrementalNodes) { + if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) { + const { fields: incrementalFields } = this.buildSelectionSet(schemaType, [incrementalNode], { + unsetTypes: true, + }); + prev[typeName].push(this.selectionSetStringFromFields(incrementalFields)); + + continue; + } + const { fields: initialFields } = this.buildSelectionSet(schemaType, [incrementalNode]); + const { fields: subsequentFields } = this.buildSelectionSet(schemaType, [incrementalNode], { + unsetTypes: true, + }); + + const initialSet = this.selectionSetStringFromFields(initialFields); + const subsequentSet = this.selectionSetStringFromFields(subsequentFields); + + prev[typeName].push({ union: [initialSet, subsequentSet] }); + } + return prev; - }, {} as Record); + }, {}); return { grouped, mustAddEmptyObject }; } @@ -423,7 +483,8 @@ export class SelectionSetToObject + selectionNodes: Array, + options?: { unsetTypes: boolean } ) { const primitiveFields = new Map(); const primitiveAliasFields = new Map(); @@ -442,7 +503,6 @@ export class SelectionSetToObject ({ isConditional: hasConditionalDirectives(field), fieldName: field.name.value, - })) + })), + options?.unsetTypes ), ...this._processor.transformAliasesPrimitiveFields( parentSchemaType, Array.from(primitiveAliasFields.values()).map(field => ({ alias: field.alias.value, fieldName: field.name.value, - })) + })), + options?.unsetTypes ), - ...this._processor.transformLinkFields(linkFields), + ...this._processor.transformLinkFields(linkFields, options?.unsetTypes), ].filter(Boolean); const allStrings: string[] = transformed.filter(t => typeof t === 'string') as string[]; + const allObjectsMerged: string[] = transformed .filter(t => typeof t !== 'string') .map((t: NameAndType) => `${t.name}: ${t.type}`); + let mergedObjectsAsString: string = null; if (allObjectsMerged.length > 0) { @@ -587,7 +652,11 @@ export class SelectionSetToObject `'${name}': ${name}`).join(`;`)} } }`); + fields.push( + `{ ' $fragmentRefs'?: { ${fragmentsSpreadUsages + .map(name => `'${name}': ${options?.unsetTypes ? `Incremental<${name}>` : name}`) + .join(`;`)} } }` + ); } } @@ -608,7 +677,6 @@ export class SelectionSetToObject { + const hasUnions = grouped[typeName].filter(s => typeof s !== 'string' && s.union).length > 0; const relevant = grouped[typeName].filter(Boolean); if (relevant.length === 0) { return null; } - if (relevant.length === 1) { - return relevant[0]; - } - return `( ${relevant.join(' & ')} )`; + + const res = relevant + .map(selectionObject => { + if (typeof selectionObject === 'string') return selectionObject; + + return '(' + selectionObject.union.join(' | ') + ')'; + }) + .join(' & '); + + return relevant.length > 1 && !hasUnions ? `( ${res} )` : res; }) .filter(Boolean) .join(' | ') + this.getEmptyObjectTypeString(mustAddEmptyObject) @@ -685,7 +760,6 @@ export class SelectionSetToObject; +}; + export interface ResolversNonOptionalTypenameConfig { unionMember?: boolean; interfaceImplementingType?: boolean; diff --git a/packages/plugins/other/visitor-plugin-common/src/utils.ts b/packages/plugins/other/visitor-plugin-common/src/utils.ts index 7372881b619..498eb4c7d9e 100644 --- a/packages/plugins/other/visitor-plugin-common/src/utils.ts +++ b/packages/plugins/other/visitor-plugin-common/src/utils.ts @@ -21,11 +21,12 @@ import { SelectionSetNode, StringValueNode, TypeNode, + DirectiveNode, } from 'graphql'; import { RawConfig } from './base-visitor.js'; import { parseMapper } from './mappers.js'; import { DEFAULT_SCALARS } from './scalars.js'; -import { NormalizedScalarsMap, ParsedScalarsMap, ScalarsMap } from './types.js'; +import { NormalizedScalarsMap, ParsedScalarsMap, ScalarsMap, FragmentDirectives } from './types.js'; export const getConfigValue = (value: T, defaultValue: T): T => { if (value === null || value === undefined) { @@ -408,7 +409,7 @@ export const getFieldNodeNameValue = (node: FieldNode): string => { }; export function separateSelectionSet(selections: ReadonlyArray): { - fields: FieldNode[]; + fields: (FieldNode & FragmentDirectives)[]; spreads: FragmentSpreadNode[]; inlines: InlineFragmentNode[]; } { @@ -438,6 +439,11 @@ export function hasConditionalDirectives(field: FieldNode): boolean { return field.directives?.some(directive => CONDITIONAL_DIRECTIVES.includes(directive.name.value)); } +export function hasIncrementalDeliveryDirectives(directives: DirectiveNode[]): boolean { + const INCREMENTAL_DELIVERY_DIRECTIVES = ['defer']; + return directives?.some(directive => INCREMENTAL_DELIVERY_DIRECTIVES.includes(directive.name.value)); +} + type WrapModifiersOptions = { wrapOptional(type: string): string; wrapArray(type: string): string; diff --git a/packages/plugins/typescript/operations/src/ts-selection-set-processor.ts b/packages/plugins/typescript/operations/src/ts-selection-set-processor.ts index 254c336162f..6de86a0c45d 100644 --- a/packages/plugins/typescript/operations/src/ts-selection-set-processor.ts +++ b/packages/plugins/typescript/operations/src/ts-selection-set-processor.ts @@ -11,7 +11,8 @@ import { GraphQLInterfaceType, GraphQLObjectType } from 'graphql'; export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor { transformPrimitiveFields( schemaType: GraphQLObjectType | GraphQLInterfaceType, - fields: PrimitiveField[] + fields: PrimitiveField[], + unsetTypes?: boolean ): ProcessResult { if (fields.length === 0) { return []; @@ -23,6 +24,10 @@ export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor `'${field.fieldName}'`).join(' | ')}>`]; + } + let hasConditionals = false; const conditilnalsList: string[] = []; let resString = `Pick<${parentName}, ${fields diff --git a/packages/plugins/typescript/operations/src/visitor.ts b/packages/plugins/typescript/operations/src/visitor.ts index c07b2d5736c..f71a5144f20 100644 --- a/packages/plugins/typescript/operations/src/visitor.ts +++ b/packages/plugins/typescript/operations/src/visitor.ts @@ -66,9 +66,11 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< const formatNamedField = ( name: string, type: GraphQLOutputType | GraphQLNamedType | null, - isConditional = false + isConditional = false, + isOptional = false ): string => { - const optional = isConditional || (!this.config.avoidOptionals.field && !!type && !isNonNullType(type)); + const optional = + isOptional || isConditional || (!this.config.avoidOptionals.field && !!type && !isNonNullType(type)); return (this.config.immutableTypes ? `readonly ${name}` : name) + (optional ? '?' : ''); }; diff --git a/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap b/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap index 6ea53d261ec..024e0bd3138 100644 --- a/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap +++ b/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap @@ -69,6 +69,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -358,6 +360,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -474,6 +478,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -567,6 +573,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/packages/plugins/typescript/operations/tests/ts-documents.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.spec.ts index de9b5c5dfe8..7447163c474 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.spec.ts @@ -4049,6 +4049,8 @@ describe('TypeScript Operations Plugin', () => { export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -4157,6 +4159,8 @@ describe('TypeScript Operations Plugin', () => { export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -4243,6 +4247,8 @@ describe('TypeScript Operations Plugin', () => { export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -6434,6 +6440,717 @@ function test(q: GetEntityBrandDataQuery): void { }); }); + describe('incremental delivery directive handling', () => { + it('should generate an union of initial and deferred fields for fragments (preResolveTypes: true)', async () => { + const schema = buildSchema(` + type Address { + street1: String! + } + + type Phone { + home: String! + } + + type Employment { + title: String! + } + + type User { + name: String! + email: String! + address: Address! + phone: Phone! + employment: Employment! + widgetCount: Int! + widgetPreference: String! + clearanceLevel: String! + favoriteFood: String! + leastFavoriteFood: String! + } + + type Query { + user: User! + } + `); + + const fragment = parse(` + fragment WidgetFragment on User { + widgetCount + widgetPreference + } + + fragment FoodFragment on User { + favoriteFood + leastFavoriteFood + } + + fragment EmploymentFragment on User { + employment { + title + } + } + + query user { + user { + # Test inline fragment defer + ... @defer { + email + } + + # Test inline fragment defer with nested selection set + ... @defer { + address { + street1 + } + } + + # Test named fragment defer + ...WidgetFragment @defer + + # Test a secondary named fragment defer + ...FoodFragment @defer + + # Not deferred fields, fragments, selection sets, etc are left alone + name + phone { + home + } + ...EmploymentFragment + ... { + clearanceLevel + } + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document: fragment }], + { preResolveTypes: true }, + { outputFile: 'graphql.ts' } + ); + + expect(content).toBeSimilarStringTo(` + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + export type UserQuery = { + __typename?: 'Query', + user: { + __typename?: 'User', + clearanceLevel: string, + name: string, + phone: { + __typename?: 'Phone', + home: string + }, + employment: { + __typename?: 'Employment', + title: string + } + } & ({ __typename?: 'User', email: string } + | { __typename?: 'User', email?: never }) + & ({ __typename?: 'User', address: { __typename?: 'Address', street1: string } } + | { __typename?: 'User', address?: never }) + & ({ __typename?: 'User', widgetCount: number, widgetPreference: string } + | { __typename?: 'User', widgetCount?: never, widgetPreference?: never }) + & ({ __typename?: 'User', favoriteFood: string, leastFavoriteFood: string } + | { __typename?: 'User', favoriteFood?: never, leastFavoriteFood?: never }) }; + `); + }); + + it('should generate an union of initial and deferred fields for fragments using MakeEmpty (preResolveTypes: false)', async () => { + const schema = buildSchema(` + type Address { + street1: String! + } + + type Phone { + home: String! + } + + type Employment { + title: String! + } + + type User { + name: String! + email: String! + address: Address! + phone: Phone! + employment: Employment! + widgetCount: Int! + clearanceLevel: String! + } + + type Query { + user: User! + } + `); + + const fragment = parse(` + fragment WidgetFragment on User { + widgetCount + } + + fragment EmploymentFragment on User { + employment { + title + } + } + + query user { + user { + # Test inline fragment defer + ... @defer { + email + } + + # Test inline fragment defer with nested selection set + ... @defer { + address { + street1 + } + } + + # Test named fragment defer + ...WidgetFragment @defer + + # Not deferred fields, fragments, selection sets, etc are left alone + name + phone { + home + } + ...EmploymentFragment + ... { + clearanceLevel + } + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document: fragment }], + { preResolveTypes: false }, + { outputFile: 'graphql.ts' } + ); + + expect(content).toBeSimilarStringTo(` + export type WidgetFragmentFragment = ( + { __typename?: 'User' } + & Pick + ); + + export type EmploymentFragmentFragment = ( + { __typename?: 'User' } + & { employment: ( + { __typename?: 'Employment' } + & Pick + ) } + ); + + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + + export type UserQuery = ( + { __typename?: 'Query' } + & { user: ( + { __typename?: 'User' } + & Pick + & { phone: ( + { __typename?: 'Phone' } + & Pick + ), employment: ( + { __typename?: 'Employment' } + & Pick + ) } + ) & (( + { __typename?: 'User' } + & Pick + ) | ( + { __typename?: 'User' } + & MakeEmpty + )) & (( + { __typename?: 'User' } + & { address: ( + { __typename?: 'Address' } + & Pick + ) } + ) | ( + { __typename?: 'User' } + & { address?: ( + { __typename?: 'Address' } + & Pick + ) } + )) & (( + { __typename?: 'User' } + & Pick + ) | ( + { __typename?: 'User' } + & MakeEmpty + )) } + ); + `); + }); + + it('should generate an union of initial and deferred fields for fragments MakeEmpty (avoidOptionals: true)', async () => { + const schema = buildSchema(` + type Address { + street1: String! + } + + type Phone { + home: String! + } + + type Employment { + title: String! + } + + type User { + name: String! + email: String! + address: Address! + phone: Phone! + employment: Employment! + widgetName: String! + widgetCount: Int! + clearanceLevel: String! + } + + type Query { + user: User! + } + `); + + const fragment = parse(` + fragment WidgetFragment on User { + widgetName + widgetCount + } + + fragment EmploymentFragment on User { + employment { + title + } + } + + query user { + user { + # Test inline fragment defer + ... @defer { + email + } + + # Test inline fragment defer with nested selection set + ... @defer { + address { + street1 + } + } + + # Test named fragment defer + ...WidgetFragment @defer + + # Not deferred fields, fragments, selection sets, etc are left alone + name + phone { + home + } + ...EmploymentFragment + ... { + clearanceLevel + } + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document: fragment }], + { + avoidOptionals: true, + preResolveTypes: false, + }, + { outputFile: 'graphql.ts' } + ); + + expect(content).toBeSimilarStringTo(` + export type WidgetFragmentFragment = ( + { __typename?: 'User' } + & Pick + ); + + export type EmploymentFragmentFragment = ( + { __typename?: 'User' } + & { employment: ( + { __typename?: 'Employment' } + & Pick + ) } + ); + + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + + export type UserQuery = ( + { __typename?: 'Query' } + & { user: ( + { __typename?: 'User' } + & Pick + & { phone: ( + { __typename?: 'Phone' } + & Pick + ), employment: ( + { __typename?: 'Employment' } + & Pick + ) } + ) & (( + { __typename?: 'User' } + & Pick + ) | ( + { __typename?: 'User' } + & MakeEmpty + )) & (( + { __typename?: 'User' } + & { address: ( + { __typename?: 'Address' } + & Pick + ) } + ) | ( + { __typename?: 'User' } + & { address?: ( + { __typename?: 'Address' } + & Pick + ) } + )) & (( + { __typename?: 'User' } + & Pick + ) | ( + { __typename?: 'User' } + & MakeEmpty + )) } + ); + `); + }); + + it('should support "preResolveTypes: true" and "avoidOptionals: true" together', async () => { + const schema = buildSchema(` + type Address { + street1: String! + } + + type Phone { + home: String! + } + + type Employment { + title: String! + } + + type User { + name: String! + email: String! + address: Address! + phone: Phone! + employment: Employment! + widgetCount: Int! + clearanceLevel: String! + } + + type Query { + user: User! + } + `); + + const fragment = parse(` + fragment WidgetFragment on User { + widgetCount + } + + fragment EmploymentFragment on User { + employment { + title + } + } + + query user { + user { + # Test inline fragment defer + ... @defer { + email + } + + # Test inline fragment defer with nested selection set + ... @defer { + address { + street1 + } + } + + # Test named fragment defer + ...WidgetFragment @defer + + # Not deferred fields, fragments, selection sets, etc are left alone + name + phone { + home + } + ...EmploymentFragment + ... { + clearanceLevel + } + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document: fragment }], + { + avoidOptionals: true, + preResolveTypes: true, + }, + { outputFile: 'graphql.ts' } + ); + + expect(content).toBeSimilarStringTo(` + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + export type UserQuery = { + __typename?: 'Query', + user: { + __typename?: 'User', + clearanceLevel: string, + name: string, + phone: { __typename?: 'Phone', home: string }, + employment: { __typename?: 'Employment', title: string } + } & ({ __typename?: 'User', email: string } + | { __typename?: 'User', email?: never }) + & ({ __typename?: 'User', address: { __typename?: 'Address', street1: string } } + | { __typename?: 'User', address?: never }) + & ({ __typename?: 'User', widgetCount: number } + | { __typename?: 'User', widgetCount?: never }) + }; + `); + }); + + it('should resolve optionals according to maybeValue together with avoidOptionals and deferred fragments', async () => { + const schema = buildSchema(` + type Address { + street1: String + } + + type Phone { + home: String! + } + + type Employment { + title: String! + } + + type User { + name: String! + email: String! + address: Address! + phone: Phone! + employment: Employment! + widgetName: String! + widgetCount: Int! + clearanceLevel: String! + } + + type Query { + user: User! + } + `); + + const fragment = parse(` + fragment WidgetFragment on User { + widgetName + widgetCount + } + + fragment EmploymentFragment on User { + employment { + title + } + } + + query user { + user { + # Test inline fragment defer + ... @defer { + email + } + + # Test inline fragment defer with nested selection set + ... @defer { + address { + street1 + } + } + + # Test named fragment defer + ...WidgetFragment @defer + + # Not deferred fields, fragments, selection sets, etc are left alone + name + phone { + home + } + ...EmploymentFragment + ... { + clearanceLevel + } + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document: fragment }], + { + preResolveTypes: true, + maybeValue: "T | 'specialType'", + avoidOptionals: true, + }, + { outputFile: 'graphql.ts' } + ); + + expect(content).toBeSimilarStringTo(` + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + export type UserQuery = { + __typename?: 'Query', + user: { + __typename?: 'User', + clearanceLevel: string, + name: string, + phone: { __typename?: 'Phone', home: string }, + employment: { __typename?: 'Employment', title: string } + } & ({ __typename?: 'User', email: string } + | { __typename?: 'User', email?: never }) + & ({ __typename?: 'User', address: { __typename?: 'Address', street1: string | 'specialType' } } + | { __typename?: 'User', address?: never }) + & ({ __typename?: 'User', widgetName: string, widgetCount: number } + | { __typename?: 'User', widgetName?: never, widgetCount?: never }) + }; + `); + }); + + it('should generate correct types with inlineFragmentTypes: "mask""', async () => { + const schema = buildSchema(` + type Address { + street1: String! + } + + type Phone { + home: String! + } + + type Employment { + title: String! + } + + type User { + name: String! + email: String! + address: Address! + phone: Phone! + employment: Employment! + widgetCount: Int! + widgetPreference: String! + clearanceLevel: String! + favoriteFood: String! + leastFavoriteFood: String! + } + + type Query { + user: User! + } + `); + + const fragment = parse(` + fragment WidgetFragment on User { + widgetCount + widgetPreference + } + + fragment FoodFragment on User { + favoriteFood + leastFavoriteFood + } + + fragment EmploymentFragment on User { + employment { + title + } + } + + query user { + user { + # Test inline fragment defer + ... @defer { + email + } + + # Test inline fragment defer with nested selection set + ... @defer { + address { + street1 + } + } + + # Test named fragment defer + ...WidgetFragment @defer + + # Test a secondary named fragment defer + ...FoodFragment @defer + + # Not deferred fields, fragments, selection sets, etc are left alone + name + phone { + home + } + ...EmploymentFragment + ... { + clearanceLevel + } + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document: fragment }], + { preResolveTypes: true, inlineFragmentTypes: 'mask' }, + { outputFile: 'graphql.ts' } + ); + + expect(content).toBeSimilarStringTo(` + export type WidgetFragmentFragment = { __typename?: 'User', widgetCount: number, widgetPreference: string } & { ' $fragmentName'?: 'WidgetFragmentFragment' }; + + export type FoodFragmentFragment = { __typename?: 'User', favoriteFood: string, leastFavoriteFood: string } & { ' $fragmentName'?: 'FoodFragmentFragment' }; + + export type EmploymentFragmentFragment = { __typename?: 'User', employment: { __typename?: 'Employment', title: string } } & { ' $fragmentName'?: 'EmploymentFragmentFragment' }; + + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + + export type UserQuery = { + __typename?: 'Query', + user: ( + { + __typename?: 'User', + clearanceLevel: string, + name: string, + phone: { __typename?: 'Phone', home: string } + } & { ' $fragmentRefs'?: { 'EmploymentFragmentFragment': EmploymentFragmentFragment } } + ) & ({ __typename?: 'User', email: string } | { __typename?: 'User', email?: never }) & ({ __typename?: 'User', address: { __typename?: 'Address', street1: string } } | { __typename?: 'User', address?: never }) & ( + { __typename?: 'User' } + & { ' $fragmentRefs'?: { 'WidgetFragmentFragment': Incremental } } + ) & ( + { __typename?: 'User' } + & { ' $fragmentRefs'?: { 'FoodFragmentFragment': Incremental } } + ) }; + `); + }); + }); + it('handles unnamed queries', async () => { const ast = parse(/* GraphQL */ ` query { diff --git a/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap b/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap index f437df770ac..9b501d329ea 100644 --- a/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap +++ b/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap @@ -6,6 +6,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; export type Omit = Pick>; export type RequireFields = Omit & { [P in K]-?: NonNullable }; @@ -608,6 +610,8 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; export type Omit = Pick>; export type RequireFields = Omit & { [P in K]-?: NonNullable }; diff --git a/packages/plugins/typescript/typed-document-node/src/index.ts b/packages/plugins/typescript/typed-document-node/src/index.ts index 4602a2e2e45..b17f7a63e47 100644 --- a/packages/plugins/typescript/typed-document-node/src/index.ts +++ b/packages/plugins/typescript/typed-document-node/src/index.ts @@ -43,7 +43,7 @@ export class TypedDocumentString { __apiType?: DocumentTypeDecoration['__apiType']; - constructor(private value: string, public __meta__?: { hash: string }) { + constructor(private value: string, public __meta__?: Record) { super(value); } diff --git a/packages/plugins/typescript/typescript/src/visitor.ts b/packages/plugins/typescript/typescript/src/visitor.ts index 33a97285b2f..cb8d08b81df 100644 --- a/packages/plugins/typescript/typescript/src/visitor.ts +++ b/packages/plugins/typescript/typescript/src/visitor.ts @@ -49,6 +49,8 @@ export interface TypeScriptPluginParsedConfig extends ParsedTypesConfig { export const EXACT_SIGNATURE = `type Exact = { [K in keyof T]: T[K] };`; export const MAKE_OPTIONAL_SIGNATURE = `type MakeOptional = Omit & { [SubKey in K]?: Maybe };`; export const MAKE_MAYBE_SIGNATURE = `type MakeMaybe = Omit & { [SubKey in K]: Maybe };`; +export const MAKE_EMPTY_SIGNATURE = `type MakeEmpty = { [_ in K]?: never };`; +export const MAKE_INCREMENTAL_SIGNATURE = `type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };`; export class TsVisitor< TRawConfig extends TypeScriptPluginConfig = TypeScriptPluginConfig, @@ -159,6 +161,8 @@ export class TsVisitor< this.getExactDefinition(), this.getMakeOptionalDefinition(), this.getMakeMaybeDefinition(), + this.getMakeEmptyDefinition(), + this.getIncrementalDefinition(), ]; if (this.config.wrapFieldDefinitions) { @@ -187,6 +191,14 @@ export class TsVisitor< return `${this.getExportPrefix()}${MAKE_MAYBE_SIGNATURE}`; } + public getMakeEmptyDefinition(): string { + return `${this.getExportPrefix()}${MAKE_EMPTY_SIGNATURE}`; + } + + public getIncrementalDefinition(): string { + return `${this.getExportPrefix()}${MAKE_INCREMENTAL_SIGNATURE}`; + } + public getMaybeValue(): string { return `${this.getExportPrefix()}type Maybe = ${this.config.maybeValue};`; } diff --git a/packages/presets/client/src/fragment-masking-plugin.ts b/packages/presets/client/src/fragment-masking-plugin.ts index df1224c21ce..66cbdd1fe89 100644 --- a/packages/presets/client/src/fragment-masking-plugin.ts +++ b/packages/presets/client/src/fragment-masking-plugin.ts @@ -5,7 +5,7 @@ export type FragmentType> infer TType, any > - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -22,22 +22,28 @@ export function makeFragmentData< const defaultUnmaskFunctionName = 'useFragment'; -const modifyType = (rawType: string, opts: { nullable: boolean; list: 'with-list' | 'only-list' | false }) => - `${ +const modifyType = ( + rawType: string, + opts: { nullable: boolean; list: 'with-list' | 'only-list' | false; empty?: boolean } +) => { + return `${ opts.list === 'only-list' ? `ReadonlyArray<${rawType}>` : opts.list === 'with-list' ? `${rawType} | ReadonlyArray<${rawType}>` : rawType }${opts.nullable ? ' | null | undefined' : ''}`; +}; const createUnmaskFunctionTypeDefinition = ( unmaskFunctionName = defaultUnmaskFunctionName, opts: { nullable: boolean; list: 'with-list' | 'only-list' | false } -) => `export function ${unmaskFunctionName}( +) => { + return `export function ${unmaskFunctionName}( _documentNode: DocumentTypeDecoration, - fragmentType: ${modifyType('FragmentType>', opts)} + fragmentType: ${modifyType(`FragmentType>`, opts)} ): ${modifyType('TType', opts)}`; +}; const createUnmaskFunctionTypeDefinitions = (unmaskFunctionName = defaultUnmaskFunctionName) => [ `// return non-nullable if \`fragmentType\` is non-nullable\n${createUnmaskFunctionTypeDefinition( @@ -66,6 +72,45 @@ ${createUnmaskFunctionTypeDefinitions(unmaskFunctionName) } `; +const isFragmentReadyFunction = (isStringDocumentMode: boolean) => { + if (isStringDocumentMode) { + return `\ +export function isFragmentReady( + queryNode: TypedDocumentString, + fragmentNode: TypedDocumentString, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = queryNode.__meta__?.deferredFields as Record; + + if (!deferredFields) return true; + + const fragName = fragmentNode.__meta__?.fragmentName; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} +`; + } + return `\ +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); +} +`; +}; + /** * Plugin for generating fragment masking helper functions. */ @@ -73,20 +118,39 @@ export const plugin: PluginFunction<{ useTypeImports?: boolean; augmentedModuleName?: string; unmaskFunctionName?: string; -}> = (_, __, { useTypeImports, augmentedModuleName, unmaskFunctionName }, _info) => { - const documentNodeImport = `${ - useTypeImports ? 'import type' : 'import' - } { ResultOf, DocumentTypeDecoration, } from '@graphql-typed-document-node/core';\n`; + emitLegacyCommonJSImports?: boolean; + isStringDocumentMode?: boolean; +}> = ( + _, + __, + { useTypeImports, augmentedModuleName, unmaskFunctionName, emitLegacyCommonJSImports, isStringDocumentMode }, + _info +) => { + const documentNodeImport = `${useTypeImports ? 'import type' : 'import'} { ResultOf, DocumentTypeDecoration${ + isStringDocumentMode ? '' : ', TypedDocumentNode' + } } from '@graphql-typed-document-node/core';\n`; + + const deferFragmentHelperImports = `${useTypeImports ? 'import type' : 'import'} { Incremental${ + isStringDocumentMode ? ', TypedDocumentString' : '' + } } from './graphql${emitLegacyCommonJSImports ? '' : '.js'}';\n`; + + const fragmentDefinitionNodeImport = isStringDocumentMode + ? '' + : `${useTypeImports ? 'import type' : 'import'} { FragmentDefinitionNode } from 'graphql';\n`; if (augmentedModuleName == null) { return [ documentNodeImport, + fragmentDefinitionNodeImport, + deferFragmentHelperImports, `\n`, fragmentTypeHelper, `\n`, createUnmaskFunction(unmaskFunctionName), `\n`, makeFragmentDataHelper, + `\n`, + isFragmentReadyFunction(isStringDocumentMode), ].join(``); } diff --git a/packages/presets/client/src/index.ts b/packages/presets/client/src/index.ts index 92d4beddbab..68b47d657b0 100644 --- a/packages/presets/client/src/index.ts +++ b/packages/presets/client/src/index.ts @@ -4,7 +4,7 @@ import type { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; import * as typedDocumentNodePlugin from '@graphql-codegen/typed-document-node'; import * as typescriptPlugin from '@graphql-codegen/typescript'; import * as typescriptOperationPlugin from '@graphql-codegen/typescript-operations'; -import { ClientSideBaseVisitor } from '@graphql-codegen/visitor-plugin-common'; +import { ClientSideBaseVisitor, DocumentMode } from '@graphql-codegen/visitor-plugin-common'; import { DocumentNode } from 'graphql'; import * as fragmentMaskingPlugin from './fragment-masking-plugin.js'; import { generateDocumentHash, normalizeAndPrintDocumentNode } from './persisted-documents.js'; @@ -239,6 +239,8 @@ export const preset: Types.OutputPreset = { config: { useTypeImports: options.config.useTypeImports, unmaskFunctionName: fragmentMaskingConfig.unmaskFunctionName, + emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports, + isStringDocumentMode: options.config.documentMode === DocumentMode.string, }, documents: [], documentTransforms: options.documentTransforms, diff --git a/packages/presets/client/tests/client-preset.spec.ts b/packages/presets/client/tests/client-preset.spec.ts index 6673406cc4a..4e012a0f23f 100644 --- a/packages/presets/client/tests/client-preset.spec.ts +++ b/packages/presets/client/tests/client-preset.spec.ts @@ -343,6 +343,8 @@ export * from "./gql";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -471,6 +473,8 @@ export * from "./gql";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -582,6 +586,8 @@ export * from "./gql";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -748,14 +754,16 @@ export * from "./gql";`); expect(result).toHaveLength(4); const gqlFile = result.find(file => file.filename === 'out1/fragment-masking.ts'); expect(gqlFile.content).toMatchInlineSnapshot(` - "import { ResultOf, DocumentTypeDecoration, } from '@graphql-typed-document-node/core'; + "import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; + import { FragmentDefinitionNode } from 'graphql'; + import { Incremental } from './graphql'; export type FragmentType> = TDocumentType extends DocumentTypeDecoration< infer TType, any > - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -795,7 +803,24 @@ export * from "./gql";`); FT extends ResultOf >(data: FT, _fragment: F): FragmentType { return data as FragmentType; - }" + } + export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined + ): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = fragName ? deferredFields[fragName] : []; + return fields.length > 0 && fields.every(field => data && field in data); + } + " `); expect(gqlFile.content).toBeSimilarStringTo(` @@ -1227,6 +1252,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1303,6 +1330,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1382,6 +1411,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1461,6 +1492,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1542,6 +1575,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1630,6 +1665,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1674,6 +1711,396 @@ export * from "./gql.js";`); `); }); + describe('handles @defer directive', () => { + it('generates correct types and metadata', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo] + } + + type Foo { + value: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/with-deferred-fragment.ts'), + generates: { + 'out1/': { + preset, + }, + }, + }); + + const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); + expect(graphqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { [K in keyof T]: T[K] }; + export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; + export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + }; + + export type Foo = { + __typename?: 'Foo'; + value?: Maybe; + }; + + export type Query = { + __typename?: 'Query'; + foo?: Maybe; + foos?: Maybe>>; + }; + + export type FooQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FooQuery = { __typename?: 'Query', foo?: ( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null }; + + export type FoosQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FoosQuery = { __typename?: 'Query', foos?: Array<( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null> | null }; + + export type FooFragment = { __typename?: 'Foo', value?: string | null } & { ' $fragmentName'?: 'FooFragment' }; + + export const FooFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Foo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]} as unknown as DocumentNode; + export const FooDocument = {"__meta__":{"deferredFields":{"Foo":["value"]}},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Foo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"foo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Foo"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"defer"}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Foo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]} as unknown as DocumentNode; + export const FoosDocument = {"__meta__":{"deferredFields":{"Foo":["value"]}},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Foos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"foos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Foo"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"defer"}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Foo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]} as unknown as DocumentNode;" + `); + }); + + it('works with persisted documents', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo] + } + + type Foo { + value: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/with-deferred-fragment.ts'), + generates: { + 'out1/': { + preset, + presetConfig: { + persistedDocuments: true, + }, + }, + }, + }); + + const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); + expect(graphqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { [K in keyof T]: T[K] }; + export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; + export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + }; + + export type Foo = { + __typename?: 'Foo'; + value?: Maybe; + }; + + export type Query = { + __typename?: 'Query'; + foo?: Maybe; + foos?: Maybe>>; + }; + + export type FooQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FooQuery = { __typename?: 'Query', foo?: ( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null }; + + export type FoosQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FoosQuery = { __typename?: 'Query', foos?: Array<( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null> | null }; + + export type FooFragment = { __typename?: 'Foo', value?: string | null } & { ' $fragmentName'?: 'FooFragment' }; + + export const FooFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Foo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]} as unknown as DocumentNode; + export const FooDocument = {"__meta__":{"hash":"39c47d2da0fb0e6867abbe2ec942d9858f2d76c7","deferredFields":{"Foo":["value"]}},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Foo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"foo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Foo"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"defer"}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Foo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]} as unknown as DocumentNode; + export const FoosDocument = {"__meta__":{"hash":"8aba765173b2302b9857334e9959d97a2168dbc8","deferredFields":{"Foo":["value"]}},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Foos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"foos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Foo"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"defer"}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Foo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]} as unknown as DocumentNode;" + `); + }); + + it('works with documentMode: string', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo] + } + + type Foo { + value: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/with-deferred-fragment.ts'), + generates: { + 'out1/': { + preset, + config: { + documentMode: 'string', + }, + }, + }, + }); + + const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); + expect(graphqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { [K in keyof T]: T[K] }; + export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; + export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + }; + + export type Foo = { + __typename?: 'Foo'; + value?: Maybe; + }; + + export type Query = { + __typename?: 'Query'; + foo?: Maybe; + foos?: Maybe>>; + }; + + export type FooQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FooQuery = { __typename?: 'Query', foo?: ( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null }; + + export type FoosQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FoosQuery = { __typename?: 'Query', foos?: Array<( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null> | null }; + + export type FooFragment = { __typename?: 'Foo', value?: string | null } & { ' $fragmentName'?: 'FooFragment' }; + + export class TypedDocumentString + extends String + implements DocumentTypeDecoration + { + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: Record) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } + } + export const FooFragmentDoc = new TypedDocumentString(\` + fragment Foo on Foo { + value + } + \`, {"fragmentName":"Foo"}) as unknown as TypedDocumentString; + export const FooDocument = new TypedDocumentString(\` + query Foo { + foo { + ...Foo @defer + } + } + fragment Foo on Foo { + value + }\`, {"deferredFields":{"Foo":["value"]}}) as unknown as TypedDocumentString; + export const FoosDocument = new TypedDocumentString(\` + query Foos { + foos { + ...Foo @defer + } + } + fragment Foo on Foo { + value + }\`, {"deferredFields":{"Foo":["value"]}}) as unknown as TypedDocumentString;" + `); + }); + + it('works with documentMode: string and persisted documents', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo] + } + + type Foo { + value: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/with-deferred-fragment.ts'), + generates: { + 'out1/': { + preset, + presetConfig: { + persistedDocuments: true, + }, + config: { + documentMode: 'string', + }, + }, + }, + }); + + const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); + expect(graphqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { [K in keyof T]: T[K] }; + export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; + export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + }; + + export type Foo = { + __typename?: 'Foo'; + value?: Maybe; + }; + + export type Query = { + __typename?: 'Query'; + foo?: Maybe; + foos?: Maybe>>; + }; + + export type FooQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FooQuery = { __typename?: 'Query', foo?: ( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null }; + + export type FoosQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FoosQuery = { __typename?: 'Query', foos?: Array<( { __typename?: 'Foo' } & ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': Incremental } } + ) ) | null> | null }; + + export type FooFragment = { __typename?: 'Foo', value?: string | null } & { ' $fragmentName'?: 'FooFragment' }; + + export class TypedDocumentString + extends String + implements DocumentTypeDecoration + { + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: Record) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } + } + export const FooFragmentDoc = new TypedDocumentString(\` + fragment Foo on Foo { + value + } + \`, {"fragmentName":"Foo"}) as unknown as TypedDocumentString; + export const FooDocument = new TypedDocumentString(\` + query Foo { + foo { + ...Foo @defer + } + } + fragment Foo on Foo { + value + }\`, {"hash":"39c47d2da0fb0e6867abbe2ec942d9858f2d76c7","deferredFields":{"Foo":["value"]}}) as unknown as TypedDocumentString; + export const FoosDocument = new TypedDocumentString(\` + query Foos { + foos { + ...Foo @defer + } + } + fragment Foo on Foo { + value + }\`, {"hash":"8aba765173b2302b9857334e9959d97a2168dbc8","deferredFields":{"Foo":["value"]}}) as unknown as TypedDocumentString;" + `); + }); + }); + describe('documentMode: "string"', () => { it('generates correct types', async () => { const result = await executeCodegen({ @@ -1709,6 +2136,8 @@ export * from "./gql.js";`); export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + export type MakeEmpty = { [_ in K]?: never }; + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -1753,7 +2182,7 @@ export * from "./gql.js";`); { __apiType?: DocumentTypeDecoration['__apiType']; - constructor(private value: string, public __meta__?: { hash: string }) { + constructor(private value: string, public __meta__?: Record) { super(value); } @@ -1765,7 +2194,7 @@ export * from "./gql.js";`); fragment Foo on Foo { value } - \`) as unknown as TypedDocumentString; + \`, {"fragmentName":"Foo"}) as unknown as TypedDocumentString; export const FooDocument = new TypedDocumentString(\` query Foo { foo { diff --git a/packages/presets/client/tests/fixtures/with-deferred-fragment.ts b/packages/presets/client/tests/fixtures/with-deferred-fragment.ts new file mode 100644 index 00000000000..fcb19040c67 --- /dev/null +++ b/packages/presets/client/tests/fixtures/with-deferred-fragment.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +//@ts-ignore +const Query = gql(/* GraphQL */ ` + query Foo { + foo { + ...Foo @defer + } + } +`); + +//@ts-ignore +const ListQuery = gql(/* GraphQL */ ` + query Foos { + foos { + ...Foo @defer + } + } +`); + +//@ts-ignore +const Fragment = gql(/* GraphQL */ ` + fragment Foo on Foo { + value + } +`); diff --git a/packages/presets/client/tests/fixtures/with-fragment.ts b/packages/presets/client/tests/fixtures/with-fragment.ts index 38738d5c16c..2097beed972 100644 --- a/packages/presets/client/tests/fixtures/with-fragment.ts +++ b/packages/presets/client/tests/fixtures/with-fragment.ts @@ -10,7 +10,7 @@ const Query = gql(/* GraphQL */ ` `); //@ts-ignore -const LsitQuery = gql(/* GraphQL */ ` +const ListQuery = gql(/* GraphQL */ ` query Foos { foos { ...Foo diff --git a/website/src/pages/plugins/presets/preset-client.mdx b/website/src/pages/plugins/presets/preset-client.mdx index 1868816dcce..4eae1bee83d 100644 --- a/website/src/pages/plugins/presets/preset-client.mdx +++ b/website/src/pages/plugins/presets/preset-client.mdx @@ -252,6 +252,61 @@ When dealing with nested Fragments, the `useFragment()` should also be used in a You can find a complete working example here: [Nested Fragment example on GitHub](https://github.com/charlypoly/codegen-repros/blob/master/client-preset-nested-fragments-interface/src/App.tsx). +### Fragment Masking with @defer directive + +If you use the `@defer` directive and have a Fragment Masking setup, you can use an `isFragmentReady` helper to check if the deferred fragment data is already resolved. +The `isFragmentReady` function takes three arguments: the query document, the fragment definition, and the data returned by the +query. You can use it to conditionally render components based on whether the data for a deferred +fragment is available, as shown in the example below: + +```jsx +// index.tsx +import { useQuery } from '@apollo/client'; +import { useFragment, graphql, FragmentType, isFragmentReady } from './gql'; + +const OrdersFragment = graphql(` + fragment OrdersFragment on User { + orders { + id + total + } + } +`) +const GetUserQueryWithDefer = graphql(` + query GetUser($id: ID!) { + user(id: $id) { + id + name + ...OrdersFragment @defer + } + } +`) + +const OrdersList = (props: { data: FragmentType }) => { + const data = useFragment(OrdersFragment, props.data); + return ( + // render orders list + ) +}; + +function App() { + const { data } = useQuery(GetUserQueryWithDefer); + return ( +
+ {data && ( + <> + Name: {data.name} + Id: {data.name} + {isFragmentReady(GetUserQueryWithDefer, OrdersFragment, data) // <- HERE + && } + + )} +
+ ); +} +export default App; +``` + ### Fragment Masking and testing A React component that relies on Fragment Masking won't accept "plain object" as follows: diff --git a/yarn.lock b/yarn.lock index d29dda47a4a..c7967f7277d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -172,7 +172,7 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@apollo/client@^3.6.9", "@apollo/client@^3.7.7": +"@apollo/client@^3.7.10": version "3.7.12" resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.7.12.tgz#9ddd355d0788374cdb900e5f40298b196176952b" integrity sha512-XvH8ssDibx5hR92Tet8CHtUxhiIs+RbYjyxkflAcnF85QT3VacUdNAhjj0OcA2kcZ+5KyceJmilmBNjj6+rJFg== @@ -2569,7 +2569,7 @@ value-or-promise "^1.0.11" ws "^8.12.0" -"@graphql-tools/utils@9.2.1", "@graphql-tools/utils@^9.0.0", "@graphql-tools/utils@^9.1.1", "@graphql-tools/utils@^9.2.1": +"@graphql-tools/utils@9.2.1", "@graphql-tools/utils@^9.0.0", "@graphql-tools/utils@^9.0.1", "@graphql-tools/utils@^9.1.1", "@graphql-tools/utils@^9.2.1": version "9.2.1" resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.2.1.tgz#1b3df0ef166cfa3eae706e3518b17d5922721c57" integrity sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A== @@ -9848,6 +9848,11 @@ lru-cache@^7.14.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.16.1.tgz#7acea16fecd9ed11430e78443c2bb81a06d3dea9" integrity sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w== +lru-cache@^8.0.0: + version "8.0.4" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.4.tgz#49fbbc46c0b4cedc36258885247f93dba341e7ec" + integrity sha512-E9FF6+Oc/uFLqZCuZwRKUzgFt5Raih6LfxknOSAVTjNkrCZkBf7DQCwJxZQgd9l4eHjIJDGR+E+1QKD1RhThPw== + lz-string@^1.4.4: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -13757,7 +13762,7 @@ typescript-json-schema@0.56.0: typescript "~4.9.5" yargs "^17.1.1" -typescript@5.0.4, typescript@^5.0.0, typescript@~4.9.5: +typescript@4.9.5, typescript@5.0.4, typescript@^5.0.0, typescript@~4.9.5: version "5.0.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==