-
Notifications
You must be signed in to change notification settings - Fork 1.4k
documentsReadOnly implemented #10659
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
base: master
Are you sure you want to change the base?
Changes from all commits
3005df7
df25ba7
104e6c2
f89eb27
819cf71
620da75
7f460d3
7ec7e06
c5641fc
75e31dd
1001bed
ee0a41f
efabaa5
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,14 @@ | ||
| --- | ||
| '@graphql-codegen/gql-tag-operations': minor | ||
| '@graphql-codegen/visitor-plugin-common': minor | ||
| '@graphql-codegen/typescript-operations': minor | ||
| '@graphql-codegen/plugin-helpers': minor | ||
| '@graphql-codegen/cli': minor | ||
| '@graphql-codegen/client-preset': minor | ||
| --- | ||
|
|
||
| Add support for `documentsReadOnly` | ||
|
|
||
| `documentsReadOnly` declares GraphQL documents that will be read but will not have type files generated for them. These documents are available to plugins for type resolution (e.g. fragment types), but no output files will be generated based on them. Accepts the same formats as `documents`. | ||
|
|
||
| This config option is useful for monorepos where each project may want to generate types for its own documents, but some may need to read shared fragments from across projects. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /* GraphQL */ ` | ||
| query User($id: ID!) { | ||
| user(id: $id) { | ||
| id | ||
| ...UserFragment | ||
| } | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export type UserQueryVariables = Exact<{ | ||
| id: Scalars['ID']['input']; | ||
| }>; | ||
|
|
||
| export type UserQuery = { | ||
| __typename?: 'Query'; | ||
| user?: { __typename?: 'User'; id: string; name: string; role: UserRole } | null; | ||
| }; | ||
|
|
||
| export type UserFragmentFragment = { | ||
| __typename?: 'User'; | ||
| id: string; | ||
| name: string; | ||
| role: UserRole; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /* GraphQL */ ` | ||
| fragment UserFragment on User { | ||
| id | ||
| name | ||
| role | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export type UserFragmentFragment = { | ||
| __typename?: 'User'; | ||
| id: string; | ||
| name: string; | ||
| role: UserRole; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| type Query { | ||
| user(id: ID!): User | ||
| } | ||
|
|
||
| type User { | ||
| id: ID! | ||
| name: String! | ||
| role: UserRole! | ||
| } | ||
|
|
||
| enum UserRole { | ||
| ADMIN | ||
| CUSTOMER | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ import { | |
| normalizeOutputParam, | ||
| Types, | ||
| } from '@graphql-codegen/plugin-helpers'; | ||
| import { NoTypeDefinitionsFound } from '@graphql-tools/load'; | ||
| import { NoTypeDefinitionsFound, type UnnormalizedTypeDefPointer } from '@graphql-tools/load'; | ||
| import { mergeTypeDefs } from '@graphql-tools/merge'; | ||
| import { CodegenContext, ensureContext } from './config.js'; | ||
| import { getDocumentTransform } from './documentTransforms.js'; | ||
|
|
@@ -86,6 +86,7 @@ export async function executeCodegen( | |
| let rootConfig: { [key: string]: any } = {}; | ||
| let rootSchemas: Types.Schema[]; | ||
| let rootDocuments: Types.OperationDocument[]; | ||
| let rootDocumentsReadOnly: Types.OperationDocument[]; | ||
| const generates: { [filename: string]: Types.ConfiguredOutput } = {}; | ||
|
|
||
| const cache = createCache(); | ||
|
|
@@ -136,6 +137,11 @@ export async function executeCodegen( | |
| /* Normalize root "documents" field */ | ||
| rootDocuments = normalizeInstanceOrArray<Types.OperationDocument>(config.documents); | ||
|
|
||
| /* Normalize root "documentsReadOnly" field */ | ||
| rootDocumentsReadOnly = normalizeInstanceOrArray<Types.OperationDocument>( | ||
| config.documentsReadOnly, | ||
| ); | ||
|
|
||
| /* Normalize "generators" field */ | ||
| const generateKeys = Object.keys(config.generates || {}); | ||
|
|
||
|
|
@@ -228,13 +234,15 @@ export async function executeCodegen( | |
| let outputSchemaAst: GraphQLSchema; | ||
| let outputSchema: DocumentNode; | ||
| const outputFileTemplateConfig = outputConfig.config || {}; | ||
| let outputDocuments: Types.DocumentFile[] = []; | ||
| const outputDocuments: Types.DocumentFile[] = []; | ||
| const outputSpecificSchemas = normalizeInstanceOrArray<Types.Schema>( | ||
| outputConfig.schema, | ||
| ); | ||
| let outputSpecificDocuments = normalizeInstanceOrArray<Types.OperationDocument>( | ||
| outputConfig.documents, | ||
| ); | ||
| let outputSpecificDocumentsReadOnly = | ||
| normalizeInstanceOrArray<Types.OperationDocument>(outputConfig.documentsReadOnly); | ||
|
|
||
| const preset: Types.OutputPreset | null = hasPreset | ||
| ? typeof outputConfig.preset === 'string' | ||
|
|
@@ -247,6 +255,10 @@ export async function executeCodegen( | |
| filename, | ||
| outputSpecificDocuments, | ||
| ); | ||
| outputSpecificDocumentsReadOnly = await preset.prepareDocuments( | ||
| filename, | ||
| outputSpecificDocumentsReadOnly, | ||
| ); | ||
| } | ||
|
|
||
| return subTask.newListr( | ||
|
|
@@ -308,41 +320,97 @@ export async function executeCodegen( | |
| task: wrapTask( | ||
| async () => { | ||
| debugLog(`[CLI] Loading Documents`); | ||
| const documentPointerMap: any = {}; | ||
|
|
||
| const populateDocumentPointerMap = ( | ||
| allDocumentsDenormalizedPointers: Types.OperationDocument[], | ||
| ): UnnormalizedTypeDefPointer => { | ||
| const pointer: UnnormalizedTypeDefPointer = {}; | ||
| for (const denormalizedPtr of allDocumentsDenormalizedPointers) { | ||
| if (typeof denormalizedPtr === 'string') { | ||
| pointer[denormalizedPtr] = {}; | ||
| } else if (typeof denormalizedPtr === 'object') { | ||
| Object.assign(pointer, denormalizedPtr); | ||
| } | ||
| } | ||
| return pointer; | ||
| }; | ||
|
|
||
| const allDocumentsDenormalizedPointers = [ | ||
| ...rootDocuments, | ||
| ...outputSpecificDocuments, | ||
| ]; | ||
| for (const denormalizedPtr of allDocumentsDenormalizedPointers) { | ||
| if (typeof denormalizedPtr === 'string') { | ||
| documentPointerMap[denormalizedPtr] = {}; | ||
| } else if (typeof denormalizedPtr === 'object') { | ||
| Object.assign(documentPointerMap, denormalizedPtr); | ||
| } | ||
| } | ||
| const documentPointerMap = populateDocumentPointerMap( | ||
| allDocumentsDenormalizedPointers, | ||
| ); | ||
|
|
||
| const hash = JSON.stringify(documentPointerMap); | ||
| const result = await cache('documents', hash, async () => { | ||
| try { | ||
| const documents = await context.loadDocuments(documentPointerMap); | ||
| return { | ||
| documents, | ||
| }; | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof NoTypeDefinitionsFound && | ||
| config.ignoreNoDocuments | ||
| ) { | ||
| return { | ||
| documents: [], | ||
| }; | ||
| const outputDocumentsStandard = await cache( | ||
| 'documents', | ||
| hash, | ||
| async (): Promise<Types.DocumentFile[]> => { | ||
| try { | ||
| const documents = await context.loadDocuments( | ||
| documentPointerMap, | ||
| 'standard', | ||
| ); | ||
| return documents; | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof NoTypeDefinitionsFound && | ||
| config.ignoreNoDocuments | ||
| ) { | ||
| return []; | ||
| } | ||
| throw error; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| const allReadOnlyDenormalizedPointers = [ | ||
| ...rootDocumentsReadOnly, | ||
| ...outputSpecificDocumentsReadOnly, | ||
| ]; | ||
|
|
||
| const readOnlyPointerMap = populateDocumentPointerMap( | ||
| allReadOnlyDenormalizedPointers, | ||
| ); | ||
|
|
||
| const readOnlyHash = JSON.stringify(readOnlyPointerMap); | ||
| const outputDocumentsReadOnly = await cache( | ||
| 'documents', | ||
| readOnlyHash, | ||
| async (): Promise<Types.DocumentFile[]> => { | ||
| try { | ||
| const documents = await context.loadDocuments( | ||
| readOnlyPointerMap, | ||
| 'read-only', | ||
| ); | ||
| return documents; | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof NoTypeDefinitionsFound && | ||
| config.ignoreNoDocuments | ||
| ) { | ||
| return []; | ||
| } | ||
| throw error; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| throw error; | ||
| const processedFile: Record<string, true> = {}; | ||
| const mergedDocuments = [ | ||
| ...outputDocumentsStandard, | ||
| ...outputDocumentsReadOnly, | ||
| ]; | ||
| for (const file of mergedDocuments) { | ||
| if (processedFile[file.hash]) { | ||
| continue; | ||
| } | ||
| }); | ||
|
|
||
| outputDocuments = result.documents; | ||
| outputDocuments.push(file); | ||
| processedFile[file.hash] = true; | ||
| } | ||
|
Comment on lines
+401
to
+413
Collaborator
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've considered forwarding
For this reason, I feel it's easier if we merge them as early as possible, and run the same processing over both types, and plugins like typescript-operations can handle the filtering without having to access another param |
||
| }, | ||
| filename, | ||
| `Load GraphQL documents: ${filename}`, | ||
|
|
@@ -437,7 +505,7 @@ export async function executeCodegen( | |
| pluginContext, | ||
| profiler: context.profiler, | ||
| documentTransforms, | ||
| }, | ||
| } satisfies Types.GenerateOptions, | ||
| ]; | ||
|
|
||
| const process = async (outputArgs: Types.GenerateOptions) => { | ||
|
|
@@ -519,9 +587,7 @@ export async function executeCodegen( | |
| const errors = executedContext.errors.map(subErr => subErr.message || subErr.toString()); | ||
| error = new AggregateError(executedContext.errors, String(errors.join('\n\n'))); | ||
| // Best-effort to all stack traces for debugging | ||
| error.stack = `${error.stack}\n\n${executedContext.errors | ||
| .map(subErr => subErr.stack) | ||
| .join('\n\n')}`; | ||
| error.stack = `${error.stack}\n\n${executedContext.errors.map(subErr => subErr.stack).join('\n\n')}`; | ||
| } | ||
|
|
||
| return { result, error }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { extname, join } from 'path'; | ||
| import { GraphQLError, GraphQLSchema } from 'graphql'; | ||
| import type { Source } from 'graphql-config'; | ||
| import { Types } from '@graphql-codegen/plugin-helpers'; | ||
| import { ApolloEngineLoader } from '@graphql-tools/apollo-engine-loader'; | ||
| import { CodeFileLoader } from '@graphql-tools/code-file-loader'; | ||
|
|
@@ -69,7 +70,7 @@ export async function loadSchema( | |
| export async function loadDocuments( | ||
| documentPointers: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[], | ||
| config: Types.Config, | ||
| ): Promise<Types.DocumentFile[]> { | ||
| ): Promise<Source[]> { | ||
|
Contributor
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. The 2 new fields were added to
Collaborator
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. Sorry I don't fully understand this statement. However I've traced the CLI code to use the right type:
I feel this may not answer your concern. Could you please help me understand this a bit more?
Contributor
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 was a bit worried that the |
||
| const loaders = [ | ||
| new CodeFileLoader({ | ||
| pluckConfig: { | ||
|
|
||
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.
It's the last chance to consider other names, such as:
externalDocuments,referenceDocuments,documentsReadOnlyhas its clear benefits as well, so I don't mind keeping it.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.
The pro of
documentsReadOnlyis that it looks related todocumentsThe con of
documentsReadOnlyis that it doesn't sound like English.externalDocumentssounds better. I'm happy to go with that if you are 🙂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.
Sorry for bringing this one up!
It's really hard to decide!
documentsReadOnlyhas clear benefits. It is self-descriptive and it appears neardocuments(for code completion) - which is quite valuable as well. But it is not a proper English.externalDocuments: The feature is about documents that are loaded for reference but don't produce output. ReadOnly describes how the current output treats them, not a property of the documents themselves - which makesdocumentsReadOnlyname slightly confusing.externalDocumentsis a proper name - in terms of English and semantics, but it is not self-explanatory.I guess a user of this new feature would have to go to the documentation in any case (either slightly confusing name or not informative enough name), so a better sounding name has a slight benefit, imho.
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.
Not at all! Naming is hard! Glad I can bounce ideas with you 🤝
Let's lock
externalDocumentsin. I like the reasoning here.Code completion show
externalDocumentsas well if users start typingdocuments, so it's not bad in that regard.