Fluent Commerce Logo
Docs
Sign In

Dynamic Queries and Utils

Essential knowledge

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}