Skip to content
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

transform documents hooks #8723

Merged
merged 18 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/short-toes-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@graphql-codegen/cli": minor
"@graphql-codegen/core": minor
"@graphql-codegen/plugin-helpers": minor
"@graphql-codegen/client-preset": minor
"@graphql-codegen/gql-tag-operations-preset": minor
"@graphql-codegen/graphql-modules-preset": minor
---

Add a feature to transform documents
n1ru4l marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions packages/graphql-codegen-cli/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,14 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom
emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config, filename),
};

const documentTransformPlugins = await Promise.all(
normalizeConfig(outputConfig.documentTransformPlugins).map(async pluginConfig => {
const name = Object.keys(pluginConfig)[0];
const plugin = await getPluginByName(name, pluginLoader);
return { [name]: { plugin, config: Object.values(pluginConfig)[0] } };
})
);

const outputs: Types.GenerateOptions[] = preset
? await context.profiler.run(
async () =>
Expand All @@ -335,6 +343,7 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom
pluginMap,
pluginContext,
profiler: context.profiler,
documentTransformPlugins,
}),
`Build Generates Section: ${filename}`
)
Expand All @@ -349,6 +358,7 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom
pluginMap,
pluginContext,
profiler: context.profiler,
documentTransformPlugins,
},
];

Expand Down
77 changes: 77 additions & 0 deletions packages/graphql-codegen-cli/tests/codegen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,4 +1086,81 @@ describe('Codegen Executor', () => {
const output = await executeCodegen(config);
expect(output[0].content).toContain('DocumentNode<MyQueryQuery, MyQueryQueryVariables>');
});

describe('Document Transform', () => {
it('Should transform documents', async () => {
const output = await executeCodegen({
schema: SIMPLE_TEST_SCHEMA,
documents: `query foo { f }`,
generates: {
'out1.ts': {
plugins: ['typescript', 'typescript-operations'],
documentTransformPlugins: ['./tests/custom-plugins/document-transform.js'],
},
},
});

expect(output.length).toBe(1);
expect(output[0].content).toContain('export type BarQuery');
});

it('Should accept config in per-plugin', async () => {
const output = await executeCodegen({
schema: SIMPLE_TEST_SCHEMA,
documents: `query root { f }`,
generates: {
'out1.ts': {
plugins: ['typescript', 'typescript-operations'],
documentTransformPlugins: [
n1ru4l marked this conversation as resolved.
Show resolved Hide resolved
{
'./tests/custom-plugins/document-transform-config.js': {
queryName: 'test',
},
},
],
},
},
});

expect(output.length).toBe(1);
expect(output[0].content).toContain('export type TestQuery');
});

it('Should allow plugin context to be accessed and modified', async () => {
const output = await executeCodegen({
schema: SIMPLE_TEST_SCHEMA,
documents: `query root { f }`,
generates: {
'out1.ts': {
documentTransformPlugins: ['./tests/custom-plugins/document-transform-context.js'],
plugins: ['./tests/custom-plugins/document-transform-context.js'],
},
},
});

expect(output.length).toBe(1);
expect(output[0].content).toContain('Hello world!');
});

it('Should execute validation before transform documents and throw when it fails', async () => {
try {
await executeCodegen({
schema: SIMPLE_TEST_SCHEMA,
generates: {
'out1.ts': {
plugins: ['typescript'],
documentTransformPlugins: ['./tests/custom-plugins/document-transform-validation.js'],
},
},
});
throw new Error(SHOULD_NOT_THROW_STRING);
} catch (e) {
expect(e.message).not.toBe(SHOULD_NOT_THROW_STRING);
expect(e.message).toContain(
'Document transform "./tests/custom-plugins/document-transform-validation.js" validation failed'
);
expect(e.message).toContain('Invalid!');
}
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
plugin: () => {}, // Nothing to do
transformDocuments: (_schema, documents, config) => {
const newDocuments = [
{
document: {
...documents[0].document,
definitions: [
{
...documents[0].document.definitions[0],
name: { kind: 'Name', value: config.queryName },
},
],
},
},
];
return newDocuments;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
plugin: (_schema, _documents, _config, { pluginContext }) => {
return `Hello ${pluginContext.myPluginInfo}!`;
},
transformDocuments: (_schema, documents, _config, { pluginContext }) => {
pluginContext.myPluginInfo = 'world';
return documents;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
plugin: () => {}, // Nothing to do
transformDocuments: (_schema, documents) => {
return documents;
},
validateBeforeTransformDocuments: () => {
throw new Error('Invalid!');
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
plugin: () => {}, // Nothing to do
transformDocuments: (_schema, documents) => {
const newDocuments = [
{
document: {
...documents[0].document,
definitions: [
{
...documents[0].document.definitions[0],
name: { kind: 'Name', value: 'bar' },
},
],
},
},
];
return newDocuments;
},
};
24 changes: 18 additions & 6 deletions packages/graphql-codegen-core/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
shouldValidateDocumentsAgainstSchema,
shouldValidateDuplicateDocuments,
} from './utils.js';
import { transformDocuments } from './transform-document.js';

export async function codegen(options: Types.GenerateOptions): Promise<string> {
const documents = options.documents || [];
Expand Down Expand Up @@ -72,7 +73,18 @@ export async function codegen(options: Types.GenerateOptions): Promise<string> {
const schemaDocumentNode =
mergeNeeded || !options.schema ? getCachedDocumentNodeFromSchema(schemaInstance) : options.schema;

if (schemaInstance && documents.length > 0 && shouldValidateDocumentsAgainstSchema(skipDocumentsValidation)) {
const transformedDocuments = await transformDocuments({
...options,
schema: schemaDocumentNode,
schemaAst: schemaInstance,
profiler,
});

if (
schemaInstance &&
transformedDocuments.length > 0 &&
shouldValidateDocumentsAgainstSchema(skipDocumentsValidation)
) {
const ignored = ['NoUnusedFragments', 'NoUnusedVariables', 'KnownDirectives'];
if (typeof skipDocumentsValidation === 'object' && skipDocumentsValidation.ignoreRules) {
ignored.push(...asArray(skipDocumentsValidation.ignoreRules));
Expand All @@ -88,26 +100,26 @@ export async function codegen(options: Types.GenerateOptions): Promise<string> {
const rules = specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule)));
const schemaHash = extractHashFromSchema(schemaInstance);

if (!schemaHash || !options.cache || documents.some(d => typeof d.hash !== 'string')) {
if (!schemaHash || !options.cache || transformedDocuments.some(d => typeof d.hash !== 'string')) {
return Promise.resolve(
validateGraphQlDocuments(
schemaInstance,
[...documents.flatMap(d => d.document), ...fragments.flatMap(f => f.document)],
[...transformedDocuments.flatMap(d => d.document), ...fragments.flatMap(f => f.document)],
rules
)
);
}

const cacheKey = [schemaHash]
.concat(documents.map(doc => doc.hash))
.concat(transformedDocuments.map(doc => doc.hash))
.concat(JSON.stringify(fragments))
.join(',');

return options.cache('documents-validation', cacheKey, () =>
Promise.resolve(
validateGraphQlDocuments(
schemaInstance,
[...documents.flatMap(d => d.document), ...fragments.flatMap(f => f.document)],
[...transformedDocuments.flatMap(d => d.document), ...fragments.flatMap(f => f.document)],
rules
)
)
Expand Down Expand Up @@ -148,7 +160,7 @@ export async function codegen(options: Types.GenerateOptions): Promise<string> {
parentConfig: options.config,
schema: schemaDocumentNode,
schemaAst: schemaInstance,
documents: options.documents,
documents: transformedDocuments,
outputFilename: options.filename,
allPlugins: options.plugins,
skipDocumentsValidation: options.skipDocumentsValidation,
Expand Down
65 changes: 65 additions & 0 deletions packages/graphql-codegen-core/src/transform-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createNoopProfiler, Types } from '@graphql-codegen/plugin-helpers';
import { buildASTSchema, GraphQLSchema } from 'graphql';

export async function transformDocuments(options: Types.GenerateOptions): Promise<Types.DocumentFile[]> {
const documentTransformPlugins = options.documentTransformPlugins || [];
let documents = options.documents;
if (documentTransformPlugins.length === 0) {
return documents;
}

const profiler = options.profiler ?? createNoopProfiler();
const outputSchema: GraphQLSchema = options.schemaAst || buildASTSchema(options.schema, options.config as any);

for (const documentTransformPlugin of documentTransformPlugins) {
const name = Object.keys(documentTransformPlugin)[0];
const transformPlugin = documentTransformPlugin[name].plugin;
const pluginConfig = documentTransformPlugin[name].config;

const config =
typeof pluginConfig !== 'object'
? pluginConfig
: {
...options.config,
...pluginConfig,
};

if (
transformPlugin.validateBeforeTransformDocuments &&
typeof transformPlugin.validateBeforeTransformDocuments === 'function'
) {
try {
await profiler.run(
async () =>
transformPlugin.validateBeforeTransformDocuments(
outputSchema,
options.documents,
config,
options.filename,
options.plugins,
options.pluginContext
),
`Document transform ${name} validate`
);
} catch (e) {
throw new Error(
`Document transform "${name}" validation failed: \n
${e.message}
`
);
}
}

if (transformPlugin.transformDocuments && typeof transformPlugin.transformDocuments === 'function') {
await profiler.run(async () => {
documents = await transformPlugin.transformDocuments(outputSchema, documents, config, {
outputFile: options.filename,
allPlugins: options.plugins,
pluginContext: options.pluginContext,
});
}, `Document transform ${name} execution`);
}
}

return documents;
}
4 changes: 4 additions & 0 deletions packages/presets/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
unmaskFunctionName: fragmentMaskingConfig.unmaskFunctionName,
},
documents: [],
documentTransformPlugins: options.documentTransformPlugins,
};
}

Expand All @@ -191,6 +192,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
schema: options.schema,
config: {},
documents: [],
documentTransformPlugins: options.documentTransformPlugins,
};
}

Expand All @@ -205,6 +207,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
...forwardedConfig,
},
documents: sources,
documentTransformPlugins: options.documentTransformPlugins,
},
{
filename: `${options.baseOutputDir}gql${gqlArtifactFileExtension}`,
Expand All @@ -216,6 +219,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
gqlTagName: options.presetConfig.gqlTagName || 'graphql',
},
documents: sources,
documentTransformPlugins: options.documentTransformPlugins,
},
...(fragmentMaskingFileGenerateConfig ? [fragmentMaskingFileGenerateConfig] : []),
...(indexFileGenerateConfig ? [indexFileGenerateConfig] : []),
Expand Down
4 changes: 4 additions & 0 deletions packages/presets/gql-tag-operations/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export const preset: Types.OutputPreset<GqlTagConfig> = {
unmaskFunctionName: fragmentMaskingConfig.unmaskFunctionName,
},
documents: [],
documentTransformPlugins: options.documentTransformPlugins,
};
}

Expand All @@ -230,6 +231,7 @@ export const preset: Types.OutputPreset<GqlTagConfig> = {
schema: options.schema,
config: {},
documents: [],
documentTransformPlugins: options.documentTransformPlugins,
};
}

Expand All @@ -241,6 +243,7 @@ export const preset: Types.OutputPreset<GqlTagConfig> = {
schema: options.schema,
config,
documents: sources,
documentTransformPlugins: options.documentTransformPlugins,
},
{
filename: `${options.baseOutputDir}/gql${gqlArtifactFileExtension}`,
Expand All @@ -253,6 +256,7 @@ export const preset: Types.OutputPreset<GqlTagConfig> = {
gqlTagName: options.presetConfig.gqlTagName || 'gql',
},
documents: sources,
documentTransformPlugins: options.documentTransformPlugins,
},
...(fragmentMaskingFileGenerateConfig ? [fragmentMaskingFileGenerateConfig] : []),
...(indexFileGenerateConfig ? [indexFileGenerateConfig] : []),
Expand Down
2 changes: 2 additions & 0 deletions packages/presets/graphql-modules/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const preset: Types.OutputPreset<ModulesConfig> = {
enumsAsTypes: true,
},
schemaAst: options.schemaAst!,
documentTransformPlugins: options.documentTransformPlugins,
};

const baseTypesFilename = baseTypesPath.replace(/\.(js|ts|d.ts)$/, '');
Expand Down Expand Up @@ -120,6 +121,7 @@ export const preset: Types.OutputPreset<ModulesConfig> = {
},
config: options.config,
schemaAst: options.schemaAst,
documentTransformPlugins: options.documentTransformPlugins,
};
});

Expand Down
Loading