Skip to content

Commit a207892

Browse files
authored
Merge pull request #42 from mizdra/refactoring
Refactoring
2 parents 8b644ce + e16b175 commit a207892

File tree

3 files changed

+272
-173
lines changed

3 files changed

+272
-173
lines changed

src/schema-scanner.test.ts

Lines changed: 176 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,16 @@ import { fakeConfig } from './test/util.js';
99
describe('getTypeInfos', () => {
1010
it('returns typename and field names', () => {
1111
const schema = buildSchema(`
12-
interface Node {
13-
id: ID!
14-
}
15-
type Book implements Node {
12+
type Book {
1613
id: ID!
1714
title: String!
1815
author: Author!
1916
}
20-
type Author implements Node {
17+
type Author {
2118
id: ID!
2219
name: String!
2320
books: [Book!]!
2421
}
25-
type Query {
26-
node(id: ID!): Node
27-
}
28-
type Subscription {
29-
bookAdded: Book!
30-
}
31-
type Mutation {
32-
addBook(title: String!, authorId: ID!): Book!
33-
}
3422
`);
3523
const config: Config = fakeConfig();
3624
expect(getTypeInfos(config, schema)).toMatchInlineSnapshot(`
@@ -55,7 +43,7 @@ describe('getTypeInfos', () => {
5543
{
5644
"comment": undefined,
5745
"name": "author",
58-
"typeString": "Book['author'] | undefined",
46+
"typeString": "OptionalAuthor | undefined",
5947
},
6048
],
6149
"name": "Book",
@@ -80,59 +68,186 @@ describe('getTypeInfos', () => {
8068
{
8169
"comment": undefined,
8270
"name": "books",
83-
"typeString": "Author['books'] | undefined",
71+
"typeString": "OptionalBook[] | undefined",
8472
},
8573
],
8674
"name": "Author",
8775
},
88-
{
89-
"comment": undefined,
90-
"fields": [
91-
{
92-
"name": "__typename",
93-
"typeString": "'Query'",
94-
},
95-
{
96-
"comment": undefined,
97-
"name": "node",
98-
"typeString": "Query['node'] | undefined",
99-
},
100-
],
101-
"name": "Query",
102-
},
103-
{
104-
"comment": undefined,
105-
"fields": [
106-
{
107-
"name": "__typename",
108-
"typeString": "'Subscription'",
109-
},
110-
{
111-
"comment": undefined,
112-
"name": "bookAdded",
113-
"typeString": "Subscription['bookAdded'] | undefined",
114-
},
115-
],
116-
"name": "Subscription",
117-
},
118-
{
119-
"comment": undefined,
120-
"fields": [
121-
{
122-
"name": "__typename",
123-
"typeString": "'Mutation'",
124-
},
125-
{
126-
"comment": undefined,
127-
"name": "addBook",
128-
"typeString": "Mutation['addBook'] | undefined",
129-
},
130-
],
131-
"name": "Mutation",
132-
},
13376
]
13477
`);
13578
});
79+
it('argument', () => {
80+
const schema = buildSchema(`
81+
type Argument {
82+
field(arg: String!): String!
83+
}
84+
`);
85+
const config: Config = fakeConfig();
86+
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
87+
{
88+
"comment": undefined,
89+
"fields": [
90+
{
91+
"name": "__typename",
92+
"typeString": "'Argument'",
93+
},
94+
{
95+
"comment": undefined,
96+
"name": "field",
97+
"typeString": "Argument['field'] | undefined",
98+
},
99+
],
100+
"name": "Argument",
101+
}
102+
`);
103+
});
104+
it('nullable', () => {
105+
const schema = buildSchema(`
106+
type Type {
107+
field1: String
108+
field2: [String]
109+
field3: SubType
110+
field4: [SubType]
111+
}
112+
type SubType {
113+
field: String!
114+
}
115+
`);
116+
const config: Config = fakeConfig();
117+
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
118+
{
119+
"comment": undefined,
120+
"fields": [
121+
{
122+
"name": "__typename",
123+
"typeString": "'Type'",
124+
},
125+
{
126+
"comment": undefined,
127+
"name": "field1",
128+
"typeString": "Type['field1'] | undefined",
129+
},
130+
{
131+
"comment": undefined,
132+
"name": "field2",
133+
"typeString": "Type['field2'] | undefined",
134+
},
135+
{
136+
"comment": undefined,
137+
"name": "field3",
138+
"typeString": "Maybe<OptionalSubType> | undefined",
139+
},
140+
{
141+
"comment": undefined,
142+
"name": "field4",
143+
"typeString": "Maybe<Maybe<OptionalSubType>[]> | undefined",
144+
},
145+
],
146+
"name": "Type",
147+
}
148+
`);
149+
});
150+
it('interface', () => {
151+
const schema = buildSchema(`
152+
interface Interface1 {
153+
fieldA: String!
154+
}
155+
interface Interface2 {
156+
fieldB: String!
157+
}
158+
type ImplementingType implements Interface1 & Interface2 {
159+
fieldA: String!
160+
fieldB: String!
161+
}
162+
`);
163+
const config: Config = fakeConfig();
164+
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
165+
{
166+
"comment": undefined,
167+
"fields": [
168+
{
169+
"name": "__typename",
170+
"typeString": "'ImplementingType'",
171+
},
172+
{
173+
"comment": undefined,
174+
"name": "fieldA",
175+
"typeString": "ImplementingType['fieldA'] | undefined",
176+
},
177+
{
178+
"comment": undefined,
179+
"name": "fieldB",
180+
"typeString": "ImplementingType['fieldB'] | undefined",
181+
},
182+
],
183+
"name": "ImplementingType",
184+
}
185+
`);
186+
});
187+
it('union', () => {
188+
const schema = buildSchema(`
189+
union Union1 = Member1 | Member2
190+
union Union2 = Member1 | Member2
191+
type Member1 {
192+
field1: String!
193+
}
194+
type Member2 {
195+
field2: String!
196+
}
197+
`);
198+
const config: Config = fakeConfig();
199+
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
200+
{
201+
"comment": undefined,
202+
"fields": [
203+
{
204+
"name": "__typename",
205+
"typeString": "'Member1'",
206+
},
207+
{
208+
"comment": undefined,
209+
"name": "field1",
210+
"typeString": "Member1['field1'] | undefined",
211+
},
212+
],
213+
"name": "Member1",
214+
}
215+
`);
216+
});
217+
it('input', () => {
218+
const schema = buildSchema(`
219+
input Input {
220+
field1: String!
221+
field2: SubType!
222+
}
223+
type SubType {
224+
field: String!
225+
}
226+
`);
227+
const config: Config = fakeConfig();
228+
expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(`
229+
{
230+
"comment": undefined,
231+
"fields": [
232+
{
233+
"name": "__typename",
234+
"typeString": "'Input'",
235+
},
236+
{
237+
"comment": undefined,
238+
"name": "field1",
239+
"typeString": "Input['field1'] | undefined",
240+
},
241+
{
242+
"comment": undefined,
243+
"name": "field2",
244+
"typeString": "OptionalSubType | undefined",
245+
},
246+
],
247+
"name": "Input",
248+
}
249+
`);
250+
});
136251
it('includes __typename if skipTypename is false', () => {
137252
const schema = buildSchema(`
138253
type Book {

src/schema-scanner.ts

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,106 @@
1-
import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers';
2-
import { GraphQLObjectType, GraphQLSchema, visit } from 'graphql';
1+
import { transformComment } from '@graphql-codegen/visitor-plugin-common';
2+
import {
3+
GraphQLSchema,
4+
ASTNode,
5+
FieldDefinitionNode,
6+
Kind,
7+
ObjectTypeDefinitionNode,
8+
TypeNode,
9+
InputObjectTypeDefinitionNode,
10+
InputValueDefinitionNode,
11+
} from 'graphql';
312
import { Config } from './config.js';
4-
import { createTypeInfoVisitor } from './visitor.js';
13+
14+
// The fork of https://github.com/dotansimha/graphql-code-generator/blob/e1dc75f3c598bf7f83138ca533619716fc73f823/packages/plugins/typescript/resolvers/src/visitor.ts#L85-L91
15+
function clearOptional(str: string): string {
16+
if (str.startsWith('Maybe')) {
17+
return str.replace(/Maybe<(.*?)>$/u, '$1');
18+
}
19+
return str;
20+
}
21+
22+
// The fork of https://github.com/dotansimha/graphql-code-generator/blob/ba84a3a2758d94dac27fcfbb1bafdf3ed7c32929/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts#L422
23+
function convertName(node: ASTNode | string, config: Config): string {
24+
let convertedName = '';
25+
convertedName += config.typesPrefix;
26+
convertedName += config.convert(node);
27+
convertedName += config.typesSuffix;
28+
return convertedName;
29+
}
30+
31+
function isTypeBasedOnUserDefinedType(node: TypeNode, userDefinedTypeNames: string[]): boolean {
32+
if (node.kind === Kind.NON_NULL_TYPE) {
33+
return isTypeBasedOnUserDefinedType(node.type, userDefinedTypeNames);
34+
} else if (node.kind === Kind.LIST_TYPE) {
35+
return isTypeBasedOnUserDefinedType(node.type, userDefinedTypeNames);
36+
} else {
37+
return userDefinedTypeNames.includes(node.name.value);
38+
}
39+
}
40+
41+
function parseTypeNode(node: TypeNode, config: Config): string {
42+
if (node.kind === Kind.NON_NULL_TYPE) {
43+
return clearOptional(parseTypeNode(node.type, config));
44+
} else if (node.kind === Kind.LIST_TYPE) {
45+
return `Maybe<${parseTypeNode(node.type, config)}[]>`;
46+
} else {
47+
return `Maybe<Optional${convertName(node.name.value, config)}>`;
48+
}
49+
}
50+
51+
function parseFieldOrInputValueDefinition(
52+
node: FieldDefinitionNode | InputValueDefinitionNode,
53+
objectTypeName: string,
54+
config: Config,
55+
userDefinedTypeNames: string[],
56+
): { typeString: string; comment: string | undefined } {
57+
const comment = node.description ? transformComment(node.description) : undefined;
58+
if (isTypeBasedOnUserDefinedType(node.type, userDefinedTypeNames)) {
59+
return { typeString: `${parseTypeNode(node.type, config)} | undefined`, comment };
60+
} else {
61+
return { typeString: `${objectTypeName}['${node.name.value}'] | undefined`, comment };
62+
}
63+
}
64+
65+
function parseObjectTypeOrInputObjectTypeDefinition(
66+
node: ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode,
67+
config: Config,
68+
userDefinedTypeNames: string[],
69+
): TypeInfo {
70+
const objectTypeName = convertName(node.name.value, config);
71+
const comment = node.description ? transformComment(node.description) : undefined;
72+
return {
73+
name: objectTypeName,
74+
fields: [
75+
// TODO: support __is<AbstractType> (__is<InterfaceType>, __is<UnionType>)
76+
...(!config.skipTypename ? [{ name: '__typename', typeString: `'${objectTypeName}'` }] : []),
77+
...(node.fields ?? []).map((field) => ({
78+
name: field.name.value,
79+
...parseFieldOrInputValueDefinition(field, objectTypeName, config, userDefinedTypeNames),
80+
})),
81+
],
82+
comment,
83+
};
84+
}
585

686
type FieldInfo = { name: string; typeString: string; comment?: string | undefined };
787
export type TypeInfo = { name: string; fields: FieldInfo[]; comment?: string | undefined };
888

989
export function getTypeInfos(config: Config, schema: GraphQLSchema): TypeInfo[] {
10-
const userDefinedTypeNames = Object.values(schema.getTypeMap())
11-
// Ignore introspectionTypes
12-
// ref: https://github.com/graphql/graphql-js/blob/b12dcffe83098922dcc6c0ec94eb6fc032bd9772/src/type/introspection.ts#L552-L559
13-
.filter((type) => type instanceof GraphQLObjectType && !type.name.startsWith('__'))
14-
.map((type) => type.name);
90+
const types = Object.values(schema.getTypeMap());
91+
92+
const objectTypeOrInputObjectTypeDefinitions = types
93+
.map((type) => type.astNode)
94+
.filter((node): node is ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode => {
95+
if (!node) return false;
96+
return node.kind === Kind.OBJECT_TYPE_DEFINITION || node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION;
97+
});
1598

16-
const visitor = createTypeInfoVisitor(config, userDefinedTypeNames);
17-
const ast = getCachedDocumentNodeFromSchema(schema);
99+
const userDefinedTypeNames = objectTypeOrInputObjectTypeDefinitions.map((type) => type.name.value);
18100

19-
visit(ast, visitor);
101+
const typeInfos = objectTypeOrInputObjectTypeDefinitions.map((node) =>
102+
parseObjectTypeOrInputObjectTypeDefinition(node, config, userDefinedTypeNames),
103+
);
20104

21-
return visitor.getTypeInfos();
105+
return typeInfos;
22106
}

0 commit comments

Comments
 (0)