From 4d39f7ed54d9e07a688c1f48673fe674c17d0daf Mon Sep 17 00:00:00 2001 From: Joanna Brodbeck Date: Fri, 6 Mar 2026 10:29:36 +0100 Subject: [PATCH 1/4] first version to support variables as part of a GraphQL operation --- .../kotlin/example/ByosApplicationTest.kt | 65 +++++++++++ src/main/java/byos/ConditionFactory.java | 105 ++++++++++++++---- 2 files changed, 151 insertions(+), 19 deletions(-) diff --git a/src/integration-test/kotlin/example/ByosApplicationTest.kt b/src/integration-test/kotlin/example/ByosApplicationTest.kt index a03837c..3be47ee 100644 --- a/src/integration-test/kotlin/example/ByosApplicationTest.kt +++ b/src/integration-test/kotlin/example/ByosApplicationTest.kt @@ -1,5 +1,7 @@ package example +import com.fasterxml.jackson.databind.ObjectMapper +import graphql.parser.Parser import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -9,6 +11,69 @@ internal class ByosApplicationTest { @Autowired private lateinit var graphQLService: GraphQLService + private val parser = Parser() + private val objectMapper = ObjectMapper() + + @Test + fun queryWithAndVariableObjectCondition() { + val query = """ + query FilmsList( + ${'$'}limit: Int!, + ${'$'}filter: String, + ${'$'}andCondition1: FilmWhere! + ) { + films: filmByIds( + where: { _and: [ + { _or: [ + { title: { _ilike: ${'$'}filter } } + ] }, + ${'$'}andCondition1 + ] }, + limit: ${'$'}limit + ) { + edges { + node { + film_id + title + } + } + } + } + """.trimIndent() + + val variables = mapOf( + "limit" to objectMapper.readTree("2"), + "filter" to objectMapper.readTree("\"%A%\""), + "andCondition1" to objectMapper.readTree("""{ "film_id": { "_gte": 2 } }""") + ) + + val expectedResult = """ + { + "data": { + "films": { + "edges": [ + { + "node": { + "film_id": 2, + "title": "ACE GOLDFINGER" + } + }, + { + "node": { + "film_id": 3, + "title": "ADAPTATION HOLES" + } + } + ] + } + } + } + """.trimIndent() + + val requestInfo = RequestInfo(parser.parseDocument(query), "FilmsList", variables) + assertJsonEquals(expectedResult, graphQLService.executeGraphQLQuery(requestInfo)) + } + @Test fun simpleQuery() { val query = """ diff --git a/src/main/java/byos/ConditionFactory.java b/src/main/java/byos/ConditionFactory.java index 47ee198..4ac8872 100644 --- a/src/main/java/byos/ConditionFactory.java +++ b/src/main/java/byos/ConditionFactory.java @@ -57,16 +57,25 @@ private static Condition getCondition(ObjectField objectField, Map getCondition((ObjectValue) objectValue, variables, table)) - .collect(Collectors.toSet())); + Value rawValue = objectField.getValue(); + if (rawValue instanceof VariableReference) { + JsonNode resolved = variables.get(((VariableReference) rawValue).getName()); + return DSL.and(List.of(getConditionFromJsonNode(resolved, variables, table))); + } + return DSL.and(asArrayValue(rawValue).getValues().stream() + .map(value -> resolveToCondition(value, variables, table)) + .collect(Collectors.toList())); } case CONDITION_OR: { - return DSL.or(asArrayValue(objectField.getValue()).getValues().stream() - .map(objectValue -> getCondition((ObjectValue) objectValue, variables, table)) - .collect(Collectors.toSet())); - } - case CONDITION_NOT: { + Value rawValue = objectField.getValue(); + if (rawValue instanceof VariableReference) { + JsonNode resolved = variables.get(((VariableReference) rawValue).getName()); + return DSL.or(List.of(getConditionFromJsonNode(resolved, variables, table))); + } + return DSL.or(asArrayValue(rawValue).getValues().stream() + .map(value -> resolveToCondition(value, variables, table)) + .collect(Collectors.toList())); + } case CONDITION_NOT: { return DSL.not(getCondition((ObjectValue) objectField.getValue(), variables, table)); } default: { @@ -79,6 +88,63 @@ private static Condition getCondition(ObjectField objectField, Map variables, Table table) { + if (value instanceof ObjectValue) { + return getCondition((ObjectValue) value, variables, table); + } else if (value instanceof VariableReference) { + JsonNode resolved = variables.get(((VariableReference) value).getName()); + return getConditionFromJsonNode(resolved, variables, table); + } + throw new IllegalArgumentException("Unsupported value type in condition array: " + value.getClass()); + } + + private static Condition getConditionFromJsonNode(JsonNode node, Map variables, Table table) { + if (node.isObject()) { + ObjectValue.Builder builder = ObjectValue.newObjectValue(); + node.fields().forEachRemaining(entry -> { + Value nestedValue = jsonNodeToValue(entry.getValue()); + builder.objectField( + ObjectField.newObjectField() + .name(entry.getKey()) + .value(nestedValue) + .build() + ); + }); + return getCondition(builder.build(), variables, table); + } + return DSL.noCondition(); + } + + private static Value jsonNodeToValue(JsonNode node) { + if (node.isTextual()) { + return StringValue.newStringValue(node.asText()).build(); + } else if (node.isInt() || node.isLong()) { + return IntValue.newIntValue(BigInteger.valueOf(node.asLong())).build(); + } else if (node.isFloat() || node.isDouble()) { + return FloatValue.newFloatValue(BigDecimal.valueOf(node.asDouble())).build(); + } else if (node.isBoolean()) { + return BooleanValue.newBooleanValue(node.asBoolean()).build(); + } else if (node.isArray()) { + ArrayValue.Builder arrayBuilder = ArrayValue.newArrayValue(); + node.forEach(element -> arrayBuilder.value(jsonNodeToValue(element))); + return arrayBuilder.build(); + } else if (node.isObject()) { + ObjectValue.Builder objBuilder = ObjectValue.newObjectValue(); + node.fields().forEachRemaining(entry -> + objBuilder.objectField( + ObjectField.newObjectField() + .name(entry.getKey()) + .value(jsonNodeToValue(entry.getValue())) + .build() + ) + ); + return objBuilder.build(); + } else if (node.isNull()) { + return NullValue.newNullValue().build(); + } + throw new IllegalArgumentException("Unsupported JsonNode type: " + node.getNodeType()); + } + private static ArrayValue asArrayValue(Value value) { if (value instanceof ArrayValue) { return (ArrayValue) value; @@ -122,29 +188,31 @@ protected static Condition getCondition(Field field, Map varia public static Object extractValue(Value value, Map variables) { if (value instanceof StringValue) { - return ((StringValue)value).getValue(); + return ((StringValue) value).getValue(); } else if (value instanceof IntValue) { - return ((IntValue)value).getValue(); + return ((IntValue) value).getValue(); } else if (value instanceof BooleanValue) { - return ((BooleanValue)value).isValue(); + return ((BooleanValue) value).isValue(); } else if (value instanceof FloatValue) { return ((FloatValue) value).getValue(); } else if (value instanceof ArrayValue) { - return ((ArrayValue)value).getValues().stream().map(v -> extractValue(v, variables)).collect(Collectors.toList()); + return ((ArrayValue) value).getValues().stream().map(v -> extractValue(v, variables)).collect(Collectors.toList()); } else if (value instanceof ObjectValue) { - return ((ObjectValue)value).getObjectFields(); + return ((ObjectValue) value).getObjectFields(); } else if (value instanceof VariableReference) { final JsonNode jsonNode = variables.get(((VariableReference) value).getName()); if (jsonNode != null) { if (jsonNode.isArray()) { return extractArrayValue((ArrayNode) jsonNode); - } - else { + } else if (jsonNode.isObject()) { + return jsonNode; + } else { return jsonNode.asText(); } } return null; } + throw new IllegalArgumentException("nyi"); } @@ -158,15 +226,14 @@ public static IntValue extractIntValue(Value value, Map variab if (value == null) { return null; } - if (value instanceof IntValue) { + if (value instanceof IntValue) { return (IntValue) value; } final Object extractedValue = extractValue(value, variables); if (extractedValue instanceof BigDecimal) { return new IntValue((BigInteger) extractedValue); - } - else if (extractedValue instanceof String) { - return new IntValue(new BigInteger((String)extractedValue)); + } else if (extractedValue instanceof String) { + return new IntValue(new BigInteger((String) extractedValue)); } return null; } From 40a3334449fa35c1aae21c17f6d918c493d7182d Mon Sep 17 00:00:00 2001 From: Joanna Brodbeck Date: Tue, 10 Mar 2026 09:02:12 +0100 Subject: [PATCH 2/4] first version to support nested lookups, in the style of hasura - added getRelatedTable function - updated example file for Config.kt to include the getRelatedTable fun - include TableAndConditionService such that the related tables can be found --- .../HardcodedTableAndConditionService.java | 6 ++ src/integration-test/kotlin/example/Config.kt | 17 +++++ src/main/java/byos/ConditionFactory.java | 69 ++++++++++++------- .../kotlin/byos/TableAndConditionService.kt | 6 ++ src/main/kotlin/byos/WhereCondition.kt | 4 +- 5 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/integration-test/java/example/HardcodedTableAndConditionService.java b/src/integration-test/java/example/HardcodedTableAndConditionService.java index c363d73..2dc468f 100644 --- a/src/integration-test/java/example/HardcodedTableAndConditionService.java +++ b/src/integration-test/java/example/HardcodedTableAndConditionService.java @@ -21,4 +21,10 @@ public Table getTableWithAliasFor(@NotNull InternalQueryNode.R public Condition getConditionFor(@NotNull String relationshipName, @NotNull Table left, @NotNull Table right) { return ConfigKt.getConditionForRelationship(relationshipName, left, right); } + + @Nullable + @Override + public Table getRelatedTable(@NotNull String relationshipName, @NotNull Table from) { + return ConfigKt.getRelatedTable(relationshipName, from); + } } diff --git a/src/integration-test/kotlin/example/Config.kt b/src/integration-test/kotlin/example/Config.kt index e03aa81..ec8cb2f 100644 --- a/src/integration-test/kotlin/example/Config.kt +++ b/src/integration-test/kotlin/example/Config.kt @@ -52,3 +52,20 @@ fun getConditionForRelationship(relationshipName: String, left: Table<*>, right: else -> null } + +fun getRelatedTable(relationshipName: String, from: Table<*>): Table<*>? = + when { + relationshipName == "actors" && from is Film -> Tables.ACTOR + relationshipName == "films" && from is Actor -> Tables.FILM + relationshipName == "stores" && from is Film -> Tables.STORE + relationshipName == "films" && from is Store -> Tables.FILM + relationshipName == "language" && from is Film -> Tables.LANGUAGE + relationshipName == "original_language" && from is Film -> Tables.LANGUAGE + relationshipName == "inventories" && from is Store -> Tables.INVENTORY + relationshipName == "film" && from is Inventory -> Tables.FILM + relationshipName == "categories" && from is Film -> Tables.CATEGORY + relationshipName == "films" && from is Category -> Tables.FILM + relationshipName == "parent_category" && from is Category -> Tables.CATEGORY + relationshipName == "subcategories" && from is Category -> Tables.CATEGORY + else -> null + } \ No newline at end of file diff --git a/src/main/java/byos/ConditionFactory.java b/src/main/java/byos/ConditionFactory.java index 4ac8872..f7c31e6 100644 --- a/src/main/java/byos/ConditionFactory.java +++ b/src/main/java/byos/ConditionFactory.java @@ -21,22 +21,27 @@ // https://www.graphql-java.com/documentation/data-mapping#scalars public class ConditionFactory { - public static Condition getWhereCondition(Argument whereArgument, Map variables, Table table) { - return getCondition(getWhereObject(whereArgument.getValue()), variables, table); + public static Condition getWhereCondition( + Argument whereArgument, + Map variables, + Table table, + TableAndConditionService tableAndConditionService + ) { + return getCondition(getWhereObject(whereArgument.getValue()), variables, table, tableAndConditionService); } - public static Condition getCondition(ObjectValue objectValue, Map variables, Table table) { + public static Condition getCondition(ObjectValue objectValue, Map variables, Table table, TableAndConditionService tableAndConditionService) { switch (objectValue.getObjectFields().size()) { case 0: { return DSL.noCondition(); } case 1: { - return getCondition(objectValue.getObjectFields().get(0), variables, table); + return getCondition(objectValue.getObjectFields().get(0), variables, table, tableAndConditionService); } default: { // multiple fields conditions are "add" concatenated by default return DSL.and(objectValue.getObjectFields().stream() - .map(objectField -> getCondition(objectField, variables, table)) + .map(objectField -> getCondition(objectField, variables, table, tableAndConditionService)) .collect(Collectors.toSet())); } } @@ -53,64 +58,80 @@ private static ObjectValue getWhereObject(Value value) { throw new IllegalArgumentException("Value of whereArgument must be an object"); } - private static Condition getCondition(ObjectField objectField, Map variables, Table table) { + private static Condition getCondition(ObjectField objectField, Map variables, Table table, TableAndConditionService tableAndConditionService) { final String name = objectField.getName(); switch (name) { case CONDITION_AND: { Value rawValue = objectField.getValue(); if (rawValue instanceof VariableReference) { JsonNode resolved = variables.get(((VariableReference) rawValue).getName()); - return DSL.and(List.of(getConditionFromJsonNode(resolved, variables, table))); + return DSL.and(List.of(getConditionFromJsonNode(resolved, variables, table, tableAndConditionService))); } return DSL.and(asArrayValue(rawValue).getValues().stream() - .map(value -> resolveToCondition(value, variables, table)) + .map(value -> resolveToCondition(value, variables, table, tableAndConditionService)) .collect(Collectors.toList())); } case CONDITION_OR: { Value rawValue = objectField.getValue(); if (rawValue instanceof VariableReference) { JsonNode resolved = variables.get(((VariableReference) rawValue).getName()); - return DSL.or(List.of(getConditionFromJsonNode(resolved, variables, table))); + return DSL.or(List.of(getConditionFromJsonNode(resolved, variables, table, tableAndConditionService))); } return DSL.or(asArrayValue(rawValue).getValues().stream() - .map(value -> resolveToCondition(value, variables, table)) + .map(value -> resolveToCondition(value, variables, table, tableAndConditionService)) .collect(Collectors.toList())); - } case CONDITION_NOT: { - return DSL.not(getCondition((ObjectValue) objectField.getValue(), variables, table)); + } + case CONDITION_NOT: { + return DSL.not(getCondition((ObjectValue) objectField.getValue(), variables, table, tableAndConditionService)); } default: { + final Field field = table.field(name); if (field != null) { return getCondition(field, variables, objectField.getValue()); } + // if the name does not match with any columns in the current table and it is is a nested object + // (not a scalar) + if (tableAndConditionService != null && objectField.getValue() instanceof ObjectValue) { + ObjectValue nestedWhere = (ObjectValue) objectField.getValue(); + + Table relatedTable = tableAndConditionService.getRelatedTable(name, table); + if (relatedTable != null) { + Condition joinCondition = tableAndConditionService.getConditionFor(name, table, relatedTable); + Condition nestedCondition = getCondition(nestedWhere, variables, relatedTable, tableAndConditionService); + return DSL.exists( + DSL.selectOne() + .from(relatedTable) + .where(joinCondition) + .and(nestedCondition) + ); + } + } } } return DSL.noCondition(); } - private static Condition resolveToCondition(Value value, Map variables, Table table) { + private static Condition resolveToCondition(Value value, Map variables, Table table, TableAndConditionService tableAndConditionService) { if (value instanceof ObjectValue) { - return getCondition((ObjectValue) value, variables, table); + return getCondition((ObjectValue) value, variables, table, tableAndConditionService); } else if (value instanceof VariableReference) { JsonNode resolved = variables.get(((VariableReference) value).getName()); - return getConditionFromJsonNode(resolved, variables, table); + return getConditionFromJsonNode(resolved, variables, table, tableAndConditionService); } throw new IllegalArgumentException("Unsupported value type in condition array: " + value.getClass()); } - private static Condition getConditionFromJsonNode(JsonNode node, Map variables, Table table) { + private static Condition getConditionFromJsonNode(JsonNode node, Map variables, Table table, TableAndConditionService tableAndConditionService) { if (node.isObject()) { ObjectValue.Builder builder = ObjectValue.newObjectValue(); node.fields().forEachRemaining(entry -> { - Value nestedValue = jsonNodeToValue(entry.getValue()); - builder.objectField( - ObjectField.newObjectField() - .name(entry.getKey()) - .value(nestedValue) - .build() - ); + builder.objectField(ObjectField.newObjectField() + .name(entry.getKey()) + .value(jsonNodeToValue(entry.getValue())) + .build()); }); - return getCondition(builder.build(), variables, table); + return getCondition(builder.build(), variables, table, tableAndConditionService); } return DSL.noCondition(); } diff --git a/src/main/kotlin/byos/TableAndConditionService.kt b/src/main/kotlin/byos/TableAndConditionService.kt index c75e72b..1b9885b 100644 --- a/src/main/kotlin/byos/TableAndConditionService.kt +++ b/src/main/kotlin/byos/TableAndConditionService.kt @@ -18,4 +18,10 @@ interface TableAndConditionService { * Returns the join condition to be applied between the given two tables for the given relationship. */ fun getConditionFor(relationshipName: String, left: Table<*>, right: Table<*>): Condition? + + /** + * Get all the related tables, mainly important for nested lookups + */ + fun getRelatedTable(relationshipName: String, from: Table<*>): Table<*>? + } diff --git a/src/main/kotlin/byos/WhereCondition.kt b/src/main/kotlin/byos/WhereCondition.kt index 24a3295..cb6838a 100644 --- a/src/main/kotlin/byos/WhereCondition.kt +++ b/src/main/kotlin/byos/WhereCondition.kt @@ -38,8 +38,8 @@ class WhereCondition(private val tableAndConditionService: TableAndConditionServ } } - fun getForWhere(argument: Argument, variables: Map , table: Table<*>): Condition { - return ConditionFactory.getWhereCondition(argument, variables, table) + fun getForWhere(argument: Argument, variables: Map, table: Table<*>): Condition { + return ConditionFactory.getWhereCondition(argument, variables, table, tableAndConditionService) } private fun extractValue(value: Value>): Any? = From dfdace633b3699e86eb382438b39b41d13edd9e1 Mon Sep 17 00:00:00 2001 From: Joanna Brodbeck Date: Thu, 26 Mar 2026 17:27:36 +0100 Subject: [PATCH 3/4] transform DSL tree to string such that regelwerk can call $replace better and improve performance --- src/main/kotlin/byos/GraphQLService.kt | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/byos/GraphQLService.kt b/src/main/kotlin/byos/GraphQLService.kt index a50c8a8..9c6c360 100644 --- a/src/main/kotlin/byos/GraphQLService.kt +++ b/src/main/kotlin/byos/GraphQLService.kt @@ -14,9 +14,14 @@ import graphql.language.FragmentDefinition import graphql.language.OperationDefinition import graphql.parser.Parser import graphql.schema.GraphQLSchema +import graphql.validation.ValidationError import graphql.validation.Validator import org.jooq.DSLContext +import org.jooq.SQLDialect +import org.jooq.impl.DSL +import org.slf4j.LoggerFactory import java.util.* +import java.util.concurrent.ConcurrentHashMap data class RequestInfo( val document: Document, @@ -87,11 +92,19 @@ class GraphQLService(val schema: GraphQLSchema, private val tableAndConditionSer val queryTrees = queryTranspiler.buildInternalQueryTrees(ast, fragments) val results = - queryTrees.map { tree -> run {} - jooq.select(queryTranspiler.resolveInternalQueryTree(tree, requestInfo.variables)).fetch() - } + queryTrees.map { tree -> run {} + val queryPart = queryTranspiler.resolveInternalQueryTree(tree, requestInfo.variables) + val sqlString = jooq.renderInlined(DSL.select(queryPart)) + jooq.fetch(sqlString) + } + +// val results = +// queryTrees.map { tree -> run {} +// jooq.select(queryTranspiler.resolveInternalQueryTree(tree, requestInfo.variables)).fetch() +// } results.map(::println) // TODO rm debug statement - return results.formatGraphQLResponse() + return results.formatGraphQLResponse(); } } + From 2be537281e7ce5ba3a8ba1aacc9f63b128b49c82 Mon Sep 17 00:00:00 2001 From: Joanna Brodbeck Date: Tue, 31 Mar 2026 14:46:49 +0200 Subject: [PATCH 4/4] remove unnecessary imports --- src/main/kotlin/byos/GraphQLService.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/byos/GraphQLService.kt b/src/main/kotlin/byos/GraphQLService.kt index 9c6c360..ef73dfa 100644 --- a/src/main/kotlin/byos/GraphQLService.kt +++ b/src/main/kotlin/byos/GraphQLService.kt @@ -14,14 +14,10 @@ import graphql.language.FragmentDefinition import graphql.language.OperationDefinition import graphql.parser.Parser import graphql.schema.GraphQLSchema -import graphql.validation.ValidationError import graphql.validation.Validator import org.jooq.DSLContext -import org.jooq.SQLDialect import org.jooq.impl.DSL -import org.slf4j.LoggerFactory import java.util.* -import java.util.concurrent.ConcurrentHashMap data class RequestInfo( val document: Document,