Skip to content

Commit

Permalink
Introduce requireRootResolvers in GraphQL Modules preset (#6796)
Browse files Browse the repository at this point in the history
* Introduce requireRootResolvers in GraphQL Modules preset

* changesets

* chore: prettier and clean debug code

Co-authored-by: Charly POLY <[email protected]>
  • Loading branch information
kamilkisiela and charlypoly authored Aug 4, 2022
1 parent a95fbad commit 8b6e8e6
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-geckos-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/graphql-modules-preset': minor
---

Introduce requireRootResolvers flag
21 changes: 15 additions & 6 deletions packages/presets/graphql-modules/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function buildModule(
importNamespace,
importPath,
encapsulate,
requireRootResolvers,
shouldDeclare,
rootTypes,
schema,
Expand All @@ -51,6 +52,7 @@ export function buildModule(
importNamespace: string;
importPath: string;
encapsulate: ModulesConfig['encapsulateModuleTypes'];
requireRootResolvers: ModulesConfig['requireRootResolvers'];
shouldDeclare: boolean;
rootTypes: string[];
baseVisitor: BaseVisitor;
Expand Down Expand Up @@ -210,6 +212,8 @@ export function buildModule(
printResolverType(
name,
'DefinedFields',
// In case of enabled `requireRootResolvers` flag, the preset has to produce a non-optional properties.
requireRootResolvers && rootTypes.includes(name),
!rootTypes.includes(name) && defined.objects.includes(name) ? ` | '__isTypeOf'` : ''
)
)
Expand Down Expand Up @@ -253,7 +257,10 @@ export function buildModule(
if (k === 'scalars') {
lines.push(`${typeName}?: ${encapsulateTypeName(importNamespace)}.Resolvers['${typeName}'];`);
} else {
lines.push(`${typeName}?: ${encapsulateTypeName(typeName)}Resolvers;`);
// In case of enabled `requireRootResolvers` flag, the preset has to produce a non-optional property.
const fieldModifier = requireRootResolvers && rootTypes.includes(typeName) ? '' : '?';

lines.push(`${typeName}${fieldModifier}: ${encapsulateTypeName(typeName)}Resolvers;`);
}
});
}
Expand Down Expand Up @@ -297,12 +304,14 @@ export function buildModule(
return `${path}?: gm.Middleware[];`;
}

function printResolverType(typeName: string, picksTypeName: string, extraKeys = '') {
return `export type ${encapsulateTypeName(
`${typeName}Resolvers`
)} = Pick<${importNamespace}.${baseVisitor.convertName(typeName, {
function printResolverType(typeName: string, picksTypeName: string, requireFieldsResolvers = false, extraKeys = '') {
const typeSignature = `Pick<${importNamespace}.${baseVisitor.convertName(typeName, {
suffix: 'Resolvers',
})}, ${picksTypeName}['${typeName}']${extraKeys}>;`;
})}, ${picksTypeName}['${typeName}']${extraKeys}>`;

return `export type ${encapsulateTypeName(`${typeName}Resolvers`)} = ${
requireFieldsResolvers ? `Required<${typeSignature}>` : typeSignature
};`;
}

function printPicks(typeName: string, records: Record<string, string[]>): string {
Expand Down
20 changes: 20 additions & 0 deletions packages/presets/graphql-modules/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ export type ModulesConfig = {
*
*/
encapsulateModuleTypes: 'prefix' | 'namespace' | 'none';
/**
* @name requireRootResolvers
* @type boolean
* @default false
* @description Generate resolvers of root types (Query, Mutation and Subscription) as non-optional.
*
* @example
* ```yml
* generates:
* src/:
* preset: modules
* presetConfig:
* baseTypesPath: types.ts
* filename: types.ts
* requireRootResolvers: true
* plugins:
* - typescript-resolvers
* ```
*/
requireRootResolvers?: boolean;
/**
* @name useGraphQLModules
* @type boolean
Expand Down
4 changes: 3 additions & 1 deletion packages/presets/graphql-modules/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { BaseVisitor, getConfigValue } from '@graphql-codegen/visitor-plugin-com

export const preset: Types.OutputPreset<ModulesConfig> = {
buildGeneratesSection: options => {
const useGraphQLModules = getConfigValue(options?.presetConfig.useGraphQLModules, true);
const { baseOutputDir } = options;
const { baseTypesPath, encapsulateModuleTypes } = options.presetConfig;
const useGraphQLModules = getConfigValue(options?.presetConfig.useGraphQLModules, true);
const requireRootResolvers = getConfigValue(options?.presetConfig.requireRootResolvers, false);

const cwd = resolve(options.presetConfig.cwd || process.cwd());
const importTypesNamespace = options.presetConfig.importTypesNamespace || 'Types';
Expand Down Expand Up @@ -102,6 +103,7 @@ export const preset: Types.OutputPreset<ModulesConfig> = {
importNamespace: importTypesNamespace,
importPath,
encapsulate: encapsulateModuleTypes || 'namespace',
requireRootResolvers,
shouldDeclare,
schema,
baseVisitor,
Expand Down
16 changes: 16 additions & 0 deletions packages/presets/graphql-modules/tests/builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ test('should generate interface field resolvers', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand Down Expand Up @@ -108,6 +109,7 @@ test('should not generate graphql-modules code when useGraphQLModules=false', ()
rootTypes: ROOT_TYPES,
baseVisitor,
useGraphQLModules: false,
requireRootResolvers: false,
}
);

Expand All @@ -131,6 +133,7 @@ test('should generate interface extensions field resolvers ', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -148,6 +151,7 @@ test('should include import statement', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -164,6 +168,7 @@ test('should work with naming conventions', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -179,6 +184,7 @@ test('encapsulate: should wrap correctly with namespace', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'namespace',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -194,6 +200,7 @@ test('encapsulate: should wrap correctly with a declared namespace', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'namespace',
requireRootResolvers: false,
shouldDeclare: true,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -208,6 +215,7 @@ test('encapsulate: should wrap correctly with prefix', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'prefix',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -232,6 +240,7 @@ test('should pick fields from defined and extended types', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand Down Expand Up @@ -265,6 +274,7 @@ test('should reexport used types but not defined in module', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -284,6 +294,7 @@ test('should export partial types, only those defined in module or root types',
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand Down Expand Up @@ -315,6 +326,7 @@ test('should export partial types of scalars, only those defined in module or ro
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -336,6 +348,7 @@ test('should use and export resolver signatures of types defined or extended in
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand Down Expand Up @@ -374,6 +387,7 @@ test('should not generate resolver signatures of types that are not defined or e
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -388,6 +402,7 @@ test('should generate an aggregation of individual resolver signatures', () => {
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand All @@ -410,6 +425,7 @@ test('should generate a signature for ResolveMiddleware (with widlcards)', () =>
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
Expand Down
40 changes: 40 additions & 0 deletions packages/presets/graphql-modules/tests/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,44 @@ describe('Integration', () => {
expect(output.length).toBe(5);
expect(output[3].content).toMatchSnapshot();
});

test('should NOT produce required root-level resolvers in Resolvers interface by default', async () => {
const output = await executeCodegen(options);

const usersModuleOutput = output.find(o => o.filename.includes('users'))!;

expect(usersModuleOutput).toBeDefined();
expect(usersModuleOutput.content).toContain(
`export type QueryResolvers = Pick<Types.QueryResolvers, DefinedFields['Query']>;`
);
expect(usersModuleOutput.content).toContain('Query?: QueryResolvers;');
});

test('should produce required root-level resolvers in Resolvers interface when requireRootResolvers flag is enabled', async () => {
const optionsCopy = Object.assign({} as any, options);

optionsCopy.generates['./tests/test-files/modules'].presetConfig = {
...optionsCopy.generates['./tests/test-files/modules'].presetConfig,
requireRootResolvers: true,
useGraphQLModules: false,
};

const output = await executeCodegen(optionsCopy);

const usersModuleOutput = output.find(o => o.filename.includes('users'))!;

expect(usersModuleOutput).toBeDefined();

// Only Query related properties should be required
expect(usersModuleOutput.content).toBeSimilarStringTo(`
export type UserResolvers = Pick<Types.UserResolvers, DefinedFields['User'] | '__isTypeOf'>;
export type QueryResolvers = Required<Pick<Types.QueryResolvers, DefinedFields['Query']>>;
`);
expect(usersModuleOutput.content).toBeSimilarStringTo(`
export interface Resolvers {
User?: UserResolvers;
Query: QueryResolvers;
};
`);
});
});

0 comments on commit 8b6e8e6

Please sign in to comment.