Skip to content

Commit 55f7101

Browse files
authored
fix: class consolidation bugs (#48)
* fix one case and write unit test for another * pass tests * refactor * refactor * refactor * refactor
1 parent 4bd0da4 commit 55f7101

File tree

10 files changed

+151
-64
lines changed

10 files changed

+151
-64
lines changed

src/definitions/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function buildInputObjectDefinition(
2828
return "";
2929
}
3030

31-
const typeWillBeConsolidated = inputTypeHasMatchingOutputType(schema, node);
31+
const typeWillBeConsolidated = inputTypeHasMatchingOutputType(node, schema);
3232
if (typeWillBeConsolidated) {
3333
return "";
3434
}

src/definitions/object.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ See the License for the specific language governing permissions and
1111
limitations under the License.
1212
*/
1313

14-
import { GraphQLSchema, Kind, ObjectTypeDefinitionNode } from "graphql";
14+
import {
15+
GraphQLSchema,
16+
isInputObjectType,
17+
isInterfaceType,
18+
ObjectTypeDefinitionNode,
19+
} from "graphql";
1520
import { buildAnnotations } from "../helpers/build-annotations";
1621
import { indent } from "@graphql-codegen/visitor-plugin-common";
1722
import { buildTypeMetadata } from "../helpers/build-type-metadata";
@@ -58,11 +63,11 @@ ${getDataClassMembers({ node, schema, config, completableFuture: true })}
5863
}`;
5964
}
6065

61-
const potentialMatchingInputType = schema.getType(`${name}Input`)?.astNode;
62-
const typeWillBeConsolidated = inputTypeHasMatchingOutputType(
63-
schema,
64-
potentialMatchingInputType,
65-
);
66+
const potentialMatchingInputType = schema.getType(`${name}Input`);
67+
const typeWillBeConsolidated =
68+
isInputObjectType(potentialMatchingInputType) &&
69+
potentialMatchingInputType.astNode &&
70+
inputTypeHasMatchingOutputType(potentialMatchingInputType.astNode, schema);
6671
const outputRestrictionAnnotation = typeWillBeConsolidated
6772
? ""
6873
: "@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])\n";
@@ -89,11 +94,13 @@ function getDataClassMembers({
8994
const typeMetadata = buildTypeMetadata(fieldNode.type, schema, config);
9095
const shouldOverrideField =
9196
!completableFuture &&
92-
node.interfaces?.some((i) => {
93-
const typeNode = schema.getType(i.name.value)?.astNode;
97+
node.interfaces?.some((interfaceNode) => {
98+
const typeNode = schema.getType(interfaceNode.name.value);
9499
return (
95-
typeNode?.kind === Kind.INTERFACE_TYPE_DEFINITION &&
96-
typeNode.fields?.some((f) => f.name.value === fieldNode.name.value)
100+
isInterfaceType(typeNode) &&
101+
typeNode.astNode?.fields?.some(
102+
(field) => field.name.value === fieldNode.name.value,
103+
)
97104
);
98105
});
99106
const fieldDefinition = buildFieldDefinition(

src/helpers/build-annotations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
EnumValueDefinitionNode,
1717
FieldDefinitionNode,
1818
InputValueDefinitionNode,
19+
Kind,
1920
TypeDefinitionNode,
2021
} from "graphql";
2122
import { buildDescriptionAnnotation } from "./build-description-annotation";
@@ -60,8 +61,8 @@ export function buildAnnotations({
6061
];
6162

6263
const shouldIndent =
63-
definitionNode?.kind === "FieldDefinition" ||
64-
definitionNode?.kind === "InputValueDefinition";
64+
definitionNode?.kind === Kind.FIELD_DEFINITION ||
65+
definitionNode?.kind === Kind.INPUT_VALUE_DEFINITION;
6566
return (
6667
annotations
6768
.map((annotation) =>

src/helpers/build-description-annotation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function buildDescriptionAnnotation(
3131
(arg) => arg.name.value === "reason",
3232
)?.value;
3333
const deprecatedReason =
34-
deprecatedReasonNode?.kind === "StringValue"
34+
deprecatedReasonNode?.kind === Kind.STRING
3535
? deprecatedReasonNode.value
3636
: "";
3737
const trimmedDeprecatedReason = trimDescription(deprecatedReason);

src/helpers/build-directive-annotations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CodegenConfigWithDefaults } from "./build-config-with-defaults";
1515
import { DefinitionNode } from "./build-annotations";
1616
import { getFederationDirectiveReplacement } from "./get-federation-directive-replacement";
1717
import { ConstDirectiveNode } from "graphql/language";
18+
import { Kind } from "graphql";
1819

1920
export function buildDirectiveAnnotations(
2021
definitionNode: DefinitionNode,
@@ -65,7 +66,7 @@ function buildKotlinAnnotations(
6566
`Directive argument ${argumentToRetain} in directive ${directive.name.value} has an unsupported type. Only INT, FLOAT, STRING, BOOLEAN, and ENUM are supported.`,
6667
);
6768
const argumentValue =
68-
argumentValueNode.kind === "StringValue"
69+
argumentValueNode.kind === Kind.STRING
6970
? `"${argumentValueNode.value}"`
7071
: argumentValueNode.value;
7172
return `${argumentToRetain} = ${argumentValue}`;

src/helpers/build-type-metadata.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
TypeNode,
1919
isScalarType,
2020
isUnionType,
21+
isInputObjectType,
2122
} from "graphql";
2223
import { getBaseTypeNode } from "@graphql-codegen/visitor-plugin-common";
2324
import { wrapTypeWithModifiers } from "@graphql-codegen/java-common";
@@ -76,10 +77,10 @@ export function buildTypeMetadata(
7677
shouldTreatUnionAsInterface ? schemaType.name : "Any",
7778
),
7879
};
79-
} else {
80+
} else if (isInputObjectType(schemaType) && schemaType.astNode) {
8081
const typeWillBeConsolidated = inputTypeHasMatchingOutputType(
81-
schema,
8282
schemaType.astNode,
83+
schema,
8384
);
8485
const typeName = typeWillBeConsolidated
8586
? getTypeNameWithoutInput(schemaType.name)
@@ -88,6 +89,11 @@ export function buildTypeMetadata(
8889
...commonMetadata,
8990
typeName: buildListType(typeNode, typeName),
9091
};
92+
} else {
93+
return {
94+
...commonMetadata,
95+
typeName: buildListType(typeNode, schemaType.name),
96+
};
9197
}
9298
}
9399

src/helpers/dependent-type-utils.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ limitations under the License.
1414
import {
1515
GraphQLSchema,
1616
GraphQLUnionType,
17+
isUnionType,
1718
Kind,
1819
TypeDefinitionNode,
1920
TypeNode,
@@ -42,7 +43,7 @@ function getFieldTypeName(fieldType: TypeNode) {
4243

4344
export function getDependentInterfaceNames(node: TypeDefinitionNode) {
4445
return node.kind === Kind.OBJECT_TYPE_DEFINITION
45-
? node.interfaces?.map((i) => i.name.value) ?? []
46+
? node.interfaces?.map((interfaceNode) => interfaceNode.name.value) ?? []
4647
: [];
4748
}
4849

@@ -57,11 +58,9 @@ export function getDependentUnionsForType(
5758
node: TypeDefinitionNode,
5859
) {
5960
const typeMap = schema.getTypeMap();
60-
const unions = Object.keys(typeMap)
61-
.filter(
62-
(type) => typeMap[type]?.astNode?.kind === Kind.UNION_TYPE_DEFINITION,
63-
)
64-
.map((type) => typeMap[type] as GraphQLUnionType);
61+
const unions = Object.values(typeMap).filter((type) =>
62+
isUnionType(type),
63+
) as GraphQLUnionType[];
6564
return unions
6665
.filter((union) =>
6766
union.getTypes().some((type) => type.name === node.name.value),
Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,61 @@
1-
import { Kind, TypeNode } from "graphql/index";
2-
import { GraphQLSchema, TypeDefinitionNode } from "graphql";
1+
import {
2+
GraphQLSchema,
3+
InputObjectTypeDefinitionNode,
4+
isInputObjectType,
5+
isObjectType,
6+
} from "graphql";
7+
import { getBaseTypeNode } from "@graphql-codegen/visitor-plugin-common";
38

49
export function inputTypeHasMatchingOutputType(
10+
inputNode: InputObjectTypeDefinitionNode,
511
schema: GraphQLSchema,
6-
typeNode?: TypeDefinitionNode | null,
712
) {
8-
if (typeNode?.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION) {
9-
return false;
10-
}
11-
12-
const typeNameWithoutInput = getTypeNameWithoutInput(typeNode.name.value);
13-
const matchingType = schema.getType(typeNameWithoutInput)?.astNode;
14-
const matchingTypeFields =
15-
matchingType?.kind === Kind.OBJECT_TYPE_DEFINITION
16-
? matchingType.fields
17-
: [];
18-
const inputFields = typeNode.fields;
19-
const fieldsMatch = matchingTypeFields?.every((field) => {
20-
const matchingInputField = inputFields?.find(
21-
(inputField) => inputField.name.value === field.name.value,
22-
);
23-
if (!matchingInputField) return false;
24-
return fieldsAreEquivalent(field.type, matchingInputField.type);
25-
});
26-
return Boolean(matchingTypeFields?.length && fieldsMatch);
13+
const inputName = inputNode.name.value;
14+
const typeNameWithoutInput = getTypeNameWithoutInput(inputName);
15+
const matchingTypeName =
16+
schema.getType(typeNameWithoutInput)?.astNode?.name.value;
17+
return Boolean(
18+
matchingTypeName && typesAreEquivalent(matchingTypeName, inputName, schema),
19+
);
2720
}
2821

2922
export function getTypeNameWithoutInput(name: string) {
3023
return name.endsWith("Input") ? name.replace("Input", "") : name;
3124
}
3225

33-
function fieldsAreEquivalent(
34-
typeField: TypeNode,
35-
inputField: TypeNode,
26+
function typesAreEquivalent(
27+
typeName: string,
28+
inputName: string,
29+
schema: GraphQLSchema,
3630
): boolean {
37-
switch (typeField.kind) {
38-
case Kind.NAMED_TYPE:
39-
return (
40-
inputField.kind === Kind.NAMED_TYPE &&
41-
typeField.name.value === getTypeNameWithoutInput(inputField.name.value)
42-
);
43-
case Kind.LIST_TYPE:
44-
return (
45-
inputField.kind === Kind.LIST_TYPE &&
46-
fieldsAreEquivalent(typeField.type, inputField.type)
47-
);
48-
case Kind.NON_NULL_TYPE:
49-
return (
50-
inputField.kind === Kind.NON_NULL_TYPE &&
51-
fieldsAreEquivalent(typeField.type, inputField.type)
52-
);
31+
const typeNode = schema.getType(typeName);
32+
const inputNode = schema.getType(inputName);
33+
if (!typeNode?.astNode && !inputNode?.astNode) {
34+
return true;
5335
}
36+
if (
37+
!isObjectType(typeNode) ||
38+
!isInputObjectType(inputNode) ||
39+
!typeNode.astNode?.fields ||
40+
!inputNode.astNode?.fields ||
41+
typeNode.astNode.fields.length !== inputNode.astNode.fields.length
42+
) {
43+
return false;
44+
}
45+
46+
return typeNode.astNode.fields.every((typeField) => {
47+
const matchingInputField = inputNode.astNode?.fields?.find(
48+
(inputField) => inputField.name.value === typeField.name.value,
49+
);
50+
if (!matchingInputField?.type) return false;
51+
const baseTypeName = getBaseTypeNode(typeField.type).name.value;
52+
const baseInputTypeName = getBaseTypeNode(matchingInputField.type).name
53+
.value;
54+
const typeNamesAreEquivalent =
55+
baseTypeName == getTypeNameWithoutInput(baseInputTypeName);
56+
if (!typeNamesAreEquivalent) {
57+
return false;
58+
}
59+
return typesAreEquivalent(baseTypeName, baseInputTypeName, schema);
60+
});
5461
}

test/unit/should_consolidate_input_and_output_types/expected.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,37 @@ interface MyTypeToConsolidateParent2 {
7777
interface MyTypeToConsolidateParent2CompletableFuture {
7878
fun field(input: MyTypeToConsolidate): java.util.concurrent.CompletableFuture<String?>
7979
}
80+
81+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
82+
data class MyTypeNotToConsolidateParent(
83+
val field: MyTypeNotToConsolidate2? = null
84+
)
85+
86+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
87+
data class MyTypeNotToConsolidateParentInput(
88+
val field: MyTypeNotToConsolidate2Input? = null
89+
)
90+
91+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
92+
data class MyTypeNotToConsolidate2(
93+
val field1: String? = null,
94+
val field2: String? = null
95+
)
96+
97+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
98+
data class MyTypeNotToConsolidate2Input(
99+
val field1: String? = null
100+
)
101+
102+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
103+
data class MySuperSetType(
104+
val field: String? = null,
105+
val field2: String? = null
106+
)
107+
108+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
109+
data class MySuperSetTypeInput(
110+
val field: String? = null,
111+
val field2: String? = null,
112+
val field3: Int? = null
113+
)

test/unit/should_consolidate_input_and_output_types/schema.graphql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,35 @@ input MyTypeToConsolidateInputParent {
9898
type MyTypeToConsolidateParent2 {
9999
field(input: MyTypeToConsolidateInput!): String
100100
}
101+
102+
# case where parent types reference types that should not be consolidated
103+
104+
type MyTypeNotToConsolidateParent {
105+
field: MyTypeNotToConsolidate2
106+
}
107+
108+
input MyTypeNotToConsolidateParentInput {
109+
field: MyTypeNotToConsolidate2Input
110+
}
111+
112+
type MyTypeNotToConsolidate2 {
113+
field1: String
114+
field2: String
115+
}
116+
117+
input MyTypeNotToConsolidate2Input {
118+
field1: String
119+
}
120+
121+
# case where input fields are a superset of type fields
122+
123+
type MySuperSetType {
124+
field: String
125+
field2: String
126+
}
127+
128+
input MySuperSetTypeInput {
129+
field: String
130+
field2: String
131+
field3: Int
132+
}

0 commit comments

Comments
 (0)