Skip to content

Commit 9fa0d54

Browse files
authored
Merge pull request #43 from mizdra/support-is-abstract-type
Support `__is<AbstractYype>`
2 parents a207892 + bd726d1 commit 9fa0d54

File tree

6 files changed

+198
-10
lines changed

6 files changed

+198
-10
lines changed

README.md

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,25 +371,128 @@ Defines the file path containing all GraphQL types. This file can be generated w
371371

372372
type: `boolean`, default: `false`
373373

374-
Does not add `__typename` to the generated mock data. The value of this option must be the same as the option of the same name in [typescript plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript).
374+
Does not add `__typename` to the fields that can be passed to factory.
375+
376+
```ts
377+
import { CodegenConfig } from '@graphql-codegen/cli';
378+
const config: CodegenConfig = {
379+
schema: './schema.graphql',
380+
generates: {
381+
'__generated__/types.ts': {
382+
plugins: ['typescript'],
383+
config: {
384+
// ...
385+
},
386+
},
387+
'./__generated__/fabbrica.ts': {
388+
plugins: ['@mizdra/graphql-fabbrica'],
389+
config: {
390+
// ...
391+
skipTypename: true,
392+
},
393+
},
394+
},
395+
};
396+
module.exports = config;
397+
```
398+
399+
### `skipIsAbstractType`
400+
401+
type: `boolean`, default: `true`
402+
403+
Does not add `__is<AbstractType>` to the fields that can be passed to factory. `__is<AbstractType>` is a field that relay-compiler automatically adds to the query[^1][^2]. It is recommended for Relay users to set this option to `false`.
404+
405+
[^1]: https://github.com/facebook/relay/issues/3129#issuecomment-659439154
406+
[^2]: https://github.com/search?q=repo%3Afacebook%2Frelay%20%2F__is%3CAbstractType%3E%2F&type=code
407+
408+
```ts
409+
import { CodegenConfig } from '@graphql-codegen/cli';
410+
const config: CodegenConfig = {
411+
schema: './schema.graphql',
412+
generates: {
413+
'__generated__/types.ts': {
414+
plugins: ['typescript'],
415+
config: {
416+
// ...
417+
},
418+
},
419+
'./__generated__/fabbrica.ts': {
420+
plugins: ['@mizdra/graphql-fabbrica'],
421+
config: {
422+
// ...
423+
skipIsAbstractType: false,
424+
},
425+
},
426+
},
427+
};
428+
module.exports = config;
429+
```
375430

376431
### `namingConvention`
377432

378433
type: `NamingConvention`, default: `change-case-all#pascalCase`
379434

380-
Allow you to override the naming convention of the output. The value of this option must be the same as the option of the same name in [graphql code generator](https://the-guild.dev/graphql/codegen/docs/config-reference/naming-convention#namingconvention).
435+
Allow you to override the naming convention of the output.
436+
437+
This option is compatible with [the one for typescript plugin](https://the-guild.dev/graphql/codegen/docs/config-reference/naming-convention#namingconvention). If you specify it for the typescript plugin, you must set the same value for graphql-fabbrica.
438+
439+
```ts
440+
import { CodegenConfig } from '@graphql-codegen/cli';
441+
const config: CodegenConfig = {
442+
schema: './schema.graphql',
443+
config: {
444+
namingConvention: 'change-case-all#lowerCase',
445+
},
446+
generates: {
447+
'__generated__/types.ts': {
448+
plugins: ['typescript'],
449+
// ...
450+
},
451+
'./__generated__/fabbrica.ts': {
452+
plugins: ['@mizdra/graphql-fabbrica'],
453+
// ...
454+
},
455+
},
456+
};
457+
module.exports = config;
458+
```
381459

382460
### `typesPrefix`
383461

384462
type: `string`, default: `''`
385463

386-
Prefixes all the generated types. This value must be the same as the option of the same name in [typescript plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#typesprefix).
464+
Prefixes all the generated types.
465+
466+
This option is compatible with [the one for typescript plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#typesprefix). If you specify it for the typescript plugin, you must set the same value for graphql-fabbrica.
467+
468+
```ts
469+
import { CodegenConfig } from '@graphql-codegen/cli';
470+
const config: CodegenConfig = {
471+
schema: './schema.graphql',
472+
config: {
473+
typesPrefix: 'I',
474+
},
475+
generates: {
476+
'__generated__/types.ts': {
477+
plugins: ['typescript'],
478+
// ...
479+
},
480+
'./__generated__/fabbrica.ts': {
481+
plugins: ['@mizdra/graphql-fabbrica'],
482+
// ...
483+
},
484+
},
485+
};
486+
module.exports = config;
487+
```
387488

388489
### `typesSuffix`
389490

390491
type: `string`, default: `''`
391492

392-
Suffixes all the generated types. This value must be the same as the option of the same name in [typescript plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#typessuffix).
493+
Suffixes all the generated types.
494+
495+
This option is compatible with [the one for typescript plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#typessuffix). If you specify it for the typescript plugin, you must set the same value for graphql-fabbrica.
393496

394497
## Troubleshooting
395498

src/config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ describe('validateConfig', () => {
1919
'`options.skipTypename` must be a boolean',
2020
);
2121
});
22+
it('skipIsAbstractType', () => {
23+
expect(() => validateConfig({ typesFile: './types', skipIsAbstractType: oneOf([true, false]) })).not.toThrow();
24+
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();
25+
expect(() => validateConfig({ typesFile: './types', skipIsAbstractType: 1 })).toThrow(
26+
'`options.skipIsAbstractType` must be a boolean',
27+
);
28+
});
2229
it('typesPrefix', () => {
2330
expect(() => validateConfig({ typesFile: './types', typesPrefix: 'Prefix' })).not.toThrow();
2431
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ConvertFn, RawTypesConfig, convertFactory } from '@graphql-codegen/visi
33
export type RawConfig = {
44
typesFile: string;
55
skipTypename?: RawTypesConfig['skipTypename'];
6+
skipIsAbstractType?: boolean | undefined;
67
namingConvention?: RawTypesConfig['namingConvention'];
78
typesPrefix?: RawTypesConfig['typesPrefix'];
89
typesSuffix?: RawTypesConfig['typesSuffix'];
@@ -12,6 +13,7 @@ export type RawConfig = {
1213
export type Config = {
1314
typesFile: string;
1415
skipTypename: Exclude<RawTypesConfig['skipTypename'], undefined>;
16+
skipIsAbstractType: boolean;
1517
typesPrefix: Exclude<RawTypesConfig['typesPrefix'], undefined>;
1618
typesSuffix: Exclude<RawTypesConfig['typesSuffix'], undefined>;
1719
convert: ConvertFn;
@@ -31,6 +33,9 @@ export function validateConfig(rawConfig: unknown): asserts rawConfig is RawConf
3133
if ('skipTypename' in rawConfig && typeof rawConfig['skipTypename'] !== 'boolean') {
3234
throw new Error('`options.skipTypename` must be a boolean');
3335
}
36+
if ('skipIsAbstractType' in rawConfig && typeof rawConfig['skipIsAbstractType'] !== 'boolean') {
37+
throw new Error('`options.skipIsAbstractType` must be a boolean');
38+
}
3439
if ('typesPrefix' in rawConfig && typeof rawConfig['typesPrefix'] !== 'string') {
3540
throw new Error('`options.typesPrefix` must be a string');
3641
}
@@ -43,6 +48,7 @@ export function normalizeConfig(rawConfig: RawConfig): Config {
4348
return {
4449
typesFile: rawConfig.typesFile,
4550
skipTypename: rawConfig.skipTypename ?? false,
51+
skipIsAbstractType: rawConfig.skipIsAbstractType ?? true,
4652
typesPrefix: rawConfig.typesPrefix ?? '',
4753
typesSuffix: rawConfig.typesSuffix ?? '',
4854
// eslint-disable-next-line @typescript-eslint/no-explicit-any

src/schema-scanner.test.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ describe('getTypeInfos', () => {
160160
fieldB: String!
161161
}
162162
`);
163-
const config: Config = fakeConfig();
164-
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
163+
expect(getTypeInfos(fakeConfig({ skipIsAbstractType: true }), schema)[0]).toMatchInlineSnapshot(`
165164
{
166165
"comment": undefined,
167166
"fields": [
@@ -183,6 +182,36 @@ describe('getTypeInfos', () => {
183182
"name": "ImplementingType",
184183
}
185184
`);
185+
expect(getTypeInfos(fakeConfig({ skipIsAbstractType: false }), schema)[0]).toMatchInlineSnapshot(`
186+
{
187+
"comment": undefined,
188+
"fields": [
189+
{
190+
"name": "__typename",
191+
"typeString": "'ImplementingType'",
192+
},
193+
{
194+
"name": "__isInterface1",
195+
"typeString": "'ImplementingType'",
196+
},
197+
{
198+
"name": "__isInterface2",
199+
"typeString": "'ImplementingType'",
200+
},
201+
{
202+
"comment": undefined,
203+
"name": "fieldA",
204+
"typeString": "ImplementingType['fieldA'] | undefined",
205+
},
206+
{
207+
"comment": undefined,
208+
"name": "fieldB",
209+
"typeString": "ImplementingType['fieldB'] | undefined",
210+
},
211+
],
212+
"name": "ImplementingType",
213+
}
214+
`);
186215
});
187216
it('union', () => {
188217
const schema = buildSchema(`
@@ -195,15 +224,39 @@ describe('getTypeInfos', () => {
195224
field2: String!
196225
}
197226
`);
198-
const config: Config = fakeConfig();
199-
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
227+
expect(getTypeInfos(fakeConfig({ skipIsAbstractType: true }), schema)[0]).toMatchInlineSnapshot(`
228+
{
229+
"comment": undefined,
230+
"fields": [
231+
{
232+
"name": "__typename",
233+
"typeString": "'Member1'",
234+
},
235+
{
236+
"comment": undefined,
237+
"name": "field1",
238+
"typeString": "Member1['field1'] | undefined",
239+
},
240+
],
241+
"name": "Member1",
242+
}
243+
`);
244+
expect(getTypeInfos(fakeConfig({ skipIsAbstractType: false }), schema)[0]).toMatchInlineSnapshot(`
200245
{
201246
"comment": undefined,
202247
"fields": [
203248
{
204249
"name": "__typename",
205250
"typeString": "'Member1'",
206251
},
252+
{
253+
"name": "__isUnion1",
254+
"typeString": "'Member1'",
255+
},
256+
{
257+
"name": "__isUnion2",
258+
"typeString": "'Member1'",
259+
},
207260
{
208261
"comment": undefined,
209262
"name": "field1",

src/schema-scanner.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TypeNode,
99
InputObjectTypeDefinitionNode,
1010
InputValueDefinitionNode,
11+
UnionTypeDefinitionNode,
1112
} from 'graphql';
1213
import { Config } from './config.js';
1314

@@ -66,14 +67,18 @@ function parseObjectTypeOrInputObjectTypeDefinition(
6667
node: ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode,
6768
config: Config,
6869
userDefinedTypeNames: string[],
70+
getAbstractTypeNames: (type: ObjectTypeDefinitionNode) => string[],
6971
): TypeInfo {
7072
const objectTypeName = convertName(node.name.value, config);
7173
const comment = node.description ? transformComment(node.description) : undefined;
74+
const abstractTypeNames = node.kind === Kind.OBJECT_TYPE_DEFINITION ? getAbstractTypeNames(node) : [];
7275
return {
7376
name: objectTypeName,
7477
fields: [
75-
// TODO: support __is<AbstractType> (__is<InterfaceType>, __is<UnionType>)
7678
...(!config.skipTypename ? [{ name: '__typename', typeString: `'${objectTypeName}'` }] : []),
79+
...(!config.skipIsAbstractType
80+
? abstractTypeNames.map((name) => ({ name: `__is${name}`, typeString: `'${objectTypeName}'` }))
81+
: []),
7782
...(node.fields ?? []).map((field) => ({
7883
name: field.name.value,
7984
...parseFieldOrInputValueDefinition(field, objectTypeName, config, userDefinedTypeNames),
@@ -95,11 +100,24 @@ export function getTypeInfos(config: Config, schema: GraphQLSchema): TypeInfo[]
95100
if (!node) return false;
96101
return node.kind === Kind.OBJECT_TYPE_DEFINITION || node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION;
97102
});
103+
const unionTypeDefinitions = types
104+
.map((type) => type.astNode)
105+
.filter((node): node is UnionTypeDefinitionNode => {
106+
if (!node) return false;
107+
return node.kind === Kind.UNION_TYPE_DEFINITION;
108+
});
109+
function getAbstractTypeNames(type: ObjectTypeDefinitionNode): string[] {
110+
const interfaceNames = (type.interfaces ?? []).map((i) => i.name.value);
111+
const unionNames = unionTypeDefinitions
112+
.filter((union) => (union.types ?? []).some((member) => member.name.value === type.name.value))
113+
.map((union) => union.name.value);
114+
return [...interfaceNames, ...unionNames];
115+
}
98116

99117
const userDefinedTypeNames = objectTypeOrInputObjectTypeDefinitions.map((type) => type.name.value);
100118

101119
const typeInfos = objectTypeOrInputObjectTypeDefinitions.map((node) =>
102-
parseObjectTypeOrInputObjectTypeDefinition(node, config, userDefinedTypeNames),
120+
parseObjectTypeOrInputObjectTypeDefinition(node, config, userDefinedTypeNames, getAbstractTypeNames),
103121
);
104122

105123
return typeInfos;

src/test/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function fakeConfig(args: Partial<Config> = {}): Config {
1111
return {
1212
typesFile: './types',
1313
skipTypename: false,
14+
skipIsAbstractType: true,
1415
typesPrefix: '',
1516
typesSuffix: '',
1617
convert: convertFactory({}),

0 commit comments

Comments
 (0)