diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/definitions/fields/ObjectField.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/definitions/fields/ObjectField.java index a6b4ff9cb..b2e5e933f 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/definitions/fields/ObjectField.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/definitions/fields/ObjectField.java @@ -24,7 +24,7 @@ public class ObjectField extends GenerationSourceField { private int firstDefault = 100; private int lastDefault = 100; - private boolean hasForwardPagination, hasBackwardPagination, hasRequiredPaginationFields; + private boolean hasForwardPagination, hasBackwardPagination, hasRequiredPaginationFields, hasPaginationTotalCountField; private final List arguments, nonReservedArguments; private final ArgumentField orderField; private final LinkedHashMap argumentsByName; @@ -128,6 +128,15 @@ public boolean hasPagination() { return hasForwardPagination() || hasBackwardPagination(); } + public boolean hasTotalCountFieldInReturnType() { + return hasPaginationTotalCountField; + } + + public void setHasTotalCountFieldInReturnType(boolean hasTotalCountField) { + hasPaginationTotalCountField = hasTotalCountField; + } + + @Override public boolean hasMutationType() { return mutationType != null; diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/operations/OperationMethodGenerator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/operations/OperationMethodGenerator.java index cb96f7b96..609a36c6d 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/operations/OperationMethodGenerator.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/operations/OperationMethodGenerator.java @@ -137,7 +137,12 @@ private CodeBlock callQueryBlock(ObjectField target, String objectToCall, String .build(); return CodeBlock .builder() - .add("return $L.$L($L", target.hasServiceReference() ? newServiceDataFetcherWithTransform() : newDataFetcher(), getFetcherMethodName(target, localObject), indentIfMultiline(innerCode)) + .add("return $L.$L($L", + target.hasServiceReference() + ? newServiceDataFetcherWithTransform() + : newDataFetcher(), + getFetcherMethodName(target, localObject), + indentIfMultiline(innerCode)) .addStatement(")") .build(); } @@ -177,8 +182,13 @@ private CodeBlock callQueryBlockInner(ObjectField target, String objectToCall, S params.add(VAR_RESOLVER_KEYS); } params.addAll(parser.getMethodInputNames(false, false, true)); - var countFunction = countFunction(objectToCall, method, params, target.hasServiceReference()); - return CodeBlock.of(" $N,\n$L,\n$L$L", VAR_PAGE_SIZE, queryFunction, countFunction, transformWrap); + + if (target.hasTotalCountFieldInReturnType()) { + var countFunction = countFunction(objectToCall, method, params, target.hasServiceReference()); + return CodeBlock.of(" $N,\n$L,\n$L$L", VAR_PAGE_SIZE, queryFunction, countFunction, transformWrap); + } else { + return CodeBlock.of(" $N,\n$L$L", VAR_PAGE_SIZE, queryFunction, transformWrap); + } } /** diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchCountDBMethodGenerator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchCountDBMethodGenerator.java index 8ecadfa09..d0b965400 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchCountDBMethodGenerator.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchCountDBMethodGenerator.java @@ -158,6 +158,7 @@ public List generateAll() { .stream() .filter(GenerationField::isGeneratedWithResolver) .filter(ObjectField::hasRequiredPaginationFields) + .filter(ObjectField::hasTotalCountFieldInReturnType) .filter(it -> !it.hasServiceReference()) .map(this::generate) .filter(it -> !it.code().isEmpty()) diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphql/schema/ProcessedSchema.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphql/schema/ProcessedSchema.java index 0cccb8e62..5ff0d16e7 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphql/schema/ProcessedSchema.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphql/schema/ProcessedSchema.java @@ -168,6 +168,8 @@ public ProcessedSchema(TypeDefinitionRegistry typeRegistry) { federationIsImported = LinkDirectiveProcessor.loadFederationImportedDefinitions(typeRegistry) != null; federationEntitiesExist = queryType != null && queryType.hasField(FEDERATION_ENTITIES_FIELD.getName()) && isFederationImported(); + + markConnectionsWithTotalCountField(); } private SchemaDefinition createSchemaDefinition() { @@ -196,6 +198,21 @@ private SchemaDefinition createSchemaDefinition() { return definitionBuilder.build(); } + /** + * Mark fields whose return type is a connection type that contains the {@code totalCount} field. This is used to + * determine whether to generate a count method or not through + * {@link no.sikt.graphitron.generators.db.FetchCountDBMethodGenerator}. + */ + private void markConnectionsWithTotalCountField() { + this.objects + .values() + .stream() + .flatMap(obj -> obj.getFields().stream()) + .filter(this::isConnectionObject) + .filter(this::hasTotalCountField) + .forEach(field -> field.setHasTotalCountFieldInReturnType(true)); + } + public boolean nodeExists() { return nodeExists; } @@ -829,6 +846,16 @@ public boolean federationEntitiesExist() { return federationEntitiesExist; } + /** + * @param field A field whose type is assumed to be a connection object present in {@link #connectionObjects}. + * @return Whether this field points to a connection object type that has a {@code totalCount} field. + */ + private boolean hasTotalCountField(FieldSpecification field) { + return connectionObjects + .get(field.getTypeName()) + .hasField(GraphQLReservedName.CONNECTION_TOTAL_COUNT.getName()); + } + /** * @return All types which could potentially have tables. */ diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/common/configuration/SchemaComponent.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/common/configuration/SchemaComponent.java index 1030dfbf8..c38fbc868 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/common/configuration/SchemaComponent.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/common/configuration/SchemaComponent.java @@ -31,7 +31,9 @@ public enum SchemaComponent { CUSTOMER_TABLE("basic/CustomerTable"), CUSTOMER_INPUT_TABLE("basic/CustomerInputTable"), CUSTOMER_CONNECTION_ONLY("basic/CustomerConnection", PAGE_INFO), + CUSTOMER_CONNECTION_WITH_NO_OPTIONALS_ONLY("basic/CustomerConnectionWithNoOptionals", PAGE_INFO), CUSTOMER_CONNECTION(CUSTOMER_TABLE, CUSTOMER_CONNECTION_ONLY), + CUSTOMER_CONNECTION_WITH_NO_OPTIONALS(CUSTOMER_TABLE, CUSTOMER_CONNECTION_WITH_NO_OPTIONALS_ONLY), CUSTOMER_CONNECTION_ORDER(CUSTOMER_CONNECTION_ONLY, ORDER), CUSTOMER_UNION("basic/CustomerUnion", CUSTOMER_QUERY), CUSTOMER_NODE_INPUT_TABLE("basic/CustomerNodeInputTable", CUSTOMER_NODE), diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/services/fetch/ResolverTest.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/services/fetch/ResolverTest.java index bc701dec5..433add646 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/services/fetch/ResolverTest.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/services/fetch/ResolverTest.java @@ -146,4 +146,23 @@ void queryAfterService() { "queryNormal() {return _iv_env -> {return" ); } + + @Test + @DisplayName("When optional field 'totalCount' is included in the schema for a field having a service and pagination, generate the count method") + void serviceWithPaginationAndAllOptionalFieldsIncluded() { + assertGeneratedContentMatches( + "operation/withPaginationAndAllOptionalFieldsIncluded", + CUSTOMER_CONNECTION + ); + } + + + @Test + @DisplayName("When optional field 'totalCount' is not included in the schema for a field having a service and pagination, do not generate the count method") + void serviceWithPaginationAndNoOptionalFieldsIncluded() { + assertGeneratedContentMatches( + "operation/withPaginationAndNoOptionalFieldsIncluded", + CUSTOMER_CONNECTION_WITH_NO_OPTIONALS + ); + } } diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/standard/fetch/ResolverPaginationTest.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/standard/fetch/ResolverPaginationTest.java index 52bb5a9c3..52fdc2c82 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/standard/fetch/ResolverPaginationTest.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/datafetchers/standard/fetch/ResolverPaginationTest.java @@ -14,6 +14,8 @@ import java.util.Set; import static no.sikt.graphitron.common.configuration.ReferencedEntry.CONTEXT_CONDITION; +import static no.sikt.graphitron.common.configuration.SchemaComponent.CUSTOMER_CONNECTION; +import static no.sikt.graphitron.common.configuration.SchemaComponent.CUSTOMER_CONNECTION_WITH_NO_OPTIONALS; import static no.sikt.graphitron.common.configuration.SchemaComponent.DUMMY_CONNECTION; import static no.sikt.graphitron.common.configuration.SchemaComponent.SPLIT_QUERY_WRAPPER; @@ -104,4 +106,34 @@ void withContextCondition() { "countQueryForQuery(_iv_ctx, _cf_ctxField)" ); } + + @Test + @DisplayName("When optional field 'totalCount' is included in the schema, generate the count method") + void countMethodCalledWhenAllOptionalFieldsIncluded() { + assertGeneratedContentContains( + "operation/allOptionalFieldsIncluded", + Set.of(CUSTOMER_CONNECTION), + """ + .loadPaginated( + _iv_pageSize, + (_iv_ctx, _iv_selectionSet) -> QueryDBQueries.customersForQuery(_iv_ctx, _iv_pageSize, _mi_after, _iv_selectionSet), + (_iv_ctx, _iv_keys) -> QueryDBQueries.countCustomersForQuery(_iv_ctx) + """ + ); + } + + @Test + @DisplayName("When optional field 'totalCount' is not included in the schema, do not generate the count method") + void countMethodNotCalledWhenAllOptionalFieldsDisabled() { + assertGeneratedContentContains( + "operation/allOptionalFieldsExcluded", + Set.of(CUSTOMER_CONNECTION_WITH_NO_OPTIONALS), + """ + .loadPaginated( + _iv_pageSize, + (_iv_ctx, _iv_selectionSet) -> QueryDBQueries.customersForQuery(_iv_ctx, _iv_pageSize, _mi_after, _iv_selectionSet) + ); + """ + ); + } } diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/PaginationTest.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/PaginationTest.java index 1f4690834..999c8ddf7 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/PaginationTest.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/PaginationTest.java @@ -1,9 +1,8 @@ package no.sikt.graphitron.queries.fetch; import no.sikt.graphitron.common.GeneratorTest; -import no.sikt.graphitron.common.configuration.SchemaComponent; import no.sikt.graphitron.generators.abstractions.ClassGenerator; -import no.sikt.graphitron.reducedgenerators.MapOnlyFetchDBClassGenerator; +import no.sikt.graphitron.reducedgenerators.PaginationOnlyDBClassGenerator; import no.sikt.graphql.schema.ProcessedSchema; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -20,20 +19,15 @@ protected String getSubpath() { return "queries/fetch/pagination"; } - @Override - protected Set getComponents() { - return makeComponents(CUSTOMER_CONNECTION); - } - @Override protected List makeGenerators(ProcessedSchema schema) { - return List.of(new MapOnlyFetchDBClassGenerator(schema)); + return List.of(new PaginationOnlyDBClassGenerator(schema)); } @Test @DisplayName("Connection with no other fields") void defaultCase() { - assertGeneratedContentMatches("default"); + assertGeneratedContentMatches("default", CUSTOMER_CONNECTION_WITH_NO_OPTIONALS); } @Test @@ -41,6 +35,7 @@ void defaultCase() { void withOtherField() { assertGeneratedContentContains( "withOtherField", + Set.of(CUSTOMER_CONNECTION_WITH_NO_OPTIONALS), ", String _mi_name, Integer _iv_pageSize, String _mi_after," ); } @@ -48,15 +43,39 @@ void withOtherField() { @Test @DisplayName("Connection not on the root level") void splitQuery() { - assertGeneratedContentMatches("splitQuery", SPLIT_QUERY_WRAPPER); + assertGeneratedContentMatches( + "splitQuery", + CUSTOMER_CONNECTION_WITH_NO_OPTIONALS, + SPLIT_QUERY_WRAPPER + ); } @Test @DisplayName("No pagination on non-connection multiset") void multiset() { - resultDoesNotContain("multiset", Set.of(CUSTOMER_TABLE), + resultDoesNotContain( + "multiset", + Set.of(CUSTOMER_TABLE), ".getOrderByValues(", ".seek(" ); } + + @Test + @DisplayName("When optional field 'totalCount' is included in the schema, generate the count method") + void shouldGenerateAllOptionalFields() { + assertGeneratedContentContains( + "allOptionalFieldsIncluded", + Set.of(CUSTOMER_CONNECTION), + "countCustomersForQuery("); + } + + @Test + @DisplayName("When optional field 'totalCount' is not included in the schema, do not generate the count method") + void shouldGenerateNoOptionalFields() { + resultDoesNotContain( + "allOptionalFieldsExcluded", + Set.of(CUSTOMER_CONNECTION_WITH_NO_OPTIONALS), + "countCustomersForQuery("); + } } diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/reducedgenerators/PaginationOnlyDBClassGenerator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/reducedgenerators/PaginationOnlyDBClassGenerator.java new file mode 100644 index 000000000..abb937f5e --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/reducedgenerators/PaginationOnlyDBClassGenerator.java @@ -0,0 +1,27 @@ +package no.sikt.graphitron.reducedgenerators; + +import no.sikt.graphitron.definitions.objects.ObjectDefinition; +import no.sikt.graphitron.generators.db.DBClassGenerator; +import no.sikt.graphitron.generators.db.FetchCountDBMethodGenerator; +import no.sikt.graphitron.generators.db.FetchMappedObjectDBMethodGenerator; +import no.sikt.graphitron.generators.db.SelectHelperDBMethodGenerator; +import no.sikt.graphitron.javapoet.TypeSpec; +import no.sikt.graphql.schema.ProcessedSchema; + +import java.util.List; + +public class PaginationOnlyDBClassGenerator extends DBClassGenerator { + public PaginationOnlyDBClassGenerator(ProcessedSchema processedSchema) { + super(processedSchema); + } + + @Override + public TypeSpec generate(ObjectDefinition target) { + return getSpec( + target.getName(), + List.of(new FetchMappedObjectDBMethodGenerator(target, processedSchema), + new SelectHelperDBMethodGenerator(target, processedSchema), + new FetchCountDBMethodGenerator(target, processedSchema)) + ).build(); + } +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnection.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnection.graphqls index edd644072..8dcd8a4d1 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnection.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnection.graphqls @@ -2,6 +2,7 @@ type CustomerConnection { edges: [CustomerConnectionEdge] pageInfo: PageInfo nodes: [CustomerTable!]! + totalCount: Int } type CustomerConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnectionWithNoOptionals.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnectionWithNoOptionals.graphqls new file mode 100644 index 000000000..664e8c7cb --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/CustomerConnectionWithNoOptionals.graphqls @@ -0,0 +1,9 @@ +type CustomerConnection { + edges: [CustomerConnectionEdge] + pageInfo: PageInfo +} + +type CustomerConnectionEdge { + cursor: String + node: CustomerTable +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/DummyConnection.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/DummyConnection.graphqls index e651f14cf..66428546f 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/DummyConnection.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/DummyConnection.graphqls @@ -2,6 +2,7 @@ type DummyConnection { edges: [DummyConnectionEdge] pageInfo: PageInfo nodes: [DummyType!]! + totalCount: Int } type DummyConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/PersonWithEmailConnection.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/PersonWithEmailConnection.graphqls index 64909e152..37c154e8d 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/PersonWithEmailConnection.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/PersonWithEmailConnection.graphqls @@ -1,6 +1,7 @@ type PersonWithEmailConnection { edges: [PersonWithEmailConnectionEdge] nodes: [PersonWithEmail!]! + totalCount: Int } type PersonWithEmailConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/SomeUnionConnection.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/SomeUnionConnection.graphqls index 9f266ebc4..17f1a8050 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/SomeUnionConnection.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/components/basic/SomeUnionConnection.graphqls @@ -2,6 +2,7 @@ type SomeUnionConnection { edges: [SomeUnionConnectionEdge] pageInfo: PageInfo nodes: [SomeUnion!]! + totalCount: Int } type SomeUnionConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/pagination/operation/allOptionalFieldsExcluded/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/pagination/operation/allOptionalFieldsExcluded/schema.graphqls new file mode 100644 index 000000000..182f536cc --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/pagination/operation/allOptionalFieldsExcluded/schema.graphqls @@ -0,0 +1,3 @@ +type Query { + customers(first: Int = 100, after: String): CustomerConnection +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/pagination/operation/allOptionalFieldsIncluded/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/pagination/operation/allOptionalFieldsIncluded/schema.graphqls new file mode 100644 index 000000000..182f536cc --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/pagination/operation/allOptionalFieldsIncluded/schema.graphqls @@ -0,0 +1,3 @@ +type Query { + customers(first: Int = 100, after: String): CustomerConnection +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndAllOptionalFieldsIncluded/expected/QueryGeneratedDataFetcher.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndAllOptionalFieldsIncluded/expected/QueryGeneratedDataFetcher.java new file mode 100644 index 000000000..09cf504a1 --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndAllOptionalFieldsIncluded/expected/QueryGeneratedDataFetcher.java @@ -0,0 +1,28 @@ +import fake.code.generated.transform.RecordTransformer; +import fake.graphql.example.model.CustomerTable; +import graphql.schema.DataFetcher; +import java.lang.Integer; +import java.lang.String; +import java.util.concurrent.CompletableFuture; +import no.sikt.graphitron.codereferences.services.ResolverFetchService; +import no.sikt.graphql.helpers.resolvers.ResolverHelpers; +import no.sikt.graphql.helpers.resolvers.ServiceDataFetcherHelper; +import no.sikt.graphql.relay.ConnectionImpl; + +public class QueryGeneratedDataFetcher { + public static DataFetcher>> customers() { + return _iv_env -> { + Integer _mi_first = _iv_env.getArgument("first"); + String _mi_after = _iv_env.getArgument("after"); + int _iv_pageSize = ResolverHelpers.getPageSize(_mi_first, 1000, 100); + var _iv_transform = new RecordTransformer(_iv_env); + var _rs_resolverFetchService = new ResolverFetchService(_iv_transform.getCtx()); + return new ServiceDataFetcherHelper<>(_iv_transform).loadPaginated( + _iv_pageSize, + () -> _rs_resolverFetchService.queryList(_iv_pageSize, _mi_after), + (_iv_keys) -> _rs_resolverFetchService.countQueryList(), + (_iv_recordTransform, _iv_response) -> _iv_recordTransform.customerTableRecordToGraphType(_iv_response, "") + ); + } ; + } +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndAllOptionalFieldsIncluded/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndAllOptionalFieldsIncluded/schema.graphqls new file mode 100644 index 000000000..488e2b010 --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndAllOptionalFieldsIncluded/schema.graphqls @@ -0,0 +1,4 @@ +type Query { + customers(first: Int = 100, after: String): CustomerConnection @service( + service: {name: "RESOLVER_FETCH_SERVICE", method: "queryList"}) +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndNoOptionalFieldsIncluded/expected/QueryGeneratedDataFetcher.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndNoOptionalFieldsIncluded/expected/QueryGeneratedDataFetcher.java new file mode 100644 index 000000000..3fd2be0f1 --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndNoOptionalFieldsIncluded/expected/QueryGeneratedDataFetcher.java @@ -0,0 +1,27 @@ +import fake.code.generated.transform.RecordTransformer; +import fake.graphql.example.model.CustomerTable; +import graphql.schema.DataFetcher; +import java.lang.Integer; +import java.lang.String; +import java.util.concurrent.CompletableFuture; +import no.sikt.graphitron.codereferences.services.ResolverFetchService; +import no.sikt.graphql.helpers.resolvers.ResolverHelpers; +import no.sikt.graphql.helpers.resolvers.ServiceDataFetcherHelper; +import no.sikt.graphql.relay.ConnectionImpl; + +public class QueryGeneratedDataFetcher { + public static DataFetcher>> customers() { + return _iv_env -> { + Integer _mi_first = _iv_env.getArgument("first"); + String _mi_after = _iv_env.getArgument("after"); + int _iv_pageSize = ResolverHelpers.getPageSize(_mi_first, 1000, 100); + var _iv_transform = new RecordTransformer(_iv_env); + var _rs_resolverFetchService = new ResolverFetchService(_iv_transform.getCtx()); + return new ServiceDataFetcherHelper<>(_iv_transform).loadPaginated( + _iv_pageSize, + () -> _rs_resolverFetchService.queryList(_iv_pageSize, _mi_after), + (_iv_recordTransform, _iv_response) -> _iv_recordTransform.customerTableRecordToGraphType(_iv_response, "") + ); + } ; + } +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndNoOptionalFieldsIncluded/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndNoOptionalFieldsIncluded/schema.graphqls new file mode 100644 index 000000000..488e2b010 --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/datafetchers/fetch/services/operation/withPaginationAndNoOptionalFieldsIncluded/schema.graphqls @@ -0,0 +1,4 @@ +type Query { + customers(first: Int = 100, after: String): CustomerConnection @service( + service: {name: "RESOLVER_FETCH_SERVICE", method: "queryList"}) +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterface/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterface/schema.graphqls index acdd1335e..14a596e83 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterface/schema.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterface/schema.graphqls @@ -17,6 +17,7 @@ type PaymentTypeTwo implements Payment @table(name: "PAYMENT_P2007_02") { type PaymentConnection { edges: [PaymentConnectionEdge] nodes: [Payment!]! + totalCount: Int } type PaymentConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterfaceWithOtherField/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterfaceWithOtherField/schema.graphqls index 4a85cad99..fccf124aa 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterfaceWithOtherField/schema.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/multiTableInterfaceWithOtherField/schema.graphqls @@ -17,6 +17,7 @@ type PaymentTypeTwo implements Payment @table(name: "PAYMENT_P2007_02") { type PaymentConnection { edges: [PaymentConnectionEdge] nodes: [Payment!]! + totalCount: Int } type PaymentConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/singleTableInterface/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/singleTableInterface/schema.graphqls index d73e73193..d83cd9fec 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/singleTableInterface/schema.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/count/singleTableInterface/schema.graphqls @@ -19,6 +19,7 @@ type AddressConnection { edges: [AddressConnectionEdge] pageInfo: PageInfo nodes: [Address!]! + totalCount: Int } type AddressConnectionEdge { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/pagination/allOptionalFieldsExcluded/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/pagination/allOptionalFieldsExcluded/schema.graphqls new file mode 100644 index 000000000..182f536cc --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/pagination/allOptionalFieldsExcluded/schema.graphqls @@ -0,0 +1,3 @@ +type Query { + customers(first: Int = 100, after: String): CustomerConnection +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/pagination/allOptionalFieldsIncluded/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/pagination/allOptionalFieldsIncluded/schema.graphqls new file mode 100644 index 000000000..182f536cc --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/queries/fetch/pagination/allOptionalFieldsIncluded/schema.graphqls @@ -0,0 +1,3 @@ +type Query { + customers(first: Int = 100, after: String): CustomerConnection +} diff --git a/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/DataFetcherHelper.java b/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/DataFetcherHelper.java index 7ec3b6e1c..48c91d8e7 100644 --- a/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/DataFetcherHelper.java +++ b/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/DataFetcherHelper.java @@ -43,7 +43,24 @@ public CompletableFuture load(DBQueryRoot dbFunction) { * Load the data for a root resolver. The result is paginated. * @param pageSize Size of the pages for pagination. * @param dbFunction Function to call to retrieve the query data. - * @param countFunction Function to call to retrieve the total count of elements that could be potentially retrieved. + * @return A paginated resolver result. + * @param Type that the resolver fetches. + */ + public CompletableFuture> loadPaginated( + int pageSize, + DBQueryRoot>> dbFunction + ) { + return loadPaginated(pageSize, dbFunction, null); + } + + + /** + * Load the data for a root resolver. The result is paginated. + * @param pageSize Size of the pages for pagination. + * @param dbFunction Function to call to retrieve the query data. + * @param countFunction Function to call to retrieve the total count of elements that could be potentially + * retrieved. This parameter will be null if the schema does not contain the optional + * {@code totalCount} field. * @return A paginated resolver result. * @param Type that the resolver fetches. */ @@ -56,7 +73,9 @@ public CompletableFuture> loadPaginated( getPaginatedConnection( dbFunction.callDBMethod(dslContext, connectionSelect), pageSize, - connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) ? (Integer) countFunction.callDBMethod(dslContext, Set.of()) : null + countFunction != null && connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) + ? (Integer) countFunction.callDBMethod(dslContext, Set.of()) + : null ) ); } @@ -77,7 +96,23 @@ public CompletableFuture load(K key, DBQuery dbFunction) { * Load the data for a resolver. The result is paginated. * @param pageSize Size of the pages for pagination. * @param dbFunction Function to call to retrieve the query data. - * @param countFunction Function to call to retrieve the total count of elements that could be potentially retrieved. + * @return A paginated resolver result. + */ + public CompletableFuture> loadPaginated( + K key, + int pageSize, + DBQuery>> dbFunction + ) { + return loadPaginated(key, pageSize, dbFunction, null); + } + + /** + * Load the data for a resolver. The result is paginated. + * @param pageSize Size of the pages for pagination. + * @param dbFunction Function to call to retrieve the query data. + * @param countFunction Function to call to retrieve the total count of elements that could be potentially + * retrieved. This parameter will be null if the schema does not contain the optional + * {@code totalCount} field. * @return A paginated resolver result. */ public CompletableFuture> loadPaginated( @@ -220,7 +255,9 @@ private CompletableFuture, ConnectionImpl>> getMapp getPaginatedConnection( resultAsMap(keys, dbFunction.callDBMethod(dslContext, idSet, selectionSet)), pageSize, - connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) ? countFunction.callDBMethod(dslContext, idSet) : null + countFunction != null && connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) + ? countFunction.callDBMethod(dslContext, idSet) + : null ) ); } diff --git a/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/ServiceDataFetcherHelper.java b/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/ServiceDataFetcherHelper.java index 080c835c6..389fa77eb 100644 --- a/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/ServiceDataFetcherHelper.java +++ b/graphitron-common/src/main/java/no/sikt/graphql/helpers/resolvers/ServiceDataFetcherHelper.java @@ -63,7 +63,25 @@ public CompletableFuture load(Supplier dbFunction, TransformCall Type that the query returns. + */ + public CompletableFuture> loadPaginated( + int pageSize, + Supplier>> dbFunction, + TransformCall>, List>> dbTransform + ) { + return loadPaginated(pageSize, dbFunction, null, dbTransform); + } + + /** + * Load the data for a root resolver. The result is paginated. + * @param pageSize Size of the pages for pagination. + * @param dbFunction Function to call to retrieve the query data. + * @param countFunction Function to call to retrieve the total count of elements that could be potentially + * retrieved. This parameter will be null if the schema does not contain the optional + * {@code totalCount} field. * @param dbTransform Function that maps the query output to the resolver output. * @return A paginated resolver result. * @param Type that the query returns. @@ -78,7 +96,9 @@ public CompletableFuture> loadPaginated( getPaginatedConnection( dbTransform.transform(abstractTransformer, dbFunction.get()), pageSize, - connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) ? countFunction.apply(Set.of()) : null + countFunction != null && connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) + ? countFunction.apply(Set.of()) + : null ) ); } @@ -101,7 +121,26 @@ public CompletableFuture load(K key, Function, Map * Load the data for a resolver. The result is paginated. * @param pageSize Size of the pages for pagination. * @param dbFunction Function to call to retrieve the query data. - * @param countFunction Function to call to retrieve the total count of elements that could be potentially retrieved. + * @param dbTransform Function that maps the query output to the resolver output. + * @return A paginated resolver result. + * @param Type that the query returns. + */ + public CompletableFuture> loadPaginated( + K key, + int pageSize, + Function, Map>>> dbFunction, + TransformCall>, List>> dbTransform + ) { + return loadPaginated(key, pageSize, dbFunction, null, dbTransform); + } + + /** + * Load the data for a resolver. The result is paginated. + * @param pageSize Size of the pages for pagination. + * @param dbFunction Function to call to retrieve the query data. + * @param countFunction Function to call to retrieve the total count of elements that could be potentially + * retrieved. This parameter will be null if the schema does not contain the optional + * {@code totalCount} field. * @param dbTransform Function that maps the query output to the resolver output. * @return A paginated resolver result. * @param Type that the query returns. @@ -151,7 +190,9 @@ private CompletableFuture, ConnectionImpl>> g .stream() .collect(Collectors.toMap(Map.Entry::getKey, it -> dbTransform.transform(abstractTransformer, it.getValue()))) : Map.of(), pageSize, - connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) ? countFunction.apply(idSet) : null + countFunction != null && connectionSelect.contains(CONNECTION_TOTAL_COUNT.getName()) + ? countFunction.apply(idSet) + : null ) ); } diff --git a/graphitron-common/src/main/java/no/sikt/graphql/relay/ConnectionImpl.java b/graphitron-common/src/main/java/no/sikt/graphql/relay/ConnectionImpl.java index 9f32fec90..20e9c266d 100644 --- a/graphitron-common/src/main/java/no/sikt/graphql/relay/ConnectionImpl.java +++ b/graphitron-common/src/main/java/no/sikt/graphql/relay/ConnectionImpl.java @@ -7,7 +7,11 @@ import java.util.List; /** - * Helper class for handling extended connection types based on the cursor connection specification. + * Helper class for handling extended connection types based on the + * cursor connection specification. + * The fields {@code nodes} and {@code totalCount} are added to the standard connection type. These fields are not part + * of the official specification, but are commonly used in GraphQL APIs. They are optional in this implementation and + * can be omitted if not needed. */ public class ConnectionImpl extends DefaultConnection { private final List nodes; @@ -15,7 +19,7 @@ public class ConnectionImpl extends DefaultConnection { public ConnectionImpl(List> edges, List nodes, PageInfo pageInfo, Integer totalCount) { super(edges, pageInfo); - this.nodes = nodes; + this.nodes = (nodes != null) ? nodes : List.of(); this.totalCount = totalCount; } @@ -46,7 +50,7 @@ public Builder setEdges(List> edges) { } public Builder setNodes(List nodes) { - this.nodes = nodes; + this.nodes = (nodes != null) ? nodes : List.of(); return this; } diff --git a/graphitron-example/graphitron-example-spec/pom.xml b/graphitron-example/graphitron-example-spec/pom.xml index ff52b1ca3..757d09ae2 100644 --- a/graphitron-example/graphitron-example-spec/pom.xml +++ b/graphitron-example/graphitron-example-spec/pom.xml @@ -66,6 +66,8 @@ ${project.basedir}/src/main/resources/graphql true + true + true ${generatedJooqPackage} diff --git a/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/SchemaTransformRunner.java b/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/SchemaTransformRunner.java index e3e7f828a..2f57fcc5b 100644 --- a/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/SchemaTransformRunner.java +++ b/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/SchemaTransformRunner.java @@ -95,6 +95,8 @@ private String transformAndWriteGeneratorSchema( false, // No feature flags for generator schema false, // Keep generator directives true, // Always exclude elements that the codegen should not generate. + true, + true, true); var generatorSchema = new SchemaTransformer(generatorConfig).transformSchema(); diff --git a/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/TransformPluginConfiguration.java b/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/TransformPluginConfiguration.java index 78c1a0aec..b44e36aad 100644 --- a/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/TransformPluginConfiguration.java +++ b/graphitron-maven-plugin/src/main/java/no/sikt/graphitron/mojo/TransformPluginConfiguration.java @@ -49,6 +49,16 @@ public class TransformPluginConfiguration { */ private boolean expandConnections = true; + /** + * Whether to include the {@code nodes} field in generated connection types. + */ + private boolean nodesFieldInConnectionsEnabled = true; + + /** + * Whether to include the {@code totalCount} field in generated connection types. + */ + private boolean totalCountFieldInConnectionsEnabled = true; + /** * Whether to add feature flags to the schema based on directory structure. */ @@ -120,6 +130,22 @@ public void setExpandConnections(boolean expandConnections) { this.expandConnections = expandConnections; } + public boolean isNodesFieldInConnectionsEnabled() { + return this.nodesFieldInConnectionsEnabled; + } + + public void setNodesFieldInConnectionsEnabled(boolean nodesFieldInConnectionsEnabled) { + this.nodesFieldInConnectionsEnabled = nodesFieldInConnectionsEnabled; + } + + public boolean isTotalCountFieldInConnectionsEnabled() { + return this.totalCountFieldInConnectionsEnabled; + } + + public void setTotalCountFieldInConnectionsEnabled(boolean totalCountFieldInConnectionsEnabled) { + this.totalCountFieldInConnectionsEnabled = totalCountFieldInConnectionsEnabled; + } + public boolean isAddFeatureFlags() { return addFeatureFlags; } @@ -175,6 +201,9 @@ public TransformConfig toTransformConfig(List schemaLocations, Map> getRegistryTransforms() { private List> getSchemaTransforms(TypeDefinitionRegistry registry) { var transforms = new ArrayList>(); + var disabledConnectionFields = getDisabledConnectionFields(); // This one goes first since removing fields and types allows us to not process them in later transforms. if (config.removeExcludedElements()) { @@ -71,6 +72,12 @@ private List> getSchemaTransforms(TypeDef transforms.add((s) -> new DirectivesFilter(s, filterDirectives).getModifiedGraphQLSchema()); } + if (!disabledConnectionFields.isEmpty()) { + transforms.add(schema -> new ConnectionFieldFilter(schema, disabledConnectionFields) + .getModifiedGraphQLSchema() + ); + } + return transforms; } @@ -161,4 +168,23 @@ private static GraphQLSchema assembleSchemaWithoutFederation(TypeDefinitionRegis }); return new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); } + + /** + * Determines which connection fields should be disabled based on the configuration. The {@code nodes} and + * {@code totalCount} fields are not part of the Relay Connection specification, so they can be disabled if desired. + * + * @return A set of connection field names to be disabled. + */ + private Set getDisabledConnectionFields() { + var disabledFields = new HashSet(); + + if (!config.nodesFieldInConnectionsEnabled()) { + disabledFields.add("nodes"); + } + if (!config.totalCountFieldInConnectionsEnabled()) { + disabledFields.add("totalCount"); + } + + return disabledFields; + } } diff --git a/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/TransformConfig.java b/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/TransformConfig.java index e499a10ed..9eeb93995 100644 --- a/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/TransformConfig.java +++ b/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/TransformConfig.java @@ -11,7 +11,9 @@ public record TransformConfig( boolean addFeatureFlags, boolean removeGeneratorDirectives, boolean removeExcludedElements, - boolean expandConnections + boolean expandConnections, + boolean nodesFieldInConnectionsEnabled, + boolean totalCountFieldInConnectionsEnabled ) { public static Set DIRECTIVES_FOR_REMOVING_ELEMENTS = Set.of("external", "notGenerated", "requires"); } diff --git a/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionFieldFilter.java b/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionFieldFilter.java new file mode 100644 index 000000000..6e535ebef --- /dev/null +++ b/graphitron-schema-transform/src/main/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionFieldFilter.java @@ -0,0 +1,48 @@ +package no.fellesstudentsystem.schema_transformer.transform; + +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldsContainer; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.SchemaTransformer; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; + +import java.util.Set; + +import static graphql.util.TraversalControl.CONTINUE; + +public class ConnectionFieldFilter implements ModifyingGraphQLTypeVisitor { + private final GraphQLSchema schema; + private final Set disabledFieldNames; + + public ConnectionFieldFilter(GraphQLSchema schema, Set disabledFieldNames) { + this.schema = schema; + this.disabledFieldNames = disabledFieldNames; + } + + @Override + public GraphQLSchema getModifiedGraphQLSchema() { + GraphQLTypeVisitorStub visitor = new GraphQLTypeVisitorStub() { + @Override + public TraversalControl visitGraphQLFieldDefinition( + GraphQLFieldDefinition node, + TraverserContext context + ) { + GraphQLSchemaElement parent = context.getParentNode(); + + if (parent instanceof GraphQLFieldsContainer fieldsContainer) { + if (fieldsContainer.getName().endsWith("Connection") && + disabledFieldNames.contains(node.getName())) { + return deleteNode(context); + } + } + + return CONTINUE; + } + }; + + return SchemaTransformer.transformSchema(schema, visitor); + } +} diff --git a/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/AbstractTest.java b/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/AbstractTest.java index 04cb261b6..1503ed30d 100644 --- a/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/AbstractTest.java +++ b/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/AbstractTest.java @@ -92,6 +92,41 @@ protected void assertTransformedSchemaMatches(GraphQLSchema generatedSchema, Gra .isZero(); } + protected void assertTransformedSchemaExactlyMatches(GraphQLSchema generatedSchema, GraphQLSchema expectedSchema) { + CapturingReporter schemaDiffReporter = new CapturingReporter(); + new SchemaDiff(SchemaDiff.Options.defaultOptions().enforceDirectives()) + .diffSchema(SchemaDiffSet.diffSetFromSdl(expectedSchema, generatedSchema), schemaDiffReporter); + + // SchemaDiff emits a lot of INFO events that are just traversal/trace output ("Examining ..."). + // Those aren't actionable schema differences, so we ignore them and only fail on meaningful INFO events. + // Note: Some versions include leading whitespace (tabs/newlines) before "Examining", so we treat any + // whitespace prefix as trace. + var traceInfoEvents = schemaDiffReporter.getInfos().stream() + .filter(it -> it.toString().matches("(?s).*reasonMsg='\\s*Examining\\b.*")) + .toList(); + var meaningfulInfoEvents = schemaDiffReporter.getInfos().stream() + .filter(it -> !it.toString().matches("(?s).*reasonMsg='\\s*Examining\\b.*")) + .toList(); + + String diffEventsReport = Stream.of( + schemaDiffReporter.getDangers().stream(), + schemaDiffReporter.getBreakages().stream(), + meaningfulInfoEvents.stream() + ) + .flatMap(s -> s) + .map(DiffEvent::toString) + .collect(Collectors.joining(",\n ")); + + assertThat(schemaDiffReporter.getDangerCount() + schemaDiffReporter.getBreakageCount() + + meaningfulInfoEvents.size()) + .as( + "Found the following differences between the schemas (ignored %s INFO trace events):\n%s", + traceInfoEvents.size(), + diffEventsReport + ) + .isZero(); + } + protected static List findSchemas(Set parentFolders) { return SchemaReader.findSchemaFilesRecursivelyInDirectory(parentFolders.stream().map(it -> SRC_TEST_RESOURCES + it).collect(Collectors.toSet())); } diff --git a/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionTest.java b/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionTest.java index 4cedb10f8..3753e7145 100644 --- a/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionTest.java +++ b/graphitron-schema-transform/src/test/java/no/fellesstudentsystem/schema_transformer/transform/ConnectionTest.java @@ -1,5 +1,7 @@ package no.fellesstudentsystem.schema_transformer.transform; +import no.fellesstudentsystem.schema_transformer.SchemaTransformer; +import no.fellesstudentsystem.schema_transformer.TransformConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -31,4 +33,69 @@ void connectionExists() { var expected = makeExpectedSchema(path); assertTransformedSchemaMatches(schema, expected); } + + @Test + @DisplayName("Includes all optional connection fields by default") + void shouldIncludeAllOptionalFieldsByDefault() { + var path = "optionalConnectionFields/allDefault"; + var transformConfig = createAndInitializeTransformConfig( + path + "/schema", + true, + true + ); + var schemaTransformer = new SchemaTransformer(transformConfig); + var transformedSchema = schemaTransformer.transformSchema(); + var expectedSchema = makeExpectedSchema(path); + assertTransformedSchemaExactlyMatches(transformedSchema, expectedSchema); + } + + @Test + @DisplayName("Excludes all optional connection fields when explicitly disabled") + void shouldExcludeAllOptionalFieldsWhenExplicitlyDisabled() { + var path = "optionalConnectionFields/allDisabled"; + var transformConfig = createAndInitializeTransformConfig( + path + "/schema", + false, + false + ); + var schemaTransformer = new SchemaTransformer(transformConfig); + var transformedSchema = schemaTransformer.transformSchema(); + var expectedSchema = makeExpectedSchema(path); + assertTransformedSchemaExactlyMatches(transformedSchema, expectedSchema); + } + + @Test + @DisplayName("Includes optional connection fields by default and correctly excludes those explicitly disabled") + void shouldHandleDefaultAndExplicitlyDisabledOptionalFields() { + var path = "optionalConnectionFields/defaultAndDisabled"; + var transformConfig = createAndInitializeTransformConfig( + path + "/schema", + false, + true + ); + var schemaTransformer = new SchemaTransformer(transformConfig); + var transformedSchema = schemaTransformer.transformSchema(); + var expectedSchema = makeExpectedSchema(path); + assertTransformedSchemaExactlyMatches(transformedSchema, expectedSchema); + } + + private TransformConfig createAndInitializeTransformConfig( + String testPath, + boolean nodesFieldInConnectionsEnabled, + boolean totalCountFieldInConnectionsEnabled + ) { + var schemaPath = findSchemas(testPath); + + return new TransformConfig( + schemaPath, + Set.of(), + null, + false, + true, + true, + true, + nodesFieldInConnectionsEnabled, + totalCountFieldInConnectionsEnabled + ); + } } diff --git a/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDefault/expected/schema.graphql b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDefault/expected/schema.graphql new file mode 100644 index 000000000..6f1403702 --- /dev/null +++ b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDefault/expected/schema.graphql @@ -0,0 +1,29 @@ +type Query { + someType( + param: String!, + ): QuerySomeTypeConnection +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String +} + +type QuerySomeTypeConnection { + edges: [QuerySomeTypeConnectionEdge] + pageInfo: PageInfo + nodes: [SomeType] + totalCount: Int +} + +type QuerySomeTypeConnectionEdge { + cursor: String + node: SomeType +} + +type SomeType { + id: ID! + field: String! +} diff --git a/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDefault/schema/schema.graphql b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDefault/schema/schema.graphql new file mode 100644 index 000000000..3a6ef2cba --- /dev/null +++ b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDefault/schema/schema.graphql @@ -0,0 +1,12 @@ +directive @asConnection(defaultFirstValue: Int = 100) on FIELD_DEFINITION + +type Query { + someType( + param: String!, + ): [SomeType] @asConnection +} + +type SomeType { + id: ID! + field: String! +} diff --git a/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDisabled/expected/schema.graphql b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDisabled/expected/schema.graphql new file mode 100644 index 000000000..0dfc985eb --- /dev/null +++ b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDisabled/expected/schema.graphql @@ -0,0 +1,27 @@ +type Query { + someType( + param: String!, + ): QuerySomeTypeConnection +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String +} + +type QuerySomeTypeConnection { + edges: [QuerySomeTypeConnectionEdge] + pageInfo: PageInfo +} + +type QuerySomeTypeConnectionEdge { + cursor: String + node: SomeType +} + +type SomeType { + id: ID! + field: String! +} diff --git a/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDisabled/schema/schema.graphql b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDisabled/schema/schema.graphql new file mode 100644 index 000000000..3a6ef2cba --- /dev/null +++ b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/allDisabled/schema/schema.graphql @@ -0,0 +1,12 @@ +directive @asConnection(defaultFirstValue: Int = 100) on FIELD_DEFINITION + +type Query { + someType( + param: String!, + ): [SomeType] @asConnection +} + +type SomeType { + id: ID! + field: String! +} diff --git a/graphitron-schema-transform/src/test/resources/optionalConnectionFields/defaultAndDisabled/expected/schema.graphql b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/defaultAndDisabled/expected/schema.graphql new file mode 100644 index 000000000..4481aa0ab --- /dev/null +++ b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/defaultAndDisabled/expected/schema.graphql @@ -0,0 +1,28 @@ +type Query { + someType( + param: String!, + ): QuerySomeTypeConnection +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String +} + +type QuerySomeTypeConnection { + edges: [QuerySomeTypeConnectionEdge] + pageInfo: PageInfo + totalCount: Int +} + +type QuerySomeTypeConnectionEdge { + cursor: String + node: SomeType +} + +type SomeType { + id: ID! + field: String! +} diff --git a/graphitron-schema-transform/src/test/resources/optionalConnectionFields/defaultAndDisabled/schema/schema.graphql b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/defaultAndDisabled/schema/schema.graphql new file mode 100644 index 000000000..3a6ef2cba --- /dev/null +++ b/graphitron-schema-transform/src/test/resources/optionalConnectionFields/defaultAndDisabled/schema/schema.graphql @@ -0,0 +1,12 @@ +directive @asConnection(defaultFirstValue: Int = 100) on FIELD_DEFINITION + +type Query { + someType( + param: String!, + ): [SomeType] @asConnection +} + +type SomeType { + id: ID! + field: String! +}