Skip to content

Commit 9c5747a

Browse files
authored
feat: consolidate matching input and output types into single generated class (#43)
* failing test * pass test * refactor * improve test cases * refactor
1 parent a9fce5e commit 9c5747a

File tree

10 files changed

+197
-0
lines changed

10 files changed

+197
-0
lines changed

src/definitions/input.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { buildTypeMetadata } from "../helpers/build-type-metadata";
1717
import { buildAnnotations } from "../helpers/build-annotations";
1818
import { indent } from "@graphql-codegen/visitor-plugin-common";
1919
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";
20+
import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type";
2021

2122
export function buildInputObjectDefinition(
2223
node: InputObjectTypeDefinitionNode,
@@ -27,6 +28,11 @@ export function buildInputObjectDefinition(
2728
return "";
2829
}
2930

31+
const typeWillBeConsolidated = inputTypeHasMatchingOutputType(node, schema);
32+
if (typeWillBeConsolidated) {
33+
return "";
34+
}
35+
3036
const classMembers = (node.fields ?? [])
3137
.map((arg) => {
3238
const typeToUse = buildTypeMetadata(arg.type, schema, config);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Kind, TypeNode } from "graphql/index";
2+
import { GraphQLSchema, InputObjectTypeDefinitionNode } from "graphql";
3+
4+
export function inputTypeHasMatchingOutputType(
5+
inputTypeNode: InputObjectTypeDefinitionNode,
6+
schema: GraphQLSchema,
7+
) {
8+
const typeNameWithoutInput = getTypeNameWithoutInput(
9+
inputTypeNode.name.value,
10+
);
11+
const matchingType = schema.getType(typeNameWithoutInput)?.astNode;
12+
const matchingTypeFields =
13+
matchingType?.kind === Kind.OBJECT_TYPE_DEFINITION
14+
? matchingType.fields
15+
: [];
16+
const inputFields = inputTypeNode.fields;
17+
const fieldsMatch = matchingTypeFields?.every((field) => {
18+
const matchingInputField = inputFields?.find(
19+
(inputField) => inputField.name.value === field.name.value,
20+
);
21+
if (!matchingInputField) return false;
22+
return fieldsAreEquivalent(field.type, matchingInputField.type);
23+
});
24+
return Boolean(matchingTypeFields?.length && fieldsMatch);
25+
}
26+
27+
function getTypeNameWithoutInput(name: string) {
28+
return name.endsWith("Input") ? name.replace("Input", "") : name;
29+
}
30+
31+
function fieldsAreEquivalent(
32+
typeField: TypeNode,
33+
inputField: TypeNode,
34+
): boolean {
35+
switch (typeField.kind) {
36+
case Kind.NAMED_TYPE:
37+
return (
38+
inputField.kind === Kind.NAMED_TYPE &&
39+
typeField.name.value === getTypeNameWithoutInput(inputField.name.value)
40+
);
41+
case Kind.LIST_TYPE:
42+
return (
43+
inputField.kind === Kind.LIST_TYPE &&
44+
fieldsAreEquivalent(typeField.type, inputField.type)
45+
);
46+
case Kind.NON_NULL_TYPE:
47+
return (
48+
inputField.kind === Kind.NON_NULL_TYPE &&
49+
fieldsAreEquivalent(typeField.type, inputField.type)
50+
);
51+
}
52+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.kotlin.generated
2+
3+
import com.expediagroup.graphql.generator.annotations.*
4+
5+
data class MyTypeToConsolidate(
6+
val field: List<String>? = null,
7+
val field2: NestedTypeToConsolidate? = null
8+
)
9+
10+
data class NestedTypeToConsolidate(
11+
val field: String? = null
12+
)
13+
14+
@GraphQLDescription("A description for MyTypeToConsolidate2")
15+
data class MyTypeToConsolidate2(
16+
val field: String? = null
17+
)
18+
19+
data class MyTypeToConsolidate3(
20+
val field: String? = null
21+
)
22+
23+
@GraphQLDescription("It always uses the type description when consolidating")
24+
data class MyTypeToConsolidate4(
25+
val field: String? = null
26+
)
27+
28+
data class MyTypeNotToConsolidate(
29+
val field: String? = null
30+
)
31+
32+
@GraphQLDescription("The type name must exactly match in order to consolidate")
33+
data class MyTypeToNotConsolidateInput(
34+
val field: String? = null
35+
)
36+
37+
data class MyTypeToNotConsolidate2(
38+
val field: String? = null
39+
)
40+
41+
data class MyTypeInputToNotConsolidate2(
42+
val field: String? = null
43+
)
44+
45+
data class MyTypeWhereFieldsDoNotMatch(
46+
val field: String? = null,
47+
val field2: String? = null
48+
)
49+
50+
data class MyTypeWhereFieldsDoNotMatchInput(
51+
val field: String? = null,
52+
val field2: Int? = null
53+
)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# typical case where input and output types are consolidated
2+
3+
type MyTypeToConsolidate {
4+
field: [String!]
5+
field2: NestedTypeToConsolidate
6+
}
7+
8+
input MyTypeToConsolidateInput {
9+
field: [String!]
10+
field2: NestedTypeToConsolidateInput
11+
}
12+
13+
type NestedTypeToConsolidate {
14+
field: String
15+
}
16+
17+
input NestedTypeToConsolidateInput {
18+
field: String
19+
}
20+
21+
# case where type has description but input does not
22+
23+
"A description for MyTypeToConsolidate2"
24+
type MyTypeToConsolidate2 {
25+
field: String
26+
}
27+
28+
input MyTypeToConsolidate2Input {
29+
field: String
30+
}
31+
32+
# case where input has description but type does not
33+
34+
type MyTypeToConsolidate3 {
35+
field: String
36+
}
37+
38+
"It ignores the description on the input when consolidating"
39+
input MyTypeToConsolidate3Input {
40+
field: String
41+
}
42+
43+
# case where both type and input have descriptions
44+
45+
"It always uses the type description when consolidating"
46+
type MyTypeToConsolidate4 {
47+
field: String
48+
}
49+
50+
"A description for MyTypeToConsolidateInput4"
51+
input MyTypeToConsolidate4Input {
52+
field: String
53+
}
54+
55+
# case where type and input names do not match
56+
57+
type MyTypeNotToConsolidate {
58+
field: String
59+
}
60+
61+
"The type name must exactly match in order to consolidate"
62+
input MyTypeToNotConsolidateInput {
63+
field: String
64+
}
65+
66+
# case where type has input in the middle of the name
67+
68+
type MyTypeToNotConsolidate2 {
69+
field: String
70+
}
71+
72+
input MyTypeInputToNotConsolidate2 {
73+
field: String
74+
}
75+
76+
# case where type and input names match but fields do not match
77+
78+
type MyTypeWhereFieldsDoNotMatch {
79+
field: String
80+
field2: String
81+
}
82+
83+
input MyTypeWhereFieldsDoNotMatchInput {
84+
field: String
85+
field2: Int
86+
}

0 commit comments

Comments
 (0)