diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/typeresolvers/TypeNameMethodGenerator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/typeresolvers/TypeNameMethodGenerator.java index 04d3c4430..7b12b131c 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/typeresolvers/TypeNameMethodGenerator.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/datafetchers/typeresolvers/TypeNameMethodGenerator.java @@ -9,7 +9,7 @@ import javax.lang.model.element.Modifier; import java.util.List; -import java.util.Set; +import java.util.Optional; import static no.sikt.graphitron.generators.codebuilding.FormatCodeBlocks.returnWrap; import static no.sikt.graphitron.generators.codebuilding.VariableNames.VAR_OBJECT; @@ -27,7 +27,7 @@ public TypeNameMethodGenerator(TypeResolverTarget localObject, ProcessedSchema p @Override public MethodSpec generate(TypeResolverTarget target) { - var components = getComponents(target).stream().map(TypeNameMethodGenerator::ifStatement).collect(CodeBlock.joining("\n")); + var components = getComponents(target).orElse(List.of()).stream().map(TypeNameMethodGenerator::ifStatement).collect(CodeBlock.joining("\n")); return getDefaultSpecBuilder(METHOD_NAME, STRING.className) .addModifiers(Modifier.STATIC) .addParameter(OBJECT.className, VAR_OBJECT) @@ -36,11 +36,11 @@ public MethodSpec generate(TypeResolverTarget target) { .build(); } - private Set getComponents(TypeResolverTarget target) { + private Optional> getComponents(TypeResolverTarget target) { if (processedSchema.isInterface(target.getName()) || processedSchema.isUnion(target.getName())) { return processedSchema.getTypesFromInterfaceOrUnion(target.getName()); } - return Set.of(); + return Optional.empty(); } private static CodeBlock ifStatement(ObjectDefinition obj) { 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..c5655e30f 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 @@ -80,7 +80,7 @@ public MethodSpec generate(ObjectField target) { } private CodeBlock getCodeForMultitableCountMethod(ObjectField target) { - var implementations = processedSchema.getTypesFromInterfaceOrUnion(target.getTypeName()); + var implementations = processedSchema.getTypesFromInterfaceOrUnion(target.getTypeName()).orElse(List.of()); var aliasSet = new LinkedHashSet(); var codeForImplementations = CodeBlock.builder(); diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchMultiTableDBMethodGenerator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchMultiTableDBMethodGenerator.java index 445d7823c..1c119c60b 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchMultiTableDBMethodGenerator.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchMultiTableDBMethodGenerator.java @@ -31,7 +31,6 @@ import static no.sikt.graphitron.mappings.JavaPoetClassName.*; import static no.sikt.graphitron.mappings.JavaPoetClassName.SELECT_JOIN_STEP; import static no.sikt.graphitron.mappings.TableReflection.*; -import static no.sikt.graphitron.validation.ValidationHandler.addErrorMessageAndThrow; import static no.sikt.graphql.naming.GraphQLReservedName.*; public class FetchMultiTableDBMethodGenerator extends FetchDBMethodGenerator { @@ -60,7 +59,7 @@ public MethodSpec generate(ObjectField target) { var unionOrInterfaceDefinition = processedSchema.isUnion(target) ? processedSchema.getUnion(target) : processedSchema.getInterface(target); // Order is important for paginated queries as it gets data fields by index in the mapping - LinkedHashSet implementations = new LinkedHashSet<>(processedSchema.getTypesFromInterfaceOrUnion(unionOrInterfaceDefinition.getName())); + var implementations = processedSchema.getTypesFromInterfaceOrUnion(unionOrInterfaceDefinition.getName()).orElse(List.of()); return getSpecBuilder(target, unionOrInterfaceDefinition.getGraphClassName(), inputParser) .addCode(implementations.isEmpty() ? returnWrap("null") : getCode(target, implementations, inputParser.getMethodInputNames(false, false, true))) @@ -72,7 +71,7 @@ protected CodeBlock getHelperMethodCallForNestedField(ObjectField field, FetchCo return null; } - private CodeBlock getCode(ObjectField target, LinkedHashSet implementations, List inputs) { + private CodeBlock getCode(ObjectField target, List implementations, List inputs) { List sortFieldQueryMethodCalls = new ArrayList<>(); LinkedHashMap mappedQueryVariables = new LinkedHashMap<>(); var joins = CodeBlock.builder(); @@ -87,9 +86,6 @@ private CodeBlock getCode(ObjectField target, LinkedHashSet im } for (var implementation : implementations) { - if (!implementation.hasTable()) { - addErrorMessageAndThrow("Type '%s' is returned in an interface query, but not have table set. This is not supported.", implementation.getName()); - } String typeName = implementation.getName(); sortFieldQueryMethodCalls.add(getSortFieldsMethodName(target, implementation)); String mappedVariableName = joinStepPrefix(typeName); @@ -153,7 +149,7 @@ private CodeBlock getCode(ObjectField target, LinkedHashSet im .build(); } - private static @NotNull CodeBlock getTokenVariableDeclaration(Set implementations) { + private static @NotNull CodeBlock getTokenVariableDeclaration(List implementations) { var map = mapOfEntries( indentIfMultiline( implementations.stream() @@ -226,7 +222,7 @@ private CodeBlock getFetchCodeBlock(ObjectField target) { RECORD1.className); } - private CodeBlock createMappingContent(GenerationSourceField target, LinkedHashSet implementations, boolean isConnection) { + private CodeBlock createMappingContent(GenerationSourceField target, List implementations, boolean isConnection) { var interfaceClassName = processedSchema.getRecordType(target).getGraphClassName(); var lambdaParameters = new LinkedHashMap(); @@ -279,7 +275,7 @@ public List generateWithSubselectMethods(ObjectField target) { return Stream.concat( Stream.of(mainMethod), processedSchema - .getTypesFromInterfaceOrUnion(unionOrInterfaceDefinition.getName()) + .getTypesFromInterfaceOrUnion(unionOrInterfaceDefinition.getName()).orElse(List.of()) .stream() .map(it -> getMethodsForImplementation(target, it, methodInputs)) .flatMap(Collection::stream) diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchSingleTableInterfaceDBMethodGenerator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchSingleTableInterfaceDBMethodGenerator.java index 851f79294..8d9fdd59f 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchSingleTableInterfaceDBMethodGenerator.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/generators/db/FetchSingleTableInterfaceDBMethodGenerator.java @@ -23,8 +23,8 @@ import static no.sikt.graphitron.generators.codebuilding.FormatCodeBlocks.indentIfMultiline; import static no.sikt.graphitron.generators.codebuilding.KeyWrapper.findKeyForResolverField; import static no.sikt.graphitron.generators.codebuilding.KeyWrapper.getKeySetForResolverFields; -import static no.sikt.graphitron.generators.codebuilding.VariableNames.VAR_ORDER_FIELDS; import static no.sikt.graphitron.generators.codebuilding.VariableNames.VAR_ITERATOR; +import static no.sikt.graphitron.generators.codebuilding.VariableNames.VAR_ORDER_FIELDS; import static no.sikt.graphitron.generators.codebuilding.VariablePrefix.internalPrefix; import static no.sikt.graphitron.mappings.JavaPoetClassName.*; import static no.sikt.graphql.naming.GraphQLReservedName.NODE_TYPE; @@ -57,7 +57,7 @@ public MethodSpec generate(ObjectField target) { .build(); } - private CodeBlock getCode(ObjectField target, Set implementations) { + private CodeBlock getCode(ObjectField target, List implementations) { var context = new FetchContext(processedSchema, target, getLocalObject(), false); var overriddenFields = getFieldsOverriddenByType(processedSchema.getInterface(target), implementations); var selectCode = generateSelectRow(context, target, implementations, overriddenFields); @@ -85,7 +85,7 @@ protected CodeBlock getHelperMethodCallForNestedField(ObjectField field, FetchCo return null; } - private @NotNull HashMap> getFieldsOverriddenByType(InterfaceDefinition targetInterface, Set implementations) { + private @NotNull HashMap> getFieldsOverriddenByType(InterfaceDefinition targetInterface, List implementations) { HashMap> overriddenFields = new HashMap<>(); // Find overridden interface fields @@ -102,10 +102,9 @@ protected CodeBlock getHelperMethodCallForNestedField(ObjectField field, FetchCo ); // Find overridden non-interface fields - var implementationList = new ArrayList<>(implementations); - for (int i = 0; i < implementationList.size(); i++) { - for (int j = i + 1; j < implementationList.size(); j++) { - compareNonInterfaceFields(implementationList.get(i), implementationList.get(j), targetInterface, overriddenFields); + for (int i = 0; i < implementations.size(); i++) { + for (int j = i + 1; j < implementations.size(); j++) { + compareNonInterfaceFields(implementations.get(i), implementations.get(j), targetInterface, overriddenFields); } } return overriddenFields; @@ -130,7 +129,7 @@ private boolean hasDifferentFieldConfiguration(ObjectField fieldA, ObjectField f return fieldDirectiveDiffers || isNodeIdField; } - protected CodeBlock generateSelectRow(FetchContext context, ObjectField target, Set implementations, HashMap> overriddenFields) { + protected CodeBlock generateSelectRow(FetchContext context, ObjectField target, List implementations, HashMap> overriddenFields) { List allFields = new LinkedList<>(); context.getReferenceObject() @@ -183,7 +182,7 @@ protected CodeBlock generateSelectRow(FetchContext context, ObjectField target, return CodeBlock.join(rowElements, ",\n"); } - private CodeBlock fetchAndMap(ObjectField target, Set implementations, String querySource, HashMap> overriddenFields, FetchContext context) { + private CodeBlock fetchAndMap(ObjectField target, List implementations, String querySource, HashMap> overriddenFields, FetchContext context) { var interfaceDefinition = processedSchema.getInterface(target); var returnInsideIfBlock = !target.hasForwardPagination(); diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ErrorMessages.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ErrorMessages.java new file mode 100644 index 000000000..0580e0eb1 --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ErrorMessages.java @@ -0,0 +1,21 @@ +package no.sikt.graphitron.validation; + +public enum ErrorMessages { + MISSING_FIELD("Input type %s referencing table %s does not map all fields required by the database. Missing required fields: %s"), + MISSING_NON_NULLABLE("Input type %s referencing table %s does not map all fields required by the database as non-nullable. Nullable required fields: %s"), + MISSING_TABLE_ON_MULTITABLE("Type(s) '%s' are used in a query %s returning multitable interface or union '%s', but do not have tables set. This is not supported."); + + private final String msg; + + ErrorMessages(String msg) { + this.msg = msg; + } + + public String getMsg() { + return msg; + } + + public String format(Object ...input) { + return String.format(msg, input); + } +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ProcessedDefinitionsValidator.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ProcessedDefinitionsValidator.java index 63e9e2a73..a39e6e9ba 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ProcessedDefinitionsValidator.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ProcessedDefinitionsValidator.java @@ -41,6 +41,7 @@ import static no.sikt.graphitron.mappings.JavaPoetClassName.STRING; import static no.sikt.graphitron.mappings.ReflectionHelpers.getJooqRecordClassForNodeIdInputField; import static no.sikt.graphitron.mappings.TableReflection.*; +import static no.sikt.graphitron.validation.ErrorMessages.*; import static no.sikt.graphitron.validation.ValidationHandler.*; import static no.sikt.graphql.directives.GenerationDirective.*; import static no.sikt.graphql.directives.GenerationDirective.NODE_ID; @@ -56,9 +57,6 @@ public class ProcessedDefinitionsValidator { protected final ProcessedSchema schema; private final List allFields; - protected static final String - ERROR_MISSING_FIELD = "Input type %s referencing table %s does not map all fields required by the database. Missing required fields: %s", - ERROR_MISSING_NON_NULLABLE = "Input type %s referencing table %s does not map all fields required by the database as non-nullable. Nullable required fields: %s"; public ProcessedDefinitionsValidator(ProcessedSchema schema) { this.schema = schema; @@ -79,9 +77,9 @@ public void validateDirectiveUsage() { // Validate cycles first to ensure there are no infinite loops later validateNoCycles(); + validateMultitableTypeTables(); validateTablesAndKeys(); validateRequiredTableFields(); - validateUnionFieldsTable(); validateSingleTableInterfaceDefinitions(); validateInterfacesReturnedInFields(); validateMultitableFieldsOutsideRoot(); @@ -112,7 +110,7 @@ public void validateDirectiveUsage() { } /* - * This is a temporary validation until GGG-104 has been fixed. + * This is a temporary validation until GG-104 has been fixed. */ private void validateUnionAndInterfaceSubTypes() { schema.getObjects() @@ -123,7 +121,7 @@ private void validateUnionAndInterfaceSubTypes() { .filter(field -> !field.getTypeName().equals(FEDERATION_SERVICE_TYPE.getName())) .filter(field -> !field.getTypeName().equals(FEDERATION_ENTITY_UNION.getName())) .forEach(field -> { - var subTypes = schema.getTypesFromInterfaceOrUnion(field.getTypeName()); + var subTypes = schema.getTypesFromInterfaceOrUnion(field.getTypeName()).orElse(List.of()); if (subTypes.size() < 2 && subTypes.stream().noneMatch(type -> type.implementsInterface(ERROR_TYPE.getName()))) { addErrorMessage( "Multitable queries is currently only supported for interface and unions with more than one implementing type. \n" + @@ -443,7 +441,7 @@ private void validateTablesAndKeys() { .filter(it -> !schema.isMultiTableInterface(it.getName())) // Don't validate fields on the interface definition since the interface itself has no table .flatMap(it -> it.getFields().stream()) .flatMap(it -> schema.isMultiTableField(it) - ? schema.getTypesFromInterfaceOrUnion(it.getTypeName()).stream().map(o -> new VirtualSourceField(o.getName(), (ObjectField) it)) + ? schema.getTypesFromInterfaceOrUnion(it.getTypeName()).orElse(List.of()).stream().map(o -> new VirtualSourceField(o.getName(), (ObjectField) it)) : Stream.of(it)) .filter(field -> !field.hasServiceReference()) .filter(field -> schema.isRecordType(field.getTypeName()) || field.hasFieldReferences() || field.isResolver()) @@ -740,24 +738,6 @@ private void validateServiceMethods() { .forEach(it -> addErrorMessage("Service reference with name '%s' does not contain a method named '%s'.", referenceSet.getClassFrom(it), it.getMethodName())); } - private void validateUnionFieldsTable() { - if (schema.getObjects().containsKey("Query")) { - for (ObjectField field : schema.getObject("Query").getFields()) { - if (field.getName().equals(FEDERATION_ENTITIES_FIELD.getName())) { - continue; - } - - if (schema.isUnion(field)) { - for (ObjectDefinition subType : schema.getUnionSubTypes(field.getTypeName())) { - if (!subType.hasTable()) { - addErrorMessage("Type %s in Union '%s' in Query has no table.", subType.getName(), schema.getUnion(field).getName()); - } - } - } - } - } - } - private void validateMultitableFieldsOutsideRoot() { allFields.stream() .filter(GenerationSourceField::isGenerated) @@ -780,14 +760,38 @@ private void validateMultitableFieldsOutsideRoot() { }); } + private void validateMultitableTypeTables() { + schema + .getObjects() + .values() + .stream() + .filter(Objects::nonNull) + .flatMap(it -> it.getFields().stream()) + .filter(schema::isMultiTableField) + .filter(it -> it.isResolver() || it.isRootField()) + .filter(it -> !it.hasServiceReference()) + .filter(it -> !it.getName().equals(FEDERATION_ENTITIES_FIELD.getName())) + .filter(it -> !it.getTypeName().equals(NODE_TYPE.getName())) + .filter(it -> !it.getTypeName().equals(ERROR_TYPE.getName())) + .forEach((field) -> { + var multitableName = field.getTypeName(); + var typesMissingTable = schema + .getTypesFromInterfaceOrUnion(multitableName).orElse(List.of()) + .stream() + .filter(it -> !it.hasTable()) + .map(AbstractObjectDefinition::getName) + .collect(Collectors.joining("', '")); + if (!typesMissingTable.isEmpty()) { + addErrorMessage(MISSING_TABLE_ON_MULTITABLE.getMsg(), typesMissingTable, field.formatPath(), field.getTypeName()); + } + }); + } + private void validateSingleTableInterfaceDefinitions() { schema.getInterfaces().forEach((name, interfaceDefinition) -> { if (name.equalsIgnoreCase(NODE_TYPE.getName()) || name.equalsIgnoreCase(ERROR_TYPE.getName())) return; - var implementations = schema.getObjects() - .values() - .stream() - .filter(it -> it.implementsInterface(schema.getInterface(name).getName())).toList(); + var implementations = schema.getTypesFromInterfaceOrUnion(interfaceDefinition).orElse(List.of()); if (interfaceDefinition.hasDiscriminator() == interfaceDefinition.isMultiTableInterface()) { addErrorMessage( @@ -928,7 +932,8 @@ private void validateInterfacesReturnedInFields() { .ofNullable(schema.getObjectOrConnectionNode(typeName)) .map(AbstractObjectDefinition::getName) .orElse(typeName); - if (!field.isRootField() && (schema.isSingleTableInterface(field) || name.equals(NODE_TYPE.getName()))) { + var isSingleTable = schema.isSingleTableInterface(field); + if (!field.isRootField() && (isSingleTable || name.equals(NODE_TYPE.getName()))) { addErrorMessage("interface (%s) returned in non root object. This is not fully " + "supported. Use with care", name); } @@ -950,16 +955,15 @@ private void validateInterfacesReturnedInFields() { .stream() .filter(it -> it.implementsInterface(schema.getInterface(name).getName())) .forEach(implementation -> { - if (!implementation.hasTable()) { + if (!implementation.hasTable() && isSingleTable) { addErrorMessage("Interface '%s' is returned in field '%s', but type '%s' " + "implementing '%s' does not have table set. This is not supported.", name, field.getName(), implementation.getName(), name); - } else if (!tableHasPrimaryKey(implementation.getTable().getName())) { + } else if (implementation.hasTable() && !tableHasPrimaryKey(implementation.getTable().getName())) { addErrorMessage("Interface '%s' is returned in field '%s', but implementing type '%s' " + "has table '%s' which does not have a primary key. This is not supported.", name, field.getName(), implementation.getName(), implementation.getTable().getName()); } }); } - } ); } @@ -1417,7 +1421,7 @@ protected void checkRequiredFields(InputField recordInput) { .stream() .map(InputField::getUpperCaseName) .collect(Collectors.toSet()); - checkRequiredFieldsExist(recordFieldNames, requiredDBFields, recordInput, ERROR_MISSING_FIELD); + checkRequiredFieldsExist(recordFieldNames, requiredDBFields, recordInput, MISSING_FIELD.getMsg()); var requiredRecordFieldNames = inputObject.getFields() @@ -1425,7 +1429,7 @@ protected void checkRequiredFields(InputField recordInput) { .filter(AbstractField::isNonNullable) .map(InputField::getUpperCaseName) .collect(Collectors.toSet()); - checkRequiredFieldsExist(requiredRecordFieldNames, requiredDBFields, recordInput, ERROR_MISSING_NON_NULLABLE); + checkRequiredFieldsExist(requiredRecordFieldNames, requiredDBFields, recordInput, MISSING_NON_NULLABLE.getMsg()); } protected void checkRequiredFieldsExist(Set actualFields, List requiredFields, InputField recordInput, String message) { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ValidationHandler.java b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ValidationHandler.java index c0409ef94..e9986a492 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ValidationHandler.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/main/java/no/sikt/graphitron/validation/ValidationHandler.java @@ -55,7 +55,7 @@ public static void logWarnings() { /** * Throws an exception if any error has previously been reported. */ - public static void throwIfErrors () { + public static void throwIfErrors() { if (!errorMessages.isEmpty()) { throw new InvalidSchemaException("Problems have been found that prevent code generation: \n" + String.join("\n", errorMessages)); } 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 8fb9d2275..3946b59cc 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 @@ -357,61 +357,52 @@ public InterfaceDefinition getInterface(FieldSpecification field) { /** * @return Get the implementations for an interface given its name */ - public Set getImplementationsForInterface(String interfaceName) { + public List getImplementationsForInterface(String interfaceName) { return getObjects() .values() .stream() .filter(it -> it.implementsInterface(interfaceName)) - .collect(Collectors.toSet()); + .toList(); } /** * @return Get the implementations for an interface */ - public Set getImplementationsForInterface(InterfaceDefinition interfaceDefinition) { + public List getImplementationsForInterface(InterfaceDefinition interfaceDefinition) { return getImplementationsForInterface(interfaceDefinition.getName()); } /** * @return Get the ObjectDefinition for each Type in a Union given its name */ - public Set getUnionSubTypes(String objectName) { + public List getUnionSubTypes(String objectName) { return getUnion(objectName). getFieldTypeNames() .stream() .map(this::getObjectOrConnectionNode) - .collect(Collectors.toSet()); + .toList(); } - /* + /** * Returns the ObjectDefinition for each Type in a union given that the name * supplied is a union. Otherwise, return the ObjectDefinition for all types * that implements the given interface. * */ - - public Set getTypesFromInterfaceOrUnion(String name) { + public Optional> getTypesFromInterfaceOrUnion(String name) { if (isUnion(name)) { - return getUnionSubTypes(isConnectionObject(name) ? getConnectionObject(name).getNodeType() : name); + return Optional.of(getUnionSubTypes(isConnectionObject(name) ? getConnectionObject(name).getNodeType() : name)); } if (isInterface(name)) { - return getImplementationsForInterface(isConnectionObject(name) ? getConnectionObject(name).getNodeType() : name); + return Optional.of(getImplementationsForInterface(isConnectionObject(name) ? getConnectionObject(name).getNodeType() : name)); } - return null; - } - - /* - * Returns the ObjectDefinition for each type implementing an interface given its interfaceTypeDefinition - * */ - public Set getTypesFromInterfaceOrUnion(InterfaceDefinition interfaceDefinition) { - return getTypesFromInterfaceOrUnion(interfaceDefinition.getName()); + return Optional.empty(); } - - /* - * Returns the ObjectDefinition for each type in a union given its unionTypeDefinition - * */ - public Set getTypesFromInterfaceOrUnion(UnionDefinition unionDefinition) { - return getTypesFromInterfaceOrUnion(unionDefinition.getName()); + /** + * Returns the ObjectDefinition for each type implementing an interface or union given its definition + */ + public Optional> getTypesFromInterfaceOrUnion(RecordObjectSpecification definition) { + return getTypesFromInterfaceOrUnion(definition.getName()); } /** diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/UnionTest.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/UnionTest.java index ca614701a..392cd9380 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/UnionTest.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/queries/fetch/UnionTest.java @@ -10,7 +10,8 @@ import java.util.List; import java.util.Set; -import static no.sikt.graphitron.common.configuration.SchemaComponent.*; +import static no.sikt.graphitron.common.configuration.SchemaComponent.PAGE_INFO; +import static no.sikt.graphitron.common.configuration.SchemaComponent.SOMEUNION_CONNECTION; @DisplayName("Query outputs - Union types") public class UnionTest extends GeneratorTest { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/MultitableTest.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/MultitableTest.java index bbcf4ea35..c25b4bfb0 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/MultitableTest.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/MultitableTest.java @@ -1,11 +1,11 @@ package no.sikt.graphitron.validation; -import no.sikt.graphitron.common.configuration.SchemaComponent; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.Set; +import static no.sikt.graphitron.common.configuration.SchemaComponent.CUSTOMER; import static no.sikt.graphitron.common.configuration.SchemaComponent.PERSON_WITH_EMAIL; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -87,4 +87,40 @@ void splitQueryRequiredOnMultitableFieldOutsideRoot() { "Multitable queries outside root is only supported for resolver fields" ); } + + @Test + @DisplayName("Type implements multitable interface but has no table") + void implementationWithoutTable() { + assertErrorsContain( + () -> getProcessedSchema("implementationWithoutTable"), + String.format("Type(s) '%s' are used in a query '%s' returning multitable interface or union '%s', but do not have tables set. This is not supported.", "Customer", "Query.someInterface", "SomeInterface") + ); + } + + @Test + @DisplayName("Two types implement multitable interface but have no tables") + void twoImplementationsWithoutTable() { + assertErrorsContain( + () -> getProcessedSchema("twoImplementationsWithoutTable"), + String.format("Type(s) '%s' are used in a query '%s' returning multitable interface or union '%s', but do not have tables set. This is not supported.", "Address', 'Customer", "Query.someInterface", "SomeInterface") + ); + } + + @Test + @DisplayName("Union contains type without table") + void typeWithoutTableInUnion() { + assertErrorsContain( + () -> getProcessedSchema("typeWithoutTableInUnion", Set.of(CUSTOMER)), + String.format("Type(s) '%s' are used in a query '%s' returning multitable interface or union '%s', but do not have tables set. This is not supported.", "Customer", "Query.someUnion", "SomeUnion") + ); + } + + @Test + @DisplayName("Union contains two types without tables") + void twoTypesWithoutTableInUnion() { + assertErrorsContain( + () -> getProcessedSchema("twoTypesWithoutTableInUnion", Set.of(CUSTOMER)), + String.format("Type(s) '%s' are used in a query '%s' returning multitable interface or union '%s', but do not have tables set. This is not supported.", "Customer', 'Address", "Query.someUnion", "SomeUnion") + ); + } } diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/QueryTest.java b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/QueryTest.java index 4aa85c8eb..fca527136 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/QueryTest.java +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/java/no/sikt/graphitron/validation/QueryTest.java @@ -28,8 +28,6 @@ void noPaginationFields() { ); } - - @Test @DisplayName("Has connection but misses some of the pagination inputs") void incompletePaginationFields() { @@ -41,15 +39,6 @@ void incompletePaginationFields() { ); } - @Test - @DisplayName("Query of Union whose subtype lacks table") - void unionSubTypeNoTable() { - assertErrorsContain( - () -> getProcessedSchema("unionSubTypeNoTable", Set.of(SOMEUNION_CONNECTION, PAGE_INFO)), - "Type Staff in Union 'SomeUnion' in Query has no table." - ); - } - @Test @DisplayName("Query with lookup keys set") void lookupAndOrderBy() { @@ -187,16 +176,6 @@ void listedOptionalInput() { ); } - @Test - @DisplayName("Query on root with an interface implementation without table set") - void interfaceWithoutTableFromRoot() { - assertErrorsContain("interfaceWithoutTableFromRoot", - "Interface 'SomeInterface' is returned in field 'someInterface', but " + - "type 'Customer' implementing 'SomeInterface' does not have table set. This is not supported." - ); - } - - @Test @DisplayName("Interface with one implementing type validation.") void interfaceWithOneSubtype() { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/query/interfaceWithoutTableFromRoot/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/implementationWithoutTable/schema.graphqls similarity index 76% rename from graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/query/interfaceWithoutTableFromRoot/schema.graphqls rename to graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/implementationWithoutTable/schema.graphqls index d54ed2ac6..57471c76a 100644 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/query/interfaceWithoutTableFromRoot/schema.graphqls +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/implementationWithoutTable/schema.graphqls @@ -1,5 +1,5 @@ type Query { - someInterface: [SomeInterface] + someInterface: SomeInterface } interface SomeInterface { diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/twoImplementationsWithoutTable/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/twoImplementationsWithoutTable/schema.graphqls new file mode 100644 index 000000000..9ba68a24c --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/twoImplementationsWithoutTable/schema.graphqls @@ -0,0 +1,15 @@ +type Query { + someInterface: SomeInterface +} + +interface SomeInterface { + id: ID +} + +type Customer implements SomeInterface { + id: ID +} + +type Address implements SomeInterface { + id: ID +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/twoTypesWithoutTableInUnion/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/twoTypesWithoutTableInUnion/schema.graphqls new file mode 100644 index 000000000..e8433deb9 --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/twoTypesWithoutTableInUnion/schema.graphqls @@ -0,0 +1,9 @@ +type Query { + someUnion: SomeUnion +} + +union SomeUnion = Customer | Address + +type Address { + id: ID +} diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/typeWithoutTableInUnion/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/typeWithoutTableInUnion/schema.graphqls new file mode 100644 index 000000000..c568d39de --- /dev/null +++ b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/multitable/typeWithoutTableInUnion/schema.graphqls @@ -0,0 +1,5 @@ +type Query { + someUnion: SomeUnion +} + +union SomeUnion = Customer diff --git a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/query/unionSubTypeNoTable/schema.graphqls b/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/query/unionSubTypeNoTable/schema.graphqls deleted file mode 100644 index d28a635cc..000000000 --- a/graphitron-codegen-parent/graphitron-java-codegen/src/test/resources/validation/query/unionSubTypeNoTable/schema.graphqls +++ /dev/null @@ -1,13 +0,0 @@ -type Query { - paginatedUnionQuery(first: Int = 100, after: String): SomeUnionConnection -} - -union SomeUnion = Language | Staff - -type Staff { - id: ID -} - -type Language @table { - name: String -}