Skip to content

feat: consolidate matching input and output types into single generated class #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/definitions/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { buildTypeMetadata } from "../helpers/build-type-metadata";
import { buildAnnotations } from "../helpers/build-annotations";
import { indent } from "@graphql-codegen/visitor-plugin-common";
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";
import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type";

export function buildInputObjectDefinition(
node: InputObjectTypeDefinitionNode,
Expand All @@ -27,6 +28,11 @@ export function buildInputObjectDefinition(
return "";
}

const typeWillBeConsolidated = inputTypeHasMatchingOutputType(node, schema);
if (typeWillBeConsolidated) {
return "";
}

const classMembers = (node.fields ?? [])
.map((arg) => {
const typeToUse = buildTypeMetadata(arg.type, schema, config);
Expand Down
52 changes: 52 additions & 0 deletions src/helpers/input-type-has-matching-output-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Kind, TypeNode } from "graphql/index";
import { GraphQLSchema, InputObjectTypeDefinitionNode } from "graphql";

export function inputTypeHasMatchingOutputType(
inputTypeNode: InputObjectTypeDefinitionNode,
schema: GraphQLSchema,
) {
const typeNameWithoutInput = getTypeNameWithoutInput(
inputTypeNode.name.value,
);
const matchingType = schema.getType(typeNameWithoutInput)?.astNode;
const matchingTypeFields =
matchingType?.kind === Kind.OBJECT_TYPE_DEFINITION
? matchingType.fields
: [];
const inputFields = inputTypeNode.fields;
const fieldsMatch = matchingTypeFields?.every((field) => {
const matchingInputField = inputFields?.find(
(inputField) => inputField.name.value === field.name.value,
);
if (!matchingInputField) return false;
return fieldsAreEquivalent(field.type, matchingInputField.type);
});
return Boolean(matchingTypeFields?.length && fieldsMatch);
}

function getTypeNameWithoutInput(name: string) {
return name.endsWith("Input") ? name.replace("Input", "") : name;
}

function fieldsAreEquivalent(
typeField: TypeNode,
inputField: TypeNode,
): boolean {
switch (typeField.kind) {
case Kind.NAMED_TYPE:
return (
inputField.kind === Kind.NAMED_TYPE &&
typeField.name.value === getTypeNameWithoutInput(inputField.name.value)
);
case Kind.LIST_TYPE:
return (
inputField.kind === Kind.LIST_TYPE &&
fieldsAreEquivalent(typeField.type, inputField.type)
);
case Kind.NON_NULL_TYPE:
return (
inputField.kind === Kind.NON_NULL_TYPE &&
fieldsAreEquivalent(typeField.type, inputField.type)
);
}
}
53 changes: 53 additions & 0 deletions test/unit/should_consolidate_input_and_output_types/expected.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.kotlin.generated

import com.expediagroup.graphql.generator.annotations.*

data class MyTypeToConsolidate(
val field: List<String>? = null,
val field2: NestedTypeToConsolidate? = null
)

data class NestedTypeToConsolidate(
val field: String? = null
)

@GraphQLDescription("A description for MyTypeToConsolidate2")
data class MyTypeToConsolidate2(
val field: String? = null
)

data class MyTypeToConsolidate3(
val field: String? = null
)

@GraphQLDescription("It always uses the type description when consolidating")
data class MyTypeToConsolidate4(
val field: String? = null
)

data class MyTypeNotToConsolidate(
val field: String? = null
)

@GraphQLDescription("The type name must exactly match in order to consolidate")
data class MyTypeToNotConsolidateInput(
val field: String? = null
)

data class MyTypeToNotConsolidate2(
val field: String? = null
)

data class MyTypeInputToNotConsolidate2(
val field: String? = null
)

data class MyTypeWhereFieldsDoNotMatch(
val field: String? = null,
val field2: String? = null
)

data class MyTypeWhereFieldsDoNotMatchInput(
val field: String? = null,
val field2: Int? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# typical case where input and output types are consolidated

type MyTypeToConsolidate {
field: [String!]
field2: NestedTypeToConsolidate
}

input MyTypeToConsolidateInput {
field: [String!]
field2: NestedTypeToConsolidateInput
}

type NestedTypeToConsolidate {
field: String
}

input NestedTypeToConsolidateInput {
field: String
}

# case where type has description but input does not

"A description for MyTypeToConsolidate2"
type MyTypeToConsolidate2 {
field: String
}

input MyTypeToConsolidate2Input {
field: String
}

# case where input has description but type does not

type MyTypeToConsolidate3 {
field: String
}

"It ignores the description on the input when consolidating"
input MyTypeToConsolidate3Input {
field: String
}

# case where both type and input have descriptions

"It always uses the type description when consolidating"
type MyTypeToConsolidate4 {
field: String
}

"A description for MyTypeToConsolidateInput4"
input MyTypeToConsolidate4Input {
field: String
}

# case where type and input names do not match

type MyTypeNotToConsolidate {
field: String
}

"The type name must exactly match in order to consolidate"
input MyTypeToNotConsolidateInput {
field: String
}

# case where type has input in the middle of the name

type MyTypeToNotConsolidate2 {
field: String
}

input MyTypeInputToNotConsolidate2 {
field: String
}

# case where type and input names match but fields do not match

type MyTypeWhereFieldsDoNotMatch {
field: String
field2: String
}

input MyTypeWhereFieldsDoNotMatchInput {
field: String
field2: Int
}