Fluent Commerce Logo
Docs
Sign In

How Dynamic Utilities work

Topic

Author:

Kirill Gaiduk

Changed on:

10 July 2025

Overview

The articles below will walk you through the Dynamic Utilities (`dynamic-core`), designed to simplify rule development and reduce repetitive code.

Prerequisites

Before diving in, make sure you have:

Dynamic Utilities Overview

Author:

Kirill Gaiduk

Changed on:

15 Aug 2025

Overview

The `util-dynamic` library is a collection of utility functions that allow you to generate GraphQL queries and mutations at runtime. This enables the creation of highly configurable and reusable rules that can adapt to different needs without requiring code changes.

Prerequisites

These articles assumes you're familiar with:

  • Java
  • Maven
  • JUnit

Key points

  • Dynamic Queries: Fetch any field from an entity at runtime by providing a POJO class that defines the desired data structure, eliminating the need for pre-compiled queries.
  • Dynamic Mutations: Update any field on an entity by passing a simple `Map` of key-value pairs, allowing for the creation of generic "field setter" rules.
  • Dynamic Connections: Easily query and iterate over paginated one-to-many relationships (like `order.items`), as the utility handles all the pagination logic for you.
  • Advanced JSON Path: A powerful `getJsonPath` method that can retrieve data from either the event or the entity itself, automatically determining the correct source.
  • Enhanced JSON Utilities: This bundle includes a more advanced `JsonUtils` with powerful methods for navigating and transforming complex JSON structures.

Value Proposition

The primary value of the Dynamic Utilities is flexibility.

  • Configurable Rules: Workflow designers can change what data a Rule fetches or updates by modifying Rule properties, without needing a developer to write and deploy new code.
  • Reusable Logic: You can create a single, generic rule (e.g., "SetEntityField") that can be used across many different entity types and scenarios, reducing code duplication.
  • Adaptive Workflows: Build workflows that can adapt to different conditions at runtime by dynamically querying or updating the data that matters most in a given context.

Apollo Android Context

The Fluent Rules SDK relies on Apollo Android to execute GraphQL queries. Apollo's method of pre-compiling queries into Java classes:

  • Enhances type safety 
  • And boosts performance

However, it also has drawbacks: 

  • Pre-compiled queries restrict Workflow designers from dynamically accessing or updating data, as they require new queries to be developed and compiled each time. 
  • Additionally, the pre-compilation process limits the ability to create universal Rules that apply similar logic across different entity types. 

The Dynamic Utilities work around these limitations and enhance Rule configurability.


Explanation through an Example

Here's how Dynamic Utilities solve the Apollo limitation by creating a single, reusable rule that can update any field on any entity:

1import com.fluentcommerce.util.dynamic.DynamicUtils;
2import com.fluentretail.api.model.attribute.Attribute;
3import java.util.Map;
4
5public class GenericFieldUpdateRule implements Rule {
6    @Override
7    public void run(Context context) {
8        // Get the field to update from rule properties
9        String fieldName = context.getProp("fieldName");
10        String fieldValue = context.getProp("fieldValue");
11        
12        // Create a simple map with the field update
13        Map<String, Object> updates = Map.of(fieldName, fieldValue);
14        
15        // Use dynamic mutation to update the entity
16        // This works for ANY entity type without pre-compiled queries
17        DynamicUtils.updateEntity(
18            context,
19            context.getEvent().getEntityId(),
20            updates
21        );
22    }
23}

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}


Dynamic Mutations

Author:

Kirill Gaiduk

Changed on:

10 July 2025

Overview

`DynamicUpdateMutation` automatically derives the correct update Mutation for the primary Entity based on the `RulesetContext` and then performs the update on one or more fields.

Key points

  • Automatic Mutation Selection: GraphQL mutations are automatically selected from the `RulesetContext` based on entity type.
  • Flexible Field Updates: Update single fields or multiple fields in bulk using simple key-value pairs.
  • Type-Based Safety: Use POJO classes for strongly-typed updates with automatic field derivation.
  • Runtime Validation: Built-in validation ensures only valid fields are included in mutations.
  • Universal Application: Works across different entity types without requiring entity-specific mutation code.

Here is a collection of common scenarios for the Dynamic Mutations usage:

Field-based Updates

Fields can be updated directly by name. You can:

  • Update a single field 
  • Provide multiple fields in a `Map<String, Object>` for bulk updates
1context.action().mutation(
2    new DynamicUpdateMutation(context, "status", context.getProp("status"))
3);
1context.action().mutation(
2  new DynamicUpdateMutation(context, ImmutableMap.of(
3    "status", context.getProp("status"),
4    "attributes", Attribute.of("New Attribute", "New Value")
5  )));

Type-based Update

For improved type safety, `DynamicUpdateMutation` can also recursively derive update fields from a defined POJO. This allows you to work with strongly typed objects while still benefiting from dynamic field updates, ensuring both flexibility and safety in your Rule logic.

1@lombok.Data
2@lombok.AllArgsConstructor
3private static class Sample {
4    String status;
5    List<Attribute> attributes;
6}
7
8// Create the update object
9Sample sample = new Sample(
10    context.getProp("status"), 
11    Attribute.of("New Attribute", "New Value")
12);
13
14// Execute the mutation
15context.action().mutation(new DynamicUpdateMutation(context, sample));

Error Handling

The `DynamicUpdateMutation` provides built-in error handling:

1// Strict validation - throws exception for invalid fields
2new DynamicUpdateMutation(context, updates, true);
3
4// Lenient validation - silently excludes invalid fields
5new DynamicUpdateMutation(context, updates, false);

This ensures that your mutations are robust and handle edge cases gracefully.

Complete Examples

Example 1: Generic Field Setter Rule

This rule demonstrates how to create a reusable field setter that works for any entity type:

1import com.fluentcommerce.util.dynamic.DynamicUtils;
2import com.fluentretail.rubix.rule.meta.ParamString;
3import com.fluentretail.rubix.rule.meta.RuleInfo;
4import com.fluentretail.rubix.v2.context.Context;
5import com.fluentretail.rubix.v2.rule.Rule;
6
7import java.util.Map;
8
9@RuleInfo(
10    name = "SetEntityField",
11    description = "Set field {fieldName} to {fieldValue} on the current entity"
12)
13@ParamString(name = "fieldName", description = "Name of the field to update")
14@ParamString(name = "fieldValue", description = "Value to set for the field")
15public class SetEntityField implements Rule {
16    @Override
17    public void run(Context context) {
18        String fieldName = context.getProp("fieldName");
19        String fieldValue = context.getProp("fieldValue");
20        
21        // Create a simple map with the field update
22        Map<String, Object> updates = Map.of(fieldName, fieldValue);
23        
24        // Use DynamicUtils for cleaner syntax
25        DynamicUtils.mutate(context, updates);
26    }
27}
1{
2    "name": "example.core.SetEntityField",
3    "props": {
4        "fieldName": "tag1",
5        "fieldValue": "newTag"
6    }
7}

Example 2: Complex Object Update

This example shows how to update complex nested objects using type-based mutations:

1import com.fluentcommerce.dto.common.Attribute;
2import com.fluentcommerce.util.dynamic.DynamicUtils;
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 java.util.List;
10
11@RuleInfo(
12    name = "UpdateOrderWithAttributes",
13    description = "Update order tag1 and add custom attributes",
14    accepts = { @EventInfo(entityType = "ORDER") }
15)
16@ParamString(name = "tag1", description = "New tag 1 for the order")
17@ParamString(name = "attributeName", description = "Name of the attribute to add")
18@ParamString(name = "attributeValue", description = "Value of the attribute to add")
19public class UpdateOrderWithAttributes implements Rule {
20    @Override
21    public void run(Context context) {
22        String tag1 = context.getProp("tag1");
23        String attributeName = context.getProp("attributeName");
24        String attributeValue = context.getProp("attributeValue");
25        
26        // Create a typed update object
27        OrderUpdate update = new OrderUpdate(
28            tag1,
29            List.of(Attribute.of(attributeName, attributeValue))
30        );
31        
32        // Execute the mutation
33        context.action().mutation(new DynamicUpdateMutation(context, update));
34    }
35    
36    @lombok.Data
37    @lombok.AllArgsConstructor
38    private static class OrderUpdate {
39        private String tag1;
40        private List<Attribute> attributes;
41    }
42}


Dynamic Pagination

Author:

Kirill Gaiduk

Changed on:

24 June 2025

Overview

`DynamicConnectionQuery` automatically derives the Connection fields (arrays) for the primary Entity based on the `RulesetContext` and can dynamically load all connection items, with pagination handled internally.

Key points

  • Automatic Pagination`DynamicUtils.queryList` completely automates fetching paginated data from a GraphQL connection. You get a simple `Iterable`, and the library handles fetching subsequent pages in the background as you iterate.
  • Lazy Loading: The utility is highly efficient because it uses a "lazy loading" approach. It only fetches the next page of data when you have iterated through the current one.
  • POJO-Driven: You define the data you need by creating a simple POJO class. The utility dynamically builds the GraphQL query to fetch only the fields defined in your POJO.
  • Server-Side Filtering: You can pass a `Map` of parameters to `queryList` to filter the connection on the server. This is much more efficient than fetching all the data and filtering it in your rule.

Here is a collection of common scenarios for the Dynamic Pagination usage:

Query from Context usage

The simplest approach is to let the system automatically select the appropriate GraphQL query based on the entity type from the `RulesetContext`.

1@lombok.Data
2public class Fulfilment {
3    String id;
4    String ref;
5    String type;
6}
7
8// Load all Order Fulfilments with status COMPLETE
9final List<Fulfilment> fulfilments = new ArrayList<>();
10final Iterable<Fulfilment> iterable = DynamicUtils.queryList(context,
11    "fulfilments", Fulfilment.class,
12    Collections.singletonMap("fulfilments", Collections.singletonMap("status", "COMPLETE")));
13orderItemIterable.forEach(fulfilments::add);

Explanation through an Example

The rule loads the attributes associated with an order and checks if attribute with specified name exists. If the condition is met, the rule sends an event.

1package com.example.rule; 
2  
3import com.fluentcommerce.util.core.EventUtils;
4import com.fluentcommerce.util.dynamic.DynamicUtils;
5import com.fluentretail.rubix.rule.meta.*;
6import com.fluentretail.rubix.v2.context.Context;
7import com.fluentretail.rubix.v2.rule.Rule;
8import java.util.List;
9import java.util.stream.Stream;
10import java.util.stream.StreamSupport;
11
12@RuleInfo(
13        name = "IfAllFulfilmentsInState",
14        description = "If all fulfilments are in status {status}, do {eventName}",
15        accepts = { @EventInfo(entityType = "ORDER") },
16        produces = { @EventInfo(eventName = "{eventName}", entityType = "ORDER", entitySubtype = EventInfoVariables.EVENT_SUBTYPE, status = EventInfoVariables.EVENT_STATUS) }
17)
18@ParamString(name = "status", array = true, description = "List of statuses to check")
19@ParamString(name = "eventName", description = "Name of the event to send")
20public class IfAllFulfilmentsInState implements Rule {
21    @Override
22    public void run(Context context) {
23        List<String> states = context.getPropList("status", String.class);
24
25        Iterable<Fulfilment> fulfilmentIterable = DynamicUtils.queryList(context, "fulfilments",
26                Fulfilment.class, null);
27        Stream<Fulfilment> fulfilmentStream = StreamSupport.stream(fulfilmentIterable.spliterator(), false);
28
29        if(fulfilmentStream.allMatch(f -> states.contains(f.status))) {
30            EventUtils.forwardInboundEventWithNewName(context, context.getProp("eventName"));
31        }
32    }
33
34    @lombok.Data
35    public static class Fulfilment {
36        String status;
37    }
38}
1{
2  "name": "example.orders.IfAllFulfilmentsInState",
3  "props": {
4    "status": [ "COMPLETE", "CANCELLED" ],
5    "eventName": "CompleteOrder"
6  }
7}

Query from Parameter usage 

Alternatively, you can explicitly specify the GraphQL query within the method, giving you more control over the query selection. This approach allows for greater flexibility when you need to handle different types of queries within the same rule.

1@lombok.Data
2public class OrderItem {
3    String id;
4    String ref;
5    Double price;
6}
7
8//Get Order Items By Order Id
9final List<OrderItem> orderItems = new ArrayList<>();
10Iterable<OrderItem> orderItemIterable = DynamicUtils.queryList(context, 
11    "order", "items", OrderItem.class,
12    Collections.singletonMap("id", orderId));
13orderItemIterable.forEach(orderItems::add);