-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[operations][client-preset] CODEGEN-500 - Support semantic non-null for clients #10323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8759145
b7d75d5
c223144
a8ca9a6
f496af2
a83c6c8
f747362
f877d4e
42a33d9
591d10d
739ada6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@graphql-codegen/typescript-operations': minor | ||
| --- | ||
|
|
||
| Add support for semanticNonNull.errorHandlingClient |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -292,4 +292,42 @@ export interface TypeScriptDocumentsPluginConfig extends RawDocumentsConfig { | |
| */ | ||
|
|
||
| allowUndefinedQueryVariables?: boolean; | ||
|
|
||
| /** | ||
| * @description Options related to `@semanticNonNull` directive | ||
| * @exampleMarkdown | ||
| * ## `errorHandlingClient` | ||
| * When using error handling clients, a semantic non-nullable field can never be `null`. | ||
| * If a field is read and its value is `null`, there must be a respective error. The error handling client will throw in this case, so the `null` value is never read. | ||
| * | ||
| * To enable this option, install `graphql-sock` peer dependency: | ||
| * | ||
| * ```sh npm2yarn | ||
| * npm install -D graphql-sock | ||
| * ``` | ||
| * | ||
| * Now, you can enable support for error handling clients: | ||
| * | ||
| * ```ts filename="codegen.ts" | ||
| * import type { CodegenConfig } from '@graphql-codegen/cli'; | ||
| * | ||
| * const config: CodegenConfig = { | ||
| * // ... | ||
| * generates: { | ||
| * 'path/to/file.ts': { | ||
| * plugins: ['typescript', 'typescript-operations'], | ||
| * config: { | ||
| * semanticNonNull: { | ||
| * errorHandlingClient: true | ||
| * } | ||
| * }, | ||
| * }, | ||
| * }, | ||
| * }; | ||
| * export default config; | ||
| * ``` | ||
| */ | ||
| semanticNonNull?: { | ||
| errorHandlingClient: boolean; | ||
| }; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking this object can be extended to handle other related There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see That client will not get the semantic nullability information but could still use I would keep it simple/yagni with just a plain Boolean option: semanticNonNull?: booleanIf really you want to bundle nullability options together, maybe something like so: nullability?: {
semanticNonNull: boolean;
catch: boolean;
} |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,13 +6,15 @@ import { TypeScriptDocumentsVisitor } from './visitor.js'; | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| export { TypeScriptDocumentsPluginConfig } from './config.js'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export const plugin: PluginFunction<TypeScriptDocumentsPluginConfig, Types.ComplexPluginOutput> = ( | ||||||||||||||||||||||
| export const plugin: PluginFunction<TypeScriptDocumentsPluginConfig, Types.ComplexPluginOutput> = async ( | ||||||||||||||||||||||
| schema: GraphQLSchema, | ||||||||||||||||||||||
| rawDocuments: Types.DocumentFile[], | ||||||||||||||||||||||
| config: TypeScriptDocumentsPluginConfig | ||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||
| const transformedSchema = config.semanticNonNull?.errorHandlingClient ? await semanticToStrict(schema) : schema; | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally would rename the
Suggested change
(This would also mean no other changes were needed in the function.) |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const documents = config.flattenGeneratedTypes | ||||||||||||||||||||||
| ? optimizeOperations(schema, rawDocuments, { | ||||||||||||||||||||||
| ? optimizeOperations(transformedSchema, rawDocuments, { | ||||||||||||||||||||||
| includeFragments: config.flattenGeneratedTypesIncludeFragments, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| : rawDocuments; | ||||||||||||||||||||||
|
|
@@ -30,7 +32,7 @@ export const plugin: PluginFunction<TypeScriptDocumentsPluginConfig, Types.Compl | |||||||||||||||||||||
| ...(config.externalFragments || []), | ||||||||||||||||||||||
| ]; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const visitor = new TypeScriptDocumentsVisitor(schema, config, allFragments); | ||||||||||||||||||||||
| const visitor = new TypeScriptDocumentsVisitor(transformedSchema, config, allFragments); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const visitorResult = oldVisit(allAst, { | ||||||||||||||||||||||
| leave: visitor, | ||||||||||||||||||||||
|
|
@@ -64,3 +66,14 @@ export const plugin: PluginFunction<TypeScriptDocumentsPluginConfig, Types.Compl | |||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export { TypeScriptDocumentsVisitor }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const semanticToStrict = async (schema: GraphQLSchema): Promise<GraphQLSchema> => { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const sock = await import('graphql-sock'); | ||||||||||||||||||||||
| return sock.semanticToStrict(schema); | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||
| "To use the `customDirective.semanticNonNull` option, you must install the 'graphql-sock' package." | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import { buildSchema, parse } from 'graphql'; | ||
| import * as prettier from 'prettier'; | ||
| import { plugin } from '../src/index.js'; | ||
|
|
||
| const schema = buildSchema(/* GraphQL */ ` | ||
| directive @semanticNonNull(levels: [Int] = [0]) on FIELD_DEFINITION | ||
|
|
||
| type Query { | ||
| me: User | ||
| } | ||
|
|
||
| type User { | ||
| field: String @semanticNonNull | ||
| fieldLevel0: String @semanticNonNull(levels: [0]) | ||
| fieldLevel1: String @semanticNonNull(levels: [1]) | ||
| fieldBothLevels: String @semanticNonNull(levels: [0, 1]) | ||
| list: [String] @semanticNonNull | ||
| listLevel0: [String] @semanticNonNull(levels: [0]) | ||
| listLevel1: [String] @semanticNonNull(levels: [1]) | ||
| listBothLevels: [String] @semanticNonNull(levels: [0, 1]) | ||
| nonNullableList: [String]! @semanticNonNull | ||
| nonNullableListLevel0: [String]! @semanticNonNull(levels: [0]) | ||
| nonNullableListLevel1: [String]! @semanticNonNull(levels: [1]) | ||
| nonNullableListBothLevels: [String]! @semanticNonNull(levels: [0, 1]) | ||
| listWithNonNullableItem: [String!] @semanticNonNull | ||
| listWithNonNullableItemLevel0: [String!] @semanticNonNull(levels: [0]) | ||
| listWithNonNullableItemLevel1: [String!] @semanticNonNull(levels: [1]) | ||
| listWithNonNullableItemBothLevels: [String!] @semanticNonNull(levels: [0, 1]) | ||
| nonNullableListWithNonNullableItem: [String!]! @semanticNonNull | ||
| nonNullableListWithNonNullableItemLevel0: [String!]! @semanticNonNull(levels: [0]) | ||
| nonNullableListWithNonNullableItemLevel1: [String!]! @semanticNonNull(levels: [1]) | ||
| nonNullableListWithNonNullableItemBothLevels: [String!]! @semanticNonNull(levels: [0, 1]) | ||
| } | ||
| `); | ||
|
|
||
| const document = parse(/* GraphQL */ ` | ||
| query { | ||
| me { | ||
| field | ||
| fieldLevel0 | ||
| fieldLevel1 | ||
| fieldBothLevels | ||
| list | ||
| listLevel0 | ||
| listLevel1 | ||
| listBothLevels | ||
| nonNullableList | ||
| nonNullableListLevel0 | ||
| nonNullableListLevel1 | ||
| nonNullableListBothLevels | ||
| listWithNonNullableItem | ||
| listWithNonNullableItemLevel0 | ||
| listWithNonNullableItemLevel1 | ||
| listWithNonNullableItemBothLevels | ||
| nonNullableListWithNonNullableItem | ||
| nonNullableListWithNonNullableItemLevel0 | ||
| nonNullableListWithNonNullableItemLevel1 | ||
| nonNullableListWithNonNullableItemBothLevels | ||
| } | ||
| } | ||
| `); | ||
|
|
||
| describe('TypeScript Operations Plugin - semanticNonNull', () => { | ||
| it('converts semanticNonNull to nonNull when semanticNonNull.errorHandlingClient=true', async () => { | ||
| const result = await plugin(schema, [{ document }], { | ||
| semanticNonNull: { | ||
| errorHandlingClient: true, | ||
| }, | ||
| }); | ||
|
|
||
| const formattedContent = prettier.format(result.content); | ||
| expect(formattedContent).toMatchInlineSnapshot(` | ||
| "export type Unnamed_1_QueryVariables = Exact<{ [key: string]: never }>; | ||
|
|
||
| export type Unnamed_1_Query = { | ||
| __typename?: "Query", | ||
| me?: { | ||
| __typename?: "User", | ||
| field: string, | ||
| fieldLevel0: string, | ||
| fieldLevel1?: string | null, | ||
| fieldBothLevels: string, | ||
| list: Array<string | null>, | ||
| listLevel0: Array<string | null>, | ||
| listLevel1?: Array<string> | null, | ||
| listBothLevels: Array<string>, | ||
| nonNullableList: Array<string | null>, | ||
| nonNullableListLevel0: Array<string | null>, | ||
| nonNullableListLevel1: Array<string>, | ||
| nonNullableListBothLevels: Array<string>, | ||
| listWithNonNullableItem: Array<string>, | ||
| listWithNonNullableItemLevel0: Array<string>, | ||
| listWithNonNullableItemLevel1?: Array<string> | null, | ||
| listWithNonNullableItemBothLevels: Array<string>, | ||
| nonNullableListWithNonNullableItem: Array<string>, | ||
| nonNullableListWithNonNullableItemLevel0: Array<string>, | ||
| nonNullableListWithNonNullableItemLevel1: Array<string>, | ||
| nonNullableListWithNonNullableItemBothLevels: Array<string>, | ||
| } | null, | ||
| }; | ||
| " | ||
| `); | ||
| }); | ||
|
|
||
| it('does not convert semanticNonNull to nonNull when semanticNonNull.errorHandlingClient=false', async () => { | ||
| const result = await plugin(schema, [{ document }], { | ||
| semanticNonNull: { | ||
| errorHandlingClient: false, | ||
| }, | ||
| }); | ||
|
|
||
| const formattedContent = prettier.format(result.content); | ||
| expect(formattedContent).toMatchInlineSnapshot(` | ||
| "export type Unnamed_1_QueryVariables = Exact<{ [key: string]: never }>; | ||
|
|
||
| export type Unnamed_1_Query = { | ||
| __typename?: "Query", | ||
| me?: { | ||
| __typename?: "User", | ||
| field?: string | null, | ||
| fieldLevel0?: string | null, | ||
| fieldLevel1?: string | null, | ||
| fieldBothLevels?: string | null, | ||
| list?: Array<string | null> | null, | ||
| listLevel0?: Array<string | null> | null, | ||
| listLevel1?: Array<string | null> | null, | ||
| listBothLevels?: Array<string | null> | null, | ||
| nonNullableList: Array<string | null>, | ||
| nonNullableListLevel0: Array<string | null>, | ||
| nonNullableListLevel1: Array<string | null>, | ||
| nonNullableListBothLevels: Array<string | null>, | ||
| listWithNonNullableItem?: Array<string> | null, | ||
| listWithNonNullableItemLevel0?: Array<string> | null, | ||
| listWithNonNullableItemLevel1?: Array<string> | null, | ||
| listWithNonNullableItemBothLevels?: Array<string> | null, | ||
| nonNullableListWithNonNullableItem: Array<string>, | ||
| nonNullableListWithNonNullableItemLevel0: Array<string>, | ||
| nonNullableListWithNonNullableItemLevel1: Array<string>, | ||
| nonNullableListWithNonNullableItemBothLevels: Array<string>, | ||
| } | null, | ||
| }; | ||
| " | ||
| `); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps, and this is just a suggestion, rename this to "semantic nullability" as a more generic term. If we were to accept a solution that used a document directive to enable the syntax, this is the likely name that we would use for that directive, and it doesn't tie the solution to one specific implementation. (GraphQL-SOCK can be updated to work with other solutions without any effort on your part.)