Dynamic Queries and Utils
Author:
Kirill Gaiduk
Changed on:
8 Sept 2025
Overview
`DynamicEntityQuery`
, with the help of the `DynamicUtils`
class, automatically derives the correct GraphQL query operation for the primary Entity from the `RulesetContext`
. It dynamically retrieves fields related to that Entity, removing the need for pre-compiled queries while preserving 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.
- Connection Handling: Built-in support for paginated connections with automatic pagination handling via
`queryList()`
methods. - Dynamic Mutations: Update entity properties dynamically using key-value pairs without pre-compiled mutation queries.
- Unified Data Access:
`getJsonPath()`
method provides unified access to both event and entity data using consistent path syntax.
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. - Reduced Maintenance
No need for individual pre-compiled queries for each entity type, eliminating the overhead of maintaining separate query files and reducing the risk of inconsistencies.
Core Components
`DynamicUtils`
: The main utility class that provides a simplified API for all dynamic operations:- Query Operations: Type-based and path-based querying
- Connection Handling: Paginated data retrieval
- Mutations: Dynamic entity updates
- Data Access: Unified event and entity data access
`DynamicEntityQuery`
: The underlying query builder that generates GraphQL queries from:- POJO class definitions (type-based)
- Path specifications (path-based)
- Query parameters and filters
`DynamicDataTypes`
: Provides the response handling mechanisms:`QueryDynamicData`
: Wrapper for query results with type conversion`QueryDynamicVariables`
: Variable handling for parameterized queries
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, `DynamicUtils.query()`
recursively derives a query based on the fields defined within the class and then executes the query. The resulting object is automatically marshaled back into the same class.
Explanation through an Example
1@lombok.Data
2private static class Sample {
3 String ref;
4 double price;
5}
6
7//Usage
8Sample sample = DynamicUtils.query(context, 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
- Simplified API:
`DynamicUtils.query()`
handles the complexity of query execution and type conversion
Alternative Direct API Usage
If you need more control over the query execution, you can use the direct API:
1DynamicDataTypes.QueryDynamicData result = (DynamicDataTypes.QueryDynamicData) context.api()
2 .query(new DynamicEntityQuery(context, Sample.class));
3Sample sample = result.as(Sample.class);
Path-based usage
Path-based queries allow you to define your query as a series of strings, where each string represents a dot-separated path through the data graph. This approach is highly flexible and enables the creation of rules that adapt seamlessly to multiple entity types and fields.
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.
1DynamicDataTypes.QueryDynamicData result = DynamicUtils.query(context,
2 ImmutableList.of("ref", "price", "items.product.name"));
3
4// Access results using flattened selectors
5ImmutableMap<String, JsonNode> flatResult = result.flat();
6assertEquals("TestProduct", flatResult.get("items[0].product.name").toString());
7
8// Or access individual fields directly
9JsonNode ref = result.get("ref");
10JsonNode price = result.get("price");
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
Connection Queries (Pagination)
For handling paginated connections like `items`
, `fulfilments`
, etc., use the `queryList()`
methods:
1Map<String, Object> parameters = ImmutableMap.of(
2 "fulfilments.items", ImmutableMap.of("filledQuantity", 1,"rejectedQuantity", 0)
3);
4
5Iterable<Fulfilment> fulfilments = DynamicUtils.queryList(
6 context,
7 "fulfilments",
8 Fulfilment.class,
9 parameters
10);
11
12// Process each fulfilment
13for (Fulfilment fulfilment : fulfilments) {
14 // Handle each fulfilment
15}
Dynamic Mutations
The `DynamicUtils.mutate()`
method allows you to update entity properties dynamically:
1Map<String, Object> updates = ImmutableMap.of(
2 "tag1", "ecom",
3 "attributes", ImmutableMap.of(
4 "processedBy", "system",
5 "processedAt", "2024-01-15T10:30:00Z"
6 )
7);
8
9DynamicUtils.mutate(context, updates);
JSON Path Access
The `getJsonPath()` method allows you to access data from either the event or entity using a unified path syntax:
1// Access event data
2JsonNode eventValue = DynamicUtils.getJsonPath(context, "event.attributes.orderType");
3
4// Access entity data
5JsonNode entityValue = DynamicUtils.getJsonPath(context, "fulfilmentChoice.deliveryType");
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(
23 eventName = "{eventName}",
24 entityType = EventInfoVariables.EVENT_TYPE,
25 entitySubtype = EventInfoVariables.EVENT_SUBTYPE)
26 }
27)
28@ParamString(name = "attributeName", array = true, description = "Name of the attribute to check")
29@ParamString(name = "eventName", description = "Name of the event to send")
30public class IfAttributeExists implements Rule {
31 @Override
32 public void run(Context context) {
33 String attributeName = context.getProp("attributeName");
34 Entity entity = DynamicUtils.query(context, Entity.class);
35
36 List<Attribute> attributes = entity.getAttributes() != null ? entity.getAttributes() : emptyList();
37 if (attributes.stream().anyMatch(attribute -> attribute.getName().equalsIgnoreCase(attributeName))) {
38 EventUtils.forwardInboundEventWithNewName(context, context.getProp("eventName"));
39 }
40 }
41
42 @lombok.Data
43 public static class Entity {
44 private String id;
45 private String status;
46 @Nullable
47 private List<Attribute> attributes;
48 }
49}
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 = DynamicUtils.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}