Dynamic Queries
Author:
Kirill Gaiduk
Changed on:
15 Aug 2025
Overview
`DynamicEntityQuery`
automatically derives the correct GraphQL query operation for the primary Entity from the `RulesetContext`
, allowing it to dynamically retrieve fields related to that Entity. This eliminates the need for pre-compiled queries while maintaining type safety and performance.
Key points
- Runtime Query Generation: Automatically builds GraphQL queries at runtime based on entity context, eliminating pre-compiled queries.
- Dual Usage Patterns: Type-based mapping with POJO classes for strong typing, or path-based queries for maximum flexibility.
- Universal & Validated: Works across different entity types with automatic schema validation and connection pattern handling.
- Flexible Access: Results accessible as typed objects, JsonNode, or flattened maps with Lombok integration.
Key Advantages Over Traditional Apollo Queries
- One Query, Many Types
`DynamicEntityQuery`
allows a single query to be applied across different entities, especially those with common interfaces, removing the need for individual, pre-compiled queries for each entity type. This flexibility reduces the need for branching logic within rules, which can be a major source of inefficiency.
For example, the`ChangeStateGQL`
rule became outdated quickly whenever new entity types were introduced. Therefore the`SetState`
was introduced which is entity type agnostic and therefore provides more flexibility. - Runtime Query Building
`DynamicEntityQuery`
supports building queries at runtime rather than relying solely on pre-compilation. This means that a rule can dynamically determine which data it requires based on parameters or other runtime information.
Here is a collection of common scenarios for the Dynamic Queries usage:
Type-based usage
To generate a dynamic query from a strongly typed Java class, `DynamicEntityQuery`
recursively derives a query based on the fields defined within the class and then executes the query. The resulting `DynamicEntityQuery.DynamicData`
object can be directly marshaled back into the same class, simplifying the process of mapping data.
Explanation through an Example
1@lombok.Data
2private static class Sample {
3 String ref;
4 double price;
5}
6
7//Usage
8Sample sample = context.api()
9 .query(new DynamicEntityQuery(context, Sample.class))
10 .as(Sample.class);
Benefits:
- Type Safety: Strongly typed results with automatic marshaling
- Lombok Integration: Use Lombok to reduce boilerplate code
- Field Mapping: Only the fields within the class are used for marshaling
Path-based usage
Path-based queries in `DynamicEntityQuery`
let you define your query as a series of strings, where each string represents a dot-separated path through the data graph. This approach allows you to target nested fields without writing custom GraphQL queries or POJOs. It supports multiple entity types and structures using the same rule logic, making it ideal for dynamic, reusable rule designs like those in the core module.
Explanation through an Example
1// Define paths as strings
2ImmutableList.of(
3 "ref",
4 "price",
5 "items.product.name"
6);
These paths would generate a GraphQL query that retrieves only the specified fields:
1query {
2 order { // root query is determined by the event context
3 ref
4 price
5 items {
6 edges {
7 node {
8 product {
9 name
10 }
11 }
12 }
13 }
14 }
15}
16
The library uses GraphQL schema introspection to ensure that each generated path aligns with valid schema fields; any invalid or mismatched paths are automatically excluded, ensuring that only valid data is queried.
The resulting `DynamicEntityQuery.DynamicData`
object can be accessed either as a Jackson `JsonNode`
for structured JSON handling or flattened into a map of graph selectors to `JsonNode`
values for quick lookup.
1ImmutableMap<String, ValueNode> flatResult = context.api()
2 .query(new DynamicEntityQuery(context,
3 ImmutableList.of("ref", "price", "items.product.name"))).flat();
4
5assertEquals("TestProduct", flatResult.get("items[0].product.name").toString());
Benefits:
- Automatic Connection Handling:
`items.product.name`
automatically expands to`items.edges.node.product.name`
- Schema Validation: Uses GraphQL introspection to ensure valid paths
- Flexible Access: Results can be accessed as
`JsonNode`
or flattened map - Error Prevention: Invalid paths are automatically excluded
Complete Examples
Example 1: Attribute Checking Rule
This rule loads attributes associated with an order and checks if a specified attribute exists:
1package com.example.rule;
2
3import com.fluentcommerce.dto.common.Attribute;
4import com.fluentcommerce.util.core.EventUtils;
5import com.fluentcommerce.util.dynamic.DynamicUtils;
6import com.fluentretail.rubix.rule.meta.EventInfo;
7import com.fluentretail.rubix.rule.meta.EventInfoVariables;
8import com.fluentretail.rubix.rule.meta.ParamString;
9import com.fluentretail.rubix.rule.meta.RuleInfo;
10import com.fluentretail.rubix.v2.context.Context;
11import com.fluentretail.rubix.v2.rule.Rule;
12
13import javax.annotation.Nullable;
14import java.util.List;
15
16import static java.util.Collections.emptyList;
17
18@RuleInfo(
19 name = "IfAttributeExists",
20 description = "If attribute {attributeName} exists, do {eventName}",
21 accepts = { @EventInfo(entityType = "ORDER") },
22 produces = { @EventInfo(eventName = "{eventName}", entityType = "ORDER", entitySubtype = EventInfoVariables.INHERITED) }
23)
24@ParamString(name = "attributeName", array = true, description = "Name of the attribute to check")
25@ParamString(name = "eventName", description = "Name of the event to send")
26public class IfAttributeExists implements Rule {
27 @Override
28 public void run(Context context) {
29 String attributeName = context.getProp("attributeName");
30 Entity entity = DynamicUtils.query(context, Entity.class);
31
32 List<Attribute> attributes = entity.getAttributes() != null ? entity.getAttributes() : emptyList();
33 if (attributes.stream().anyMatch(attribute -> attribute.getName().equalsIgnoreCase(attributeName))) {
34 EventUtils.forwardInboundEventWithNewName(context, context.getProp("eventName"));
35 }
36 }
37
38 @lombok.Data
39 public static class Entity {
40 private String id;
41 private String status;
42 @Nullable
43 private List<Attribute> attributes;
44 }
45}
1{
2 "name": "example.orders.IfAttributeExists",
3 "props": {
4 "attributeName": "fcOrderStatusHistory",
5 "eventName": "CompleteOrder"
6 }
7}
Example 2: Flexible Property Comparison
This rule compares a parameter value against an arbitrary field of the primary entity:
1import com.fasterxml.jackson.databind.JsonNode;
2import com.fluentcommerce.util.core.JsonUtils;
3import com.fluentretail.rubix.rule.meta.EventInfo;
4import com.fluentretail.rubix.rule.meta.ParamString;
5import com.fluentretail.rubix.rule.meta.RuleInfo;
6import com.fluentretail.rubix.v2.context.Context;
7import com.fluentretail.rubix.v2.rule.Rule;
8
9import static com.fluentcommerce.util.core.EventUtils.forwardInboundEventWithNewName;
10import static com.fluentcommerce.util.core.LogUtils.logOnce;
11import static com.fluentcommerce.util.dynamic.DynamicUtils.getJsonPath;
12
13@RuleInfo(
14 name = "IfPropertyEquals",
15 description = "If {jsonpath} is {value}, do {eventName}",
16 produces = { @EventInfo(eventName = "{eventName}") }
17)
18@ParamString(name = "jsonpath", description = "Property of the entity, e.g. 'fulfilmentChoice.deliveryType'")
19@ParamString(name = "value", description = "Value to compare, e.g. 'EXPRESS'")
20@ParamString(name = "eventName", description = "Name of the event to send")
21public class IfPropertyEquals implements Rule {
22 @Override
23 public void run(Context context) {
24 String jsonPath = context.getProp("jsonpath");
25 String value = context.getProp("value");
26
27 JsonNode result = getJsonPath(context, jsonPath);
28
29 if (JsonUtils.marshallAndEquals(value, result)) {
30 forwardInboundEventWithNewName(context, context.getProp("eventName"));
31 } else {
32 logOnce(context, IfPropertyEquals.class, "Values '%s' and '%s' are not equal", result, value);
33 }
34 }
35}
1{
2 "name": "example.core.IfPropertyEquals",
3 "props": {
4 "jsonpath": "fulfilmentChoice.shippingType",
5 "value": "EXPRESS",
6 "eventName": "SourceOrderForExpressDelivery"
7 }
8}