Skip to content

Commit 716f575

Browse files
authored
Merge pull request #27 from mizdra/support-add-typename
Add `skipTypename` option
2 parents d897760 + 8546b4e commit 716f575

File tree

10 files changed

+135
-80
lines changed

10 files changed

+135
-80
lines changed

e2e/codegen.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ const config: CodegenConfig = {
66
'__generated__/types.ts': {
77
plugins: ['typescript'],
88
config: {
9-
skipTypename: true, // TODO: remove this
109
nonOptionalTypename: true,
1110
enumsAsTypes: true,
1211
avoidOptionals: true,
12+
skipTypename: true,
1313
},
1414
},
1515
'./__generated__/fabbrica.ts': {
1616
plugins: ['@mizdra/graphql-fabbrica'],
1717
config: {
1818
typesFile: './types',
19+
skipTypename: true,
1920
},
2021
},
2122
},

src/code-generator.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { describe, expect, it } from 'vitest';
22
import { generateCode } from './code-generator.js';
3-
import { Options } from './option.js';
3+
import { Config } from './config.js';
44
import { TypeInfo } from './schema-scanner.js';
5+
import { oneOf } from './test/util.js';
56

67
describe('generateCode', () => {
78
it('generates code', () => {
8-
const options: Options = {
9+
const config: Config = {
910
typesFile: './types',
11+
skipTypename: oneOf([true, false]),
1012
};
1113
const typeInfos: TypeInfo[] = [
1214
{ name: 'Book', fieldNames: ['id', 'title', 'author'] },
1315
{ name: 'Author', fieldNames: ['id', 'name', 'books'] },
1416
];
15-
const actual = generateCode(options, typeInfos);
17+
const actual = generateCode(config, typeInfos);
1618
expect(actual).toMatchSnapshot();
1719
});
1820
});

src/code-generator.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Options } from './option.js';
1+
import { Config } from './config.js';
22
import { TypeInfo } from './schema-scanner.js';
33

4-
function generatePreludeCode(options: Options, typeInfos: TypeInfo[]): string {
4+
function generatePreludeCode(config: Config, typeInfos: TypeInfo[]): string {
55
const joinedTypeNames = typeInfos.map(({ name }) => name).join(', ');
66
const code = `
77
import {
@@ -11,7 +11,7 @@ import {
1111
type DefaultFieldsResolver,
1212
defineTypeFactoryInternal,
1313
} from '@mizdra/graphql-fabbrica/helper';
14-
import type { ${joinedTypeNames} } from '${options.typesFile}';
14+
import type { ${joinedTypeNames} } from '${config.typesFile}';
1515
1616
export * from '@mizdra/graphql-fabbrica/helper';
1717
`.trim();
@@ -63,9 +63,9 @@ export function define${name}Factory<
6363
return `${code}\n`;
6464
}
6565

66-
export function generateCode(options: Options, typeInfos: TypeInfo[]): string {
66+
export function generateCode(config: Config, typeInfos: TypeInfo[]): string {
6767
let code = '';
68-
code += generatePreludeCode(options, typeInfos);
68+
code += generatePreludeCode(config, typeInfos);
6969
for (const typeInfo of typeInfos) {
7070
code += generateTypeFactoryCode(typeInfo);
7171
}

src/config.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { validateConfig } from './config.js';
3+
import { oneOf } from './test/util.js';
4+
5+
describe('validateConfig', () => {
6+
it('options must be an object', () => {
7+
expect(() => validateConfig(1)).toThrow('`options` must be an object');
8+
expect(() => validateConfig(null)).toThrow('`options` must be an object');
9+
});
10+
it('typesFile', () => {
11+
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();
12+
expect(() => validateConfig({})).toThrow('`option.typesFile` is required');
13+
expect(() => validateConfig({ typesFile: 1 })).toThrow('`options.typesFile` must be a string');
14+
});
15+
it('skipTypename', () => {
16+
expect(() => validateConfig({ typesFile: './types', skipTypename: oneOf([true, false]) })).not.toThrow();
17+
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();
18+
expect(() => validateConfig({ typesFile: './types', skipTypename: 1 })).toThrow(
19+
'`options.skipTypename` must be a boolean',
20+
);
21+
});
22+
});

src/config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export type RawConfig = {
2+
typesFile: string;
3+
skipTypename?: boolean;
4+
// TODO: support addIsAbstractType
5+
};
6+
7+
export type Config = {
8+
typesFile: string;
9+
skipTypename: boolean;
10+
// TODO: support addIsAbstractType
11+
};
12+
13+
export function validateConfig(rawConfig: unknown): asserts rawConfig is RawConfig {
14+
if (typeof rawConfig !== 'object' || rawConfig === null) {
15+
throw new Error('`options` must be an object');
16+
}
17+
if (!('typesFile' in rawConfig)) {
18+
throw new Error('`option.typesFile` is required');
19+
}
20+
if (typeof rawConfig['typesFile'] !== 'string') {
21+
throw new Error('`options.typesFile` must be a string');
22+
}
23+
if ('skipTypename' in rawConfig && typeof rawConfig['skipTypename'] !== 'boolean') {
24+
throw new Error('`options.skipTypename` must be a boolean');
25+
}
26+
}
27+
28+
export function normalizeConfig(rawConfig: RawConfig): Config {
29+
return {
30+
typesFile: rawConfig.typesFile,
31+
skipTypename: rawConfig.skipTypename ?? false,
32+
};
33+
}

src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
import type { CodegenPlugin } from '@graphql-codegen/plugin-helpers';
44
import { generateCode } from './code-generator.js';
5-
import { validateOptions } from './option.js';
5+
import { normalizeConfig, validateConfig } from './config.js';
66
import { getTypeInfos } from './schema-scanner.js';
77

88
const plugin: CodegenPlugin = {
99
plugin(schema, _documents, config, _info) {
10-
validateOptions(config);
11-
const typeInfos = getTypeInfos(schema);
12-
const code = generateCode(config, typeInfos);
10+
validateConfig(config);
11+
const normalizedConfig = normalizeConfig(config);
12+
const typeInfos = getTypeInfos(normalizedConfig, schema);
13+
const code = generateCode(normalizedConfig, typeInfos);
1314
return code;
1415
},
1516
};

src/option.test.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/option.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/schema-scanner.test.ts

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,56 @@
11
import { parse, buildASTSchema } from 'graphql';
2-
import { expect, it } from 'vitest';
2+
import { describe, expect, it } from 'vitest';
3+
import { Config } from './config.js';
34
import { getTypeInfos } from './schema-scanner.js';
45

5-
it('getTypeInfos', () => {
6-
const ast = parse(`
7-
interface Node {
8-
id: ID!
9-
}
10-
type Book implements Node {
11-
id: ID!
12-
title: String!
13-
author: Author!
14-
}
15-
type Author implements Node {
16-
id: ID!
17-
name: String!
18-
books: [Book!]!
19-
}
20-
type Query {
21-
node(id: ID!): Node
22-
}
23-
type Subscription {
24-
bookAdded: Book!
25-
}
26-
type Mutation {
27-
addBook(title: String!, authorId: ID!): Book!
28-
}
29-
`);
30-
const schema = buildASTSchema(ast);
31-
expect(getTypeInfos(schema)).toStrictEqual([
32-
{ name: 'Book', fieldNames: ['id', 'title', 'author'] },
33-
{ name: 'Author', fieldNames: ['id', 'name', 'books'] },
34-
{ name: 'Query', fieldNames: ['node'] },
35-
{ name: 'Subscription', fieldNames: ['bookAdded'] },
36-
{ name: 'Mutation', fieldNames: ['addBook'] },
37-
]);
6+
function buildSchemaFromString(schemaString: string) {
7+
const ast = parse(schemaString);
8+
return buildASTSchema(ast);
9+
}
10+
11+
describe('getTypeInfos', () => {
12+
it('returns typename and field names', () => {
13+
const schema = buildSchemaFromString(`
14+
interface Node {
15+
id: ID!
16+
}
17+
type Book implements Node {
18+
id: ID!
19+
title: String!
20+
author: Author!
21+
}
22+
type Author implements Node {
23+
id: ID!
24+
name: String!
25+
books: [Book!]!
26+
}
27+
type Query {
28+
node(id: ID!): Node
29+
}
30+
type Subscription {
31+
bookAdded: Book!
32+
}
33+
type Mutation {
34+
addBook(title: String!, authorId: ID!): Book!
35+
}
36+
`);
37+
const config: Config = { typesFile: './types', skipTypename: true };
38+
expect(getTypeInfos(config, schema)).toStrictEqual([
39+
{ name: 'Book', fieldNames: ['id', 'title', 'author'] },
40+
{ name: 'Author', fieldNames: ['id', 'name', 'books'] },
41+
{ name: 'Query', fieldNames: ['node'] },
42+
{ name: 'Subscription', fieldNames: ['bookAdded'] },
43+
{ name: 'Mutation', fieldNames: ['addBook'] },
44+
]);
45+
});
46+
it('includes __typename if skipTypename is false', () => {
47+
const schema = buildSchemaFromString(`
48+
type Book {
49+
id: ID!
50+
title: String!
51+
}
52+
`);
53+
const config: Config = { typesFile: './types', skipTypename: false };
54+
expect(getTypeInfos(config, schema)).toStrictEqual([{ name: 'Book', fieldNames: ['__typename', 'id', 'title'] }]);
55+
});
3856
});

src/schema-scanner.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { GraphQLObjectType, GraphQLSchema } from 'graphql';
2+
import { Config } from './config.js';
23

34
export type TypeInfo = { name: string; fieldNames: string[] };
45

5-
export function getTypeInfos(schema: GraphQLSchema): TypeInfo[] {
6+
function getAdditionalFieldNames(config: Config): string[] {
7+
// TODO: support __is<AbstractType> (__is<InterfaceType>, __is<UnionType>)
8+
const result = [];
9+
if (!config.skipTypename) result.push('__typename');
10+
return result;
11+
}
12+
13+
export function getTypeInfos(config: Config, schema: GraphQLSchema): TypeInfo[] {
614
const result: TypeInfo[] = [];
715
const types = Object.values(schema.getTypeMap());
816
for (const type of types) {
@@ -13,10 +21,10 @@ export function getTypeInfos(schema: GraphQLSchema): TypeInfo[] {
1321
// ref: https://github.com/graphql/graphql-js/blob/b12dcffe83098922dcc6c0ec94eb6fc032bd9772/src/type/introspection.ts#L552-L559
1422
if (type.name.startsWith('__')) continue;
1523

16-
// TODO: support __typename, __is<AbstractType> (__is<Interface>, __is<Union>)
1724
const fieldMap = type.getFields();
1825
const fieldNames = Object.values(fieldMap).map((field) => field.name);
19-
result.push({ name: type.name, fieldNames });
26+
const additionalFieldNames = getAdditionalFieldNames(config);
27+
result.push({ name: type.name, fieldNames: [...additionalFieldNames, ...fieldNames] });
2028
}
2129
return result;
2230
}

0 commit comments

Comments
 (0)