diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 54b72f106..ceb1bd7d0 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -56,6 +56,7 @@ export const defaultConfig: Omit, 'transformSchema'> & enumsAsStrings: false, enumsAsConst: false, preImport: '', + disableUndefinedScalar: false, }; function warnConfig( diff --git a/packages/cli/src/generate.ts b/packages/cli/src/generate.ts index d3959f597..26878f551 100644 --- a/packages/cli/src/generate.ts +++ b/packages/cli/src/generate.ts @@ -90,6 +90,16 @@ export interface GenerateOptions { schema: GraphQLSchema, graphql_js: typeof graphql ) => Promise | GraphQLSchema; + + /** + * Disable making all scalar fields undefined, only use when: + * 1. using `useQuery()` with `prepare` and `suspense` enabled. + * 2. `useTransactionQuery()` with suspense enabled. + * 3. `useLazyQuery()` and `useMutation()` after invoked. + * + * Be sure to be using only of such combination when using this flag on true. + */ + disableUndefinedScalar?: boolean; } export interface TransformSchemaOptions { @@ -109,6 +119,7 @@ export async function generate( endpoint, enumsAsStrings, enumsAsConst, + disableUndefinedScalar, subscriptions, javascriptOutput, transformSchema, @@ -150,6 +161,9 @@ export async function generate( } enumsAsConst ??= gqtyConfig.enumsAsConst ?? defaultConfig.enumsAsConst; + disableUndefinedScalar ??= + gqtyConfig.disableUndefinedScalar ?? defaultConfig.disableUndefinedScalar; + scalarTypes ||= gqtyConfig.scalarTypes || defaultConfig.scalarTypes; endpoint ||= gqtyConfig.introspection?.endpoint ?? defaultConfig.introspection.endpoint; @@ -782,7 +796,9 @@ export async function generate( [K in keyof T]: T[K] | undefined; }; - export interface ScalarsEnums extends MakeNullable { + export interface ScalarsEnums extends ${ + disableUndefinedScalar ? 'Scalars' : 'MakeNullable' + } { ${deps.sortBy(enumsNames).reduce((acum, enumName) => { acum += `${enumName}: ${enumName} | undefined;`; return acum; diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index 21f7c5451..f150b3708 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -279,6 +279,281 @@ test('basic functionality works', async () => { ).toBeTruthy(); }); +test('disable scalar works', async () => { + const { getEnveloped } = await createTestApp({ + schema: { + typeDefs: gql` + "Query" + type Query { + "Hello field" + hello: String! + deprecatedArg(arg: Int = 123): Int @deprecated + } + `, + resolvers: { + Query: { + hello() { + return 'hello world'; + }, + }, + }, + }, + }); + + const shouldBeIncluded = '// This should be included'; + + const { schemaCode, clientCode, generatedSchema, scalarsEnumsHash } = + await generate(getEnveloped().schema, { + preImport: ` + ${shouldBeIncluded} + `, + react: true, + subscriptions: true, + }); + + expect(schemaCode).toMatchInlineSnapshot(` + "/** + * GQTY AUTO-GENERATED CODE: PLEASE DO NOT MODIFY MANUALLY + */ + + // This should be included + + 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; + }; + /** All built-in and custom scalars, mapped to their actual values */ + export interface Scalars { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + } + + export const scalarsEnumsHash: import('gqty').ScalarsEnumsHash = { + Boolean: true, + Int: true, + String: true, + }; + export const generatedSchema = { + mutation: {}, + query: { + __typename: { __type: 'String!' }, + deprecatedArg: { __type: 'Int', __args: { arg: 'Int' } }, + hello: { __type: 'String!' }, + }, + subscription: {}, + } as const; + + export interface Mutation { + __typename?: 'Mutation'; + } + + /** + * Query + */ + export interface Query { + __typename?: 'Query'; + /** + * @deprecated No longer supported + */ + deprecatedArg: (args?: { + /** + * @defaultValue \`123\` + */ + arg?: Maybe; + }) => Maybe; + /** + * Hello field + */ + hello: ScalarsEnums['String']; + } + + export interface Subscription { + __typename?: 'Subscription'; + } + + export interface SchemaObjectTypes { + Mutation: Mutation; + Query: Query; + Subscription: Subscription; + } + export type SchemaObjectTypesNames = 'Mutation' | 'Query' | 'Subscription'; + + export interface GeneratedSchema { + query: Query; + mutation: Mutation; + subscription: Subscription; + } + + export type MakeNullable = { + [K in keyof T]: T[K] | undefined; + }; + + export interface ScalarsEnums extends Scalars {} + " + `); + + expect(clientCode).toMatchInlineSnapshot(` + "/** + * GQTY: You can safely modify this file and Query Fetcher based on your needs + */ + + import { createReactClient } from '@gqty/react'; + import { createSubscriptionsClient } from '@gqty/subscriptions'; + import type { QueryFetcher } from 'gqty'; + import { createClient } from 'gqty'; + import type { + GeneratedSchema, + SchemaObjectTypes, + SchemaObjectTypesNames, + } from './schema.generated'; + import { generatedSchema, scalarsEnumsHash } from './schema.generated'; + + const queryFetcher: QueryFetcher = async function ( + query, + variables, + fetchOptions + ) { + // Modify "/api/graphql" if needed + const response = await fetch('/api/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables, + }), + mode: 'cors', + ...fetchOptions, + }); + + const json = await response.json(); + + return json; + }; + + const subscriptionsClient = + typeof window !== 'undefined' + ? createSubscriptionsClient({ + wsEndpoint: () => { + // Modify if needed + const url = new URL('/api/graphql', window.location.href); + url.protocol = url.protocol.replace('http', 'ws'); + return url.href; + }, + }) + : undefined; + + export const client = createClient< + GeneratedSchema, + SchemaObjectTypesNames, + SchemaObjectTypes + >({ + schema: generatedSchema, + scalarsEnumsHash, + queryFetcher, + subscriptionsClient, + }); + + const { query, mutation, mutate, subscription, resolved, refetch, track } = + client; + + export { query, mutation, mutate, subscription, resolved, refetch, track }; + + const { + graphql, + useQuery, + usePaginatedQuery, + useTransactionQuery, + useLazyQuery, + useRefetch, + useMutation, + useMetaState, + prepareReactRender, + useHydrateCache, + prepareQuery, + useSubscription, + } = createReactClient(client, { + defaults: { + // Set this flag as "true" if your usage involves React Suspense + // Keep in mind that you can overwrite it in a per-hook basis + suspense: false, + + // Set this flag based on your needs + staleWhileRevalidate: false, + }, + }); + + export { + graphql, + useQuery, + usePaginatedQuery, + useTransactionQuery, + useLazyQuery, + useRefetch, + useMutation, + useMetaState, + prepareReactRender, + useHydrateCache, + prepareQuery, + useSubscription, + }; + + export * from './schema.generated'; + " + `); + + expect(JSON.stringify(generatedSchema, null, 2)).toMatchInlineSnapshot(` + "{ + "query": { + "__typename": { + "__type": "String!" + }, + "deprecatedArg": { + "__type": "Int", + "__args": { + "arg": "Int" + } + }, + "hello": { + "__type": "String!" + } + }, + "mutation": {}, + "subscription": {} + }" + `); + + expect(JSON.stringify(scalarsEnumsHash, null, 2)).toMatchInlineSnapshot(` + "{ + "Boolean": true, + "Int": true, + "String": true + }" + `); + + expect(clientCode.includes('= createReactClient')).toBeTruthy(); + + expect( + schemaCode + .split('\n') + .slice(3) + .join('\n') + .trim() + .startsWith(shouldBeIncluded) + ).toBeTruthy(); +}); + test('custom scalars works', async () => { const { getEnveloped } = await createTestApp({ schema: {