diff --git a/examples/ecommerce/components/DatalayerProvider.tsx b/examples/ecommerce/components/DatalayerProvider.tsx index a57de18c..3cc47795 100644 --- a/examples/ecommerce/components/DatalayerProvider.tsx +++ b/examples/ecommerce/components/DatalayerProvider.tsx @@ -1,15 +1,24 @@ 'use client' -import { Provider, createClient } from '@/fuse/client' +import { + Provider, + ssrExchange, + cacheExchange, + fetchExchange, + createClient, + persistedExchange, +} from '@/fuse/client' import React, { Suspense } from 'react' export const DatalayerProvider = (props: any) => { const [client, ssr] = React.useMemo(() => { - const { client, ssr } = createClient({ + const ssr = ssrExchange(); + const { client } = createClient({ url: process.env.NODE_ENV === 'production' ? 'https://spacex-fuse.vercel.app/api/fuse' : 'http://localhost:3000/api/fuse', + exchanges: [cacheExchange, ssr, persistedExchange, fetchExchange], }) return [client, ssr] diff --git a/examples/spacex/app/api/fuse/route.ts b/examples/spacex/app/api/fuse/route.ts index ed13c1d2..a9a807ea 100644 --- a/examples/spacex/app/api/fuse/route.ts +++ b/examples/spacex/app/api/fuse/route.ts @@ -1,6 +1,12 @@ import { createAPIRouteHandler } from 'fuse/next' +import persistedDocuments from '@/fuse/persisted-documents.json' -const layer = createAPIRouteHandler() +const layer = createAPIRouteHandler({ + persistedOperations: { + enabled: true, + operations: persistedDocuments, + }, +}) export const GET = layer export const POST = layer diff --git a/examples/spacex/app/client/page.tsx b/examples/spacex/app/client/page.tsx index ab9db1ea..9166f324 100644 --- a/examples/spacex/app/client/page.tsx +++ b/examples/spacex/app/client/page.tsx @@ -23,8 +23,8 @@ export default function Page() { } const LaunchesQuery = graphql(` - query Launches_SSR($limit: Int, $offset: Int) { - launches(limit: $limit, offset: $offset) { + query Launches_SSR($offset: Int) { + launches(limit: 10, offset: $offset) { nodes { id ...LaunchFields @@ -44,7 +44,7 @@ function Launches() { const [result] = useQuery({ query: LaunchesQuery, - variables: { limit: 10, offset }, + variables: { offset }, }) return ( diff --git a/examples/spacex/app/rsc/page.tsx b/examples/spacex/app/rsc/page.tsx index 434a7a72..6ea76b88 100644 --- a/examples/spacex/app/rsc/page.tsx +++ b/examples/spacex/app/rsc/page.tsx @@ -3,15 +3,14 @@ import * as React from 'react' import { graphql } from '@/fuse' import { execute } from '@/fuse/server' import { LaunchItem } from '@/components/LaunchItem' -import { headers } from 'next/headers' import styles from './page.module.css' import { PageNumbers } from '@/components/PageNumbers' import { LaunchDetails } from '@/components/LaunchDetails' const LaunchesQuery = graphql(` - query Launches_RSC($limit: Int, $offset: Int) { - launches(limit: $limit, offset: $offset) { + query Launches_RSC($offset: Int) { + launches(limit: 10, offset: $offset) { nodes { id ...LaunchFields diff --git a/examples/spacex/components/DatalayerProvider.tsx b/examples/spacex/components/DatalayerProvider.tsx index a57de18c..9e08793a 100644 --- a/examples/spacex/components/DatalayerProvider.tsx +++ b/examples/spacex/components/DatalayerProvider.tsx @@ -1,15 +1,31 @@ 'use client' -import { Provider, createClient } from '@/fuse/client' +import { + Provider, + ssrExchange, + cacheExchange, + fetchExchange, + createClient, + persistedExchange, + debugExchange, +} from '@/fuse/client' import React, { Suspense } from 'react' export const DatalayerProvider = (props: any) => { const [client, ssr] = React.useMemo(() => { - const { client, ssr } = createClient({ + const ssr = ssrExchange() + const { client } = createClient({ url: process.env.NODE_ENV === 'production' ? 'https://spacex-fuse.vercel.app/api/fuse' : 'http://localhost:3000/api/fuse', + exchanges: [ + cacheExchange, + ssr, + persistedExchange, + debugExchange, + fetchExchange, + ], }) return [client, ssr] diff --git a/examples/spacex/fuse/fragment-masking.ts b/examples/spacex/fuse/fragment-masking.ts index 355c9d1a..2ba06f10 100644 --- a/examples/spacex/fuse/fragment-masking.ts +++ b/examples/spacex/fuse/fragment-masking.ts @@ -1,85 +1,66 @@ -import { - ResultOf, - DocumentTypeDecoration, - TypedDocumentNode, -} from '@graphql-typed-document-node/core' -import { FragmentDefinitionNode } from 'graphql' -import { Incremental } from './graphql' +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; -export type FragmentType< - TDocumentType extends DocumentTypeDecoration, -> = TDocumentType extends DocumentTypeDecoration + +export type FragmentType> = TDocumentType extends DocumentTypeDecoration< + infer TType, + any +> ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never : never - : never + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType>, -): TType + fragmentType: FragmentType> +): TType; // return nullable if `fragmentType` is nullable export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: - | FragmentType> - | null - | undefined, -): TType | null | undefined + 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 + 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 + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined; export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: - | FragmentType> - | ReadonlyArray>> - | null - | undefined, + fragmentType: FragmentType> | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { - return fragmentType as any + return fragmentType as any; } + export function makeFragmentData< F extends DocumentTypeDecoration, - FT extends ResultOf, + FT extends ResultOf >(data: FT, _fragment: F): FragmentType { - return data as FragmentType + return data as FragmentType; } export function isFragmentReady( queryNode: DocumentTypeDecoration, fragmentNode: TypedDocumentNode, - data: - | FragmentType, any>> - | null - | undefined, + data: FragmentType, any>> | null | undefined ): data is FragmentType { - const deferredFields = ( - queryNode as { - __meta__?: { deferredFields: Record } - } - ).__meta__?.deferredFields + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; - if (!deferredFields) return true + if (!deferredFields) return true; - const fragDef = fragmentNode.definitions[0] as - | FragmentDefinitionNode - | undefined - const fragName = fragDef?.name?.value + 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) + const fields = (fragName && deferredFields[fragName]) || []; + return fields.length > 0 && fields.every(field => data && field in data); } diff --git a/examples/spacex/fuse/gql.ts b/examples/spacex/fuse/gql.ts index f9995d6c..9988d44e 100644 --- a/examples/spacex/fuse/gql.ts +++ b/examples/spacex/fuse/gql.ts @@ -13,9 +13,9 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - '\n query Launches_SSR($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n': + '\n query Launches_SSR($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n': types.Launches_SsrDocument, - '\n query Launches_RSC($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n': + '\n query Launches_RSC($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n': types.Launches_RscDocument, '\n query LaunchDetails($id: ID!) {\n node(id: $id) {\n ... on Launch {\n id\n name\n details\n launchDate\n image\n site {\n ...LaunchSiteFields\n }\n rocket {\n cost\n country\n company\n description\n }\n }\n }\n }\n': types.LaunchDetailsDocument, @@ -29,7 +29,7 @@ const documents = { types.TotalCountFieldsFragmentDoc, '\n mutation Hello($name: String!) {\n sayHello(name: $name)\n }\n': types.HelloDocument, - '\n query PageLaunches($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n name\n }\n totalCount\n }\n }\n': + '\n query PageLaunches($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n name\n }\n totalCount\n }\n }\n': types.PageLaunchesDocument, } @@ -51,14 +51,14 @@ 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 query Launches_SSR($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n', -): (typeof documents)['\n query Launches_SSR($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n'] + source: '\n query Launches_SSR($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n', +): (typeof documents)['\n query Launches_SSR($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\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 Launches_RSC($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n', -): (typeof documents)['\n query Launches_RSC($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n'] + source: '\n query Launches_RSC($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n', +): (typeof documents)['\n query Launches_RSC($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n ...LaunchFields\n }\n ...TotalCountFields\n }\n }\n'] /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -99,8 +99,8 @@ export function graphql( * 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 PageLaunches($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n name\n }\n totalCount\n }\n }\n', -): (typeof documents)['\n query PageLaunches($limit: Int, $offset: Int) {\n launches(limit: $limit, offset: $offset) {\n nodes {\n id\n name\n }\n totalCount\n }\n }\n'] + source: '\n query PageLaunches($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n name\n }\n totalCount\n }\n }\n', +): (typeof documents)['\n query PageLaunches($offset: Int) {\n launches(limit: 10, offset: $offset) {\n nodes {\n id\n name\n }\n totalCount\n }\n }\n'] export function graphql(source: string) { return (documents as any)[source] ?? {} diff --git a/examples/spacex/fuse/graphql.ts b/examples/spacex/fuse/graphql.ts index 2c9b3256..738e5c80 100644 --- a/examples/spacex/fuse/graphql.ts +++ b/examples/spacex/fuse/graphql.ts @@ -136,7 +136,6 @@ export type Site = Node & { export type SiteStatus = 'ACTIVE' | 'INACTIVE' | 'UNKNOWN' export type Launches_SsrQueryVariables = Exact<{ - limit?: InputMaybe offset?: InputMaybe }> @@ -156,7 +155,6 @@ export type Launches_SsrQuery = { } export type Launches_RscQueryVariables = Exact<{ - limit?: InputMaybe offset?: InputMaybe }> @@ -250,7 +248,6 @@ export type HelloMutationVariables = Exact<{ export type HelloMutation = { __typename: 'Mutation'; sayHello?: string | null } export type PageLaunchesQueryVariables = Exact<{ - limit?: InputMaybe offset?: InputMaybe }> @@ -276,6 +273,7 @@ export const LaunchFieldsFragmentDoc = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, { kind: 'Field', name: { kind: 'Name', value: 'launchDate' } }, @@ -298,6 +296,7 @@ export const SiteLocationFieldsFragmentDoc = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'latitude' } }, { kind: 'Field', name: { kind: 'Name', value: 'longitude' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, @@ -320,6 +319,7 @@ export const LaunchSiteFieldsFragmentDoc = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, { kind: 'Field', name: { kind: 'Name', value: 'details' } }, @@ -330,6 +330,7 @@ export const LaunchSiteFieldsFragmentDoc = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'FragmentSpread', name: { kind: 'Name', value: 'SiteLocationFields' }, @@ -350,6 +351,7 @@ export const LaunchSiteFieldsFragmentDoc = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'latitude' } }, { kind: 'Field', name: { kind: 'Name', value: 'longitude' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, @@ -372,6 +374,7 @@ export const TotalCountFieldsFragmentDoc = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'totalCount' } }, ], }, @@ -379,6 +382,7 @@ export const TotalCountFieldsFragmentDoc = { ], } as unknown as DocumentNode export const Launches_SsrDocument = { + __meta__: { hash: '152c2558141de086b4bd6905725533e9f7949725' }, kind: 'Document', definitions: [ { @@ -386,14 +390,6 @@ export const Launches_SsrDocument = { operation: 'query', name: { kind: 'Name', value: 'Launches_SSR' }, variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'limit' }, - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, - }, { kind: 'VariableDefinition', variable: { @@ -406,6 +402,7 @@ export const Launches_SsrDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'launches' }, @@ -413,10 +410,7 @@ export const Launches_SsrDocument = { { kind: 'Argument', name: { kind: 'Name', value: 'limit' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'limit' }, - }, + value: { kind: 'IntValue', value: '10' }, }, { kind: 'Argument', @@ -430,12 +424,17 @@ export const Launches_SsrDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'nodes' }, selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'FragmentSpread', @@ -464,6 +463,7 @@ export const Launches_SsrDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, { kind: 'Field', name: { kind: 'Name', value: 'launchDate' } }, @@ -481,6 +481,7 @@ export const Launches_SsrDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'totalCount' } }, ], }, @@ -488,6 +489,7 @@ export const Launches_SsrDocument = { ], } as unknown as DocumentNode export const Launches_RscDocument = { + __meta__: { hash: '0192e96f2b35d87a9354448458e417c76a10df21' }, kind: 'Document', definitions: [ { @@ -495,14 +497,6 @@ export const Launches_RscDocument = { operation: 'query', name: { kind: 'Name', value: 'Launches_RSC' }, variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'limit' }, - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, - }, { kind: 'VariableDefinition', variable: { @@ -515,6 +509,7 @@ export const Launches_RscDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'launches' }, @@ -522,10 +517,7 @@ export const Launches_RscDocument = { { kind: 'Argument', name: { kind: 'Name', value: 'limit' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'limit' }, - }, + value: { kind: 'IntValue', value: '10' }, }, { kind: 'Argument', @@ -539,12 +531,17 @@ export const Launches_RscDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'nodes' }, selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'FragmentSpread', @@ -573,6 +570,7 @@ export const Launches_RscDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, { kind: 'Field', name: { kind: 'Name', value: 'launchDate' } }, @@ -590,6 +588,7 @@ export const Launches_RscDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'totalCount' } }, ], }, @@ -597,6 +596,7 @@ export const Launches_RscDocument = { ], } as unknown as DocumentNode export const LaunchDetailsDocument = { + __meta__: { hash: '06f997a01891cf62f3b0499580b0d70a0d9658ae' }, kind: 'Document', definitions: [ { @@ -616,6 +616,7 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'node' }, @@ -632,6 +633,7 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'InlineFragment', typeCondition: { @@ -641,6 +643,10 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, { @@ -658,6 +664,10 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, { kind: 'FragmentSpread', name: { kind: 'Name', value: 'LaunchSiteFields' }, @@ -671,6 +681,10 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, { kind: 'Field', name: { kind: 'Name', value: 'cost' }, @@ -709,6 +723,7 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'latitude' } }, { kind: 'Field', name: { kind: 'Name', value: 'longitude' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, @@ -726,6 +741,7 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, { kind: 'Field', name: { kind: 'Name', value: 'details' } }, @@ -736,6 +752,7 @@ export const LaunchDetailsDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'FragmentSpread', name: { kind: 'Name', value: 'SiteLocationFields' }, @@ -749,6 +766,7 @@ export const LaunchDetailsDocument = { ], } as unknown as DocumentNode export const HelloDocument = { + __meta__: { hash: '8b1fddae0eef22e17bd8ae5b0f68dc2f7c02ef67' }, kind: 'Document', definitions: [ { @@ -771,6 +789,7 @@ export const HelloDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'sayHello' }, @@ -791,6 +810,7 @@ export const HelloDocument = { ], } as unknown as DocumentNode export const PageLaunchesDocument = { + __meta__: { hash: '05081df7e8571aaa7aa52b3a7abbc88857c55a6c' }, kind: 'Document', definitions: [ { @@ -798,14 +818,6 @@ export const PageLaunchesDocument = { operation: 'query', name: { kind: 'Name', value: 'PageLaunches' }, variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'limit' }, - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, - }, { kind: 'VariableDefinition', variable: { @@ -818,6 +830,7 @@ export const PageLaunchesDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'launches' }, @@ -825,10 +838,7 @@ export const PageLaunchesDocument = { { kind: 'Argument', name: { kind: 'Name', value: 'limit' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'limit' }, - }, + value: { kind: 'IntValue', value: '10' }, }, { kind: 'Argument', @@ -842,12 +852,17 @@ export const PageLaunchesDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, { kind: 'Field', name: { kind: 'Name', value: 'nodes' }, selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'name' } }, ], diff --git a/examples/spacex/fuse/index.ts b/examples/spacex/fuse/index.ts index f9bc8e59..f5159916 100644 --- a/examples/spacex/fuse/index.ts +++ b/examples/spacex/fuse/index.ts @@ -1,2 +1,2 @@ -export * from './fragment-masking' -export * from './gql' +export * from "./fragment-masking"; +export * from "./gql"; \ No newline at end of file diff --git a/examples/spacex/fuse/persisted-documents.json b/examples/spacex/fuse/persisted-documents.json new file mode 100644 index 00000000..24871aee --- /dev/null +++ b/examples/spacex/fuse/persisted-documents.json @@ -0,0 +1,7 @@ +{ + "152c2558141de086b4bd6905725533e9f7949725": "fragment LaunchFields on Launch { __typename id image launchDate name } fragment TotalCountFields on QueryLaunchesList { __typename totalCount } query Launches_SSR($offset: Int) { __typename launches(limit: 10, offset: $offset) { __typename nodes { __typename id ...LaunchFields } ...TotalCountFields } }", + "0192e96f2b35d87a9354448458e417c76a10df21": "fragment LaunchFields on Launch { __typename id image launchDate name } fragment TotalCountFields on QueryLaunchesList { __typename totalCount } query Launches_RSC($offset: Int) { __typename launches(limit: 10, offset: $offset) { __typename nodes { __typename id ...LaunchFields } ...TotalCountFields } }", + "06f997a01891cf62f3b0499580b0d70a0d9658ae": "fragment LaunchSiteFields on Site { __typename details id location { __typename ...SiteLocationFields } name status } fragment SiteLocationFields on Location { __typename latitude longitude name region } query LaunchDetails($id: ID!) { __typename node(id: $id) { __typename ... on Launch { __typename details id image launchDate name rocket { __typename company cost country description } site { __typename ...LaunchSiteFields } } } }", + "8b1fddae0eef22e17bd8ae5b0f68dc2f7c02ef67": "mutation Hello($name: String!) { __typename sayHello(name: $name) }", + "05081df7e8571aaa7aa52b3a7abbc88857c55a6c": "query PageLaunches($offset: Int) { __typename launches(limit: 10, offset: $offset) { __typename nodes { __typename id name } totalCount } }" +} diff --git a/examples/spacex/fuse/server.ts b/examples/spacex/fuse/server.ts index 91fdbaae..9482c1f0 100644 --- a/examples/spacex/fuse/server.ts +++ b/examples/spacex/fuse/server.ts @@ -1,3 +1,4 @@ // This is a generated file! + export * from 'fuse/next/server' diff --git a/examples/spacex/pages/test.tsx b/examples/spacex/pages/test.tsx index 53affee6..08498150 100644 --- a/examples/spacex/pages/test.tsx +++ b/examples/spacex/pages/test.tsx @@ -9,6 +9,7 @@ import { ssrExchange, cacheExchange, fetchExchange, + persistedExchange } from '@/fuse/pages' import { graphql } from '@/fuse' @@ -25,8 +26,8 @@ function Page() { } const LaunchesQuery = graphql(` - query PageLaunches($limit: Int, $offset: Int) { - launches(limit: $limit, offset: $offset) { + query PageLaunches($offset: Int) { + launches(limit: 10, offset: $offset) { nodes { id name @@ -43,7 +44,7 @@ function Launches() { const [result] = useQuery({ query: LaunchesQuery, - variables: { limit: 10, offset }, + variables: { offset }, }) return ( @@ -65,10 +66,10 @@ export async function getServerSideProps() { process.env.NODE_ENV === 'production' ? 'https://spacex-fuse.vercel.app/api/fuse' : 'http://localhost:3000/api/fuse', - exchanges: [cacheExchange, ssrCache, fetchExchange], + exchanges: [cacheExchange, ssrCache, persistedExchange, fetchExchange], }) - await client.query(LaunchesQuery, { limit: 10, offset: 0 }).toPromise() + await client.query(LaunchesQuery, { offset: 0 }).toPromise() const graphqlState = ssrCache.extractData() @@ -79,9 +80,11 @@ export async function getServerSideProps() { } } -export default withGraphQLClient(() => ({ +export default withGraphQLClient((ssrCache) => ({ url: process.env.NODE_ENV === 'production' ? 'https://spacex-fuse.vercel.app/api/fuse' : 'http://localhost:3000/api/fuse', + // TODO: we might want to add these as defaults + exchanges: [cacheExchange, ssrCache, persistedExchange, fetchExchange], }))(Page) diff --git a/examples/spacex/schema.graphql b/examples/spacex/schema.graphql index 4ec9b149..f735bc84 100644 --- a/examples/spacex/schema.graphql +++ b/examples/spacex/schema.graphql @@ -75,4 +75,4 @@ enum SiteStatus { ACTIVE INACTIVE UNKNOWN -} +} \ No newline at end of file diff --git a/packages/core/client.d.ts b/packages/core/client.d.ts index 79ced1f9..f25949c2 100644 --- a/packages/core/client.d.ts +++ b/packages/core/client.d.ts @@ -5,6 +5,7 @@ import { ClientOptions, SSRExchange, Client, + Exchange, } from '@urql/next' export * from 'urql' export { UrqlProvider as Provider, useQuery } @@ -14,3 +15,4 @@ export function createClient(opts: Optional): { client: Client ssr: SSRExchange } +export const persistedExchange: Exchange diff --git a/packages/core/package.json b/packages/core/package.json index 0755eb28..432facd9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -72,12 +72,15 @@ "@graphql-typed-document-node/core": "^3.2.0", "@graphql-yoga/plugin-defer-stream": "^3.0.0", "@graphql-yoga/plugin-disable-introspection": "^2.0.0", + "@graphql-yoga/plugin-persisted-operations": "^3.0.1", "@parcel/watcher": "^2.3.0", "@pothos/core": "^3.38.0", "@pothos/plugin-dataloader": "^3.17.1", "@pothos/plugin-relay": "^3.44.0", "@urql/core": "^4.2.0", + "@urql/exchange-persisted": "^4.1.1", "@urql/next": "^1.1.0", + "@vercel/kv": "^1.0.1", "dataloader": "^2.2.2", "graphql": "^16.8.1", "graphql-scalars": "^1.22.4", diff --git a/packages/core/rsc.d.ts b/packages/core/rsc.d.ts index e8ad6b9e..f6f7a038 100644 --- a/packages/core/rsc.d.ts +++ b/packages/core/rsc.d.ts @@ -4,6 +4,7 @@ import { ClientOptions, AnyVariables, GraphQLRequestParams, + Exchange, } from '@urql/core' import { ExecutionResult } from 'graphql' import { GraphQLParams } from 'graphql-yoga' @@ -22,3 +23,5 @@ export function execute< context?: (params: GraphQLParams) => Record }, ): Promise> + +export const persistedExchange: Exchange diff --git a/packages/core/src/next/client.ts b/packages/core/src/next/client.ts index 0be614ac..78c8bf60 100644 --- a/packages/core/src/next/client.ts +++ b/packages/core/src/next/client.ts @@ -7,6 +7,16 @@ import { ssrExchange, } from '@urql/next' import type { Client, ClientOptions, SSRExchange } from '@urql/next' +import { persistedExchange as urqlPersistedExchange } from '@urql/exchange-persisted' + +export const persistedExchange = urqlPersistedExchange({ + enforcePersistedQueries: process.env.NODE_ENV === 'production', + enableForMutation: true, + preferGetForPersistedQueries: true, + generateHash: (_, document) => { + return Promise.resolve((document as any)['__meta__']['hash']) + }, +}) export * from 'urql' export { useQuery, UrqlProvider as Provider } diff --git a/packages/core/src/next/index.ts b/packages/core/src/next/index.ts index f6f63903..aac8fa68 100644 --- a/packages/core/src/next/index.ts +++ b/packages/core/src/next/index.ts @@ -1,13 +1,15 @@ // @ts-ignore -import { GetContext, builder } from 'fuse' +import { GetContext, builder, NotFoundError, ForbiddenError } from 'fuse' import type { NextApiRequest, NextPageContext, NextApiResponse } from 'next' import { createStellateLoggerPlugin } from 'stellate/graphql-yoga' import { createYoga, GraphQLParams, YogaInitialContext } from 'graphql-yoga' import { useDeferStream } from '@graphql-yoga/plugin-defer-stream' import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection' import { blockFieldSuggestionsPlugin } from '@escape.tech/graphql-armor-block-field-suggestions' +import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations' import { writeFile } from 'fs/promises' import { printSchema } from 'graphql' +import { createClient, VercelKV } from '@vercel/kv' // prettier-ignore const defaultQuery = /* GraphQL */ `query { @@ -26,12 +28,26 @@ type InitialContext = { request: YogaInitialContext['request'] } +type VercelKvPersistedStore = { url: string; token: string; type: 'vercel' } + export function createAPIRouteHandler< AdditionalContext extends Record = any, >(options?: { context?: GetContext stellate?: StellateOptions + persistedOperations?: { + enabled: boolean + operations?: Record + store?: VercelKvPersistedStore + } }) { + let client: VercelKV | undefined + if (options?.persistedOperations?.store?.type === 'vercel') { + client = createClient({ + url: options.persistedOperations.store.url, + token: options.persistedOperations.store.token, + }) + } return (request: Request, context: NextPageContext) => { const completedSchema = builder.toSchema({}) if (process.env.NODE_ENV === 'development') { @@ -73,9 +89,28 @@ export function createAPIRouteHandler< fetchAPI: { Response }, plugins: [ useDeferStream(), + !!options?.persistedOperations?.enabled && + usePersistedOperations({ + customErrors: { + notFound: NotFoundError, + persistedQueryOnly: ForbiddenError, + keyNotFound: NotFoundError, + }, + allowArbitraryOperations: process.env.NODE_ENV === 'development', + getPersistedOperation(sha256Hash: string) { + if ( + options?.persistedOperations?.store?.type === 'vercel' && + client + ) { + return client.get(sha256Hash) + } else if (options.persistedOperations?.store) { + return options.persistedOperations.store[sha256Hash] + } + }, + }), process.env.NODE_ENV === 'production' && useDisableIntrospection(), process.env.NODE_ENV === 'production' && blockFieldSuggestionsPlugin(), - Boolean(process.env.NODE_ENV === 'production' && options?.stellate) && + !!(process.env.NODE_ENV === 'production' && options?.stellate) && createStellateLoggerPlugin({ serviceName: options!.stellate!.serviceName, token: options!.stellate!.loggingToken, @@ -93,7 +128,20 @@ export function createPagesRouteHandler< >(options?: { context?: GetContext stellate?: StellateOptions + persistedOperations?: { + enabled: boolean + operations?: Record + store?: VercelKvPersistedStore + } }) { + let client: VercelKV | undefined + if (options?.persistedOperations?.store?.type === 'vercel') { + client = createClient({ + url: options.persistedOperations.store.url, + token: options.persistedOperations.store.token, + }) + } + const schema = builder.toSchema({}) if (process.env.NODE_ENV === 'development') { writeFile('./schema.graphql', printSchema(schema), 'utf-8') @@ -132,6 +180,25 @@ export function createPagesRouteHandler< graphqlEndpoint: '/api/fuse', plugins: [ useDeferStream(), + !!options?.persistedOperations?.enabled && + usePersistedOperations({ + customErrors: { + notFound: NotFoundError, + persistedQueryOnly: ForbiddenError, + keyNotFound: NotFoundError, + }, + allowArbitraryOperations: process.env.NODE_ENV === 'development', + getPersistedOperation(sha256Hash: string) { + if ( + options?.persistedOperations?.store?.type === 'vercel' && + client + ) { + return client.get(sha256Hash) + } else if (options.persistedOperations?.store) { + return options.persistedOperations.store[sha256Hash] + } + }, + }), process.env.NODE_ENV === 'production' && useDisableIntrospection(), process.env.NODE_ENV === 'production' && blockFieldSuggestionsPlugin(), Boolean(process.env.NODE_ENV === 'production' && options?.stellate) && diff --git a/packages/core/src/next/pages.ts b/packages/core/src/next/pages.ts index 627610d8..97237cf6 100644 --- a/packages/core/src/next/pages.ts +++ b/packages/core/src/next/pages.ts @@ -14,8 +14,16 @@ import type { ReactNode, ReactElement } from 'react' import type { NextComponentType, NextPage, NextPageContext } from 'next' import type NextApp from 'next/app' import type { AppContext } from 'next/app' +import { persistedExchange as urqlPersistedExchange } from '@urql/exchange-persisted' export * from 'urql' +export const persistedExchange = urqlPersistedExchange({ + enforcePersistedQueries: process.env.NODE_ENV === 'production', + enableForMutation: true, + preferGetForPersistedQueries: true, + generateHash: (_, document) => + Promise.resolve((document as any)['__meta__']['hash']), +}) let ssr: SSRExchange let client: Client | null = null diff --git a/packages/core/src/next/plugin.ts b/packages/core/src/next/plugin.ts index 73ba9b74..7501ff6b 100644 --- a/packages/core/src/next/plugin.ts +++ b/packages/core/src/next/plugin.ts @@ -3,7 +3,7 @@ import { existsSync, promises as fs } from 'fs' import { resolve } from 'path' import { DateTimeResolver, JSONResolver } from 'graphql-scalars' // Add when enabling persisted operations -// import { addTypenameSelectionDocumentTransform } from '@graphql-codegen/client-preset'; +import { addTypenameSelectionDocumentTransform } from '@graphql-codegen/client-preset' interface Options { port?: number @@ -101,9 +101,10 @@ async function boostrapCodegen(port: number, path: string) { [baseDirectory + '/fuse/']: { documents: ['./**/*.{ts,tsx}', '!./{node_modules,.next,.git}/**/*'], preset: 'client', - // presetConfig: { - // persistedDocuments: true, - // }, + presetConfig: { + persistedDocuments: true, + }, + documentTransforms: [addTypenameSelectionDocumentTransform], config: { scalars: { ID: { diff --git a/packages/core/src/next/rsc.ts b/packages/core/src/next/rsc.ts index 231a393b..57f081d1 100644 --- a/packages/core/src/next/rsc.ts +++ b/packages/core/src/next/rsc.ts @@ -14,6 +14,7 @@ import { // @ts-expect-error import { builder } from 'fuse' import { GraphQLParams } from 'graphql-yoga' +import { persistedExchange as urqlPersistedExchange } from '@urql/exchange-persisted' export { registerUrql as registerClient } from '@urql/next/rsc' export * from '@urql/core' @@ -72,3 +73,10 @@ export const createClient = ( } return create(options) } +export const persistedExchange = urqlPersistedExchange({ + enforcePersistedQueries: process.env.NODE_ENV === 'production', + enableForMutation: true, + preferGetForPersistedQueries: true, + generateHash: (_, document) => + Promise.resolve((document as any)['__meta__']['hash']), +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fba8bcfb..f966cb14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ importers: '@graphql-yoga/plugin-disable-introspection': specifier: ^2.0.0 version: 2.0.0(graphql-yoga@5.0.0)(graphql@16.8.1) + '@graphql-yoga/plugin-persisted-operations': + specifier: ^3.0.1 + version: 3.0.1(@graphql-tools/utils@10.0.8)(graphql-yoga@5.0.0)(graphql@16.8.1) '@parcel/watcher': specifier: ^2.3.0 version: 2.3.0 @@ -139,9 +142,15 @@ importers: '@urql/core': specifier: ^4.2.0 version: 4.2.0(graphql@16.8.1) + '@urql/exchange-persisted': + specifier: ^4.1.1 + version: 4.1.1(graphql@16.8.1) '@urql/next': specifier: ^1.1.0 version: 1.1.0(next@14.0.3)(react@18.2.0)(urql@4.0.6) + '@vercel/kv': + specifier: ^1.0.1 + version: 1.0.1 dataloader: specifier: ^2.2.2 version: 2.2.2 @@ -3553,6 +3562,19 @@ packages: graphql-yoga: 5.0.0(graphql@16.8.1) dev: false + /@graphql-yoga/plugin-persisted-operations@3.0.1(@graphql-tools/utils@10.0.8)(graphql-yoga@5.0.0)(graphql@16.8.1): + resolution: {integrity: sha512-CwFC+Cfc+ta78PZcfR8cjpVXuSW+YsASLE1AU3+SvoKCRLDbH+enm+zzoLvk4PPp96rRQtgVGfZ34ZCyhNNIEw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@graphql-tools/utils': ^10.0.0 + graphql: ^15.2.0 || ^16.0.0 + graphql-yoga: ^5.0.1 + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + graphql-yoga: 5.0.0(graphql@16.8.1) + dev: false + /@graphql-yoga/subscription@5.0.0: resolution: {integrity: sha512-Ri7sK8hmxd/kwaEa0YT8uqQUb2wOLsmBMxI90QDyf96lzOMJRgBuNYoEkU1pSgsgmW2glceZ96sRYfaXqwVxUw==} engines: {node: '>=18.0.0'} @@ -5031,6 +5053,12 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false + /@upstash/redis@1.25.1: + resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==} + dependencies: + crypto-js: 4.2.0 + dev: false + /@urql/core@4.2.0(graphql@16.8.1): resolution: {integrity: sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==} dependencies: @@ -5040,6 +5068,15 @@ packages: - graphql dev: false + /@urql/exchange-persisted@4.1.1(graphql@16.8.1): + resolution: {integrity: sha512-Nl1qRjZCV7POEglG6rqFF3phw5xVuK5i6b+CAxWLP8g6+1AC9HOecyLAuDWjMRXZOyJBJImW65R4buDHRW1uBA==} + dependencies: + '@urql/core': 4.2.0(graphql@16.8.1) + wonka: 6.3.4 + transitivePeerDependencies: + - graphql + dev: false + /@urql/next@1.1.0(next@14.0.3)(react@18.2.0)(urql@4.0.6): resolution: {integrity: sha512-yem5319hUn9cg+BIOMBRGb2Xt39Jusz4eu3Vdv0Fb5+mRLal644JxYpB98OBj0idBtMvl04zuJ69FGuHz4Aa6w==} peerDependencies: @@ -5058,6 +5095,13 @@ packages: server-only: 0.0.1 dev: false + /@vercel/kv@1.0.1: + resolution: {integrity: sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==} + engines: {node: '>=14.6'} + dependencies: + '@upstash/redis': 1.25.1 + dev: false + /@vitest/expect@0.34.6: resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} dependencies: @@ -6054,6 +6098,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: