The articles below will walk you through the Dynamic Utilities (`dynamic-core`), designed to simplify rule development and reduce repetitive code.PrerequisitesBefore diving in, make sure you have:
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.PrerequisitesThese 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.
Java Docs
The helper methods contained in the Utility Bundles libraries are many and varied, so the JavaDocs provided with the packages should be treated as the source of truth for documentation.
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:
1packagecom.fluentcommerce.sample1.test;23importcom.fluentcommerce.util.dynamic.graphql.DynamicUpdateMutation;4importcom.fluentretail.rubix.v2.context.Context;5importcom.fluentretail.rubix.v2.rule.Rule;6importcom.fluentretail.rubix.rule.meta.ParamString;7importcom.fluentretail.rubix.rule.meta.RuleInfo;8importcom.google.common.collect.ImmutableMap;9importlombok.extern.slf4j.Slf4j;1011importjava.util.Map;1213/**
14 * This rule updates a field with a new value.
15 */16@RuleInfo(name ="UpdateField", description ="Set a field {fieldName} to a value {fieldValue}")17@Slf4j18@ParamString(name ="fieldName", description ="Name of the field to be updated")19@ParamString(name ="fieldValue", description ="New value")20publicclassUpdateFieldimplementsRule{21@Override22publicvoidrun(finalContext context){23// Get a field to update from rule properties and its new value24finalString fieldName = context.getProp("fieldName");25finalString fieldValue = context.getProp("fieldValue");2627// Create a simple map with the field update28finalMap<String,Object> updates =ImmutableMap.of(fieldName, fieldValue);2930// Use dynamic update mutation to update an entity31// This works for ANY entity type without pre-compiled queries32 context.action().mutation(newDynamicUpdateMutation(context, updates), fieldValue);33}34}
The given example is one of the use cases.
The Dynamic Utilities are flexible and can be used in a wide range of the Rule writing scenarios.The most common ones are provided below.
`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.
1@lombok.Data2publicclassFulfilment{3String id;4String ref;5String type;6}78// Load Fulfillment9// This will work in the Context of Fulfillment10finalFulfilment fulfilment =DynamicUtils.query(context,Fulfilment.class);
Tip
We recommend using Lombok to reduce the overhead of writing these mapping classes, as it can streamline field definitions and boilerplate code.
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(newDynamicEntityQuery(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.
Example 1: Multiple-Fields Query
1// Define paths as strings2ImmutableList.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.
Note
Because the connection pattern is so common in the GraphQL API, you don’t need to specify each level explicitly. For instance, `items.product.name` will automatically expand to `items.edges.node.product.name`.
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,2ImmutableList.of("ref","price","items.product.name"));34// Access results using flattened selectors5ImmutableMap<String,JsonNode> flatResult = result.flat();6assertEquals("TestProduct", flatResult.get("items[0].product.name").toString());78// Or access individual fields directly9JsonNode ref = result.get("ref");10JsonNode price = result.get("price");
Example 2: Single-Field Query
You can also use `DynamicUtils.query` to retrieve a single field value directly from the entity:
1// Get a deliveryType of a fulfilment as TEXT2// This will work in the Context of Fulfillment3finalString deliveryType =DynamicUtils.query(context,4ImmutableList.of("deliveryType")).get("deliveryType").asText();
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:
Example 1: Specific Fulfillments Loading
1@lombok.Data2publicclassFulfilment{3String id;4String ref;5String type;6}78finalMap<String,Object> parameters =newHashMap<>();9finalMap<String,Object> quantity =newHashMap<>();10quantity.put("filledQuantity",1);11quantity.put("rejectedQuantity",0);12parameters.put("fulfilments.items", quantity);1314// This will work in the Context of Order15Iterable<Fulfilment> fulfilments =DynamicUtils.queryList(16 context,17"fulfilments.items",18Fulfilment.class,19 parameters
20);2122// Process each fulfilment23for(Fulfilment fulfilment : fulfilments){24// Handle each fulfilment25}
Example 2: Order Fulfillments Loading
1@lombok.Data2publicclassFulfilment{3String id;4String ref;5String type;6}78// Load all fulfilments of the order9// This will work in the Context of Order10finalList<Fulfilment> fulfilments =newArrayList<>();11Iterable<Fulfilment> fulfilmentIterable =DynamicUtils.queryList(context,"fulfilments",Fulfilment.class,null);12fulfilmentIterable.forEach(fulfilments::add);
Example 3: Fulfillment Articles Loading
1@lombok.Data2publicclassArticle{3String id;4String ref;5String name;6String type;7String status;8}910// Load all articles of the fulfilment11// This will work in the Context of Order12finalMap<String,Object> parameters =newHashMap<>();13parameters.put("id", fulfilment.getId());14Iterable<Article> articles =DynamicUtils15.queryList(context,"fulfilment","articles",Article.class, parameters);16fulfilment.setArticles(newArrayList<>());17articles.forEach(fulfilment.getArticles()::add);
Dynamic Mutations
The `DynamicUtils.mutate()` method allows you to update entity properties dynamically:
This rule compares a parameter value against an arbitrary field of the primary entity:
1importcom.fasterxml.jackson.databind.JsonNode;2importcom.fluentcommerce.util.core.JsonUtils;3importcom.fluentretail.rubix.rule.meta.EventInfo;4importcom.fluentretail.rubix.rule.meta.ParamString;5importcom.fluentretail.rubix.rule.meta.RuleInfo;6importcom.fluentretail.rubix.v2.context.Context;7importcom.fluentretail.rubix.v2.rule.Rule;89importstaticcom.fluentcommerce.util.core.EventUtils.forwardInboundEventWithNewName;10importstaticcom.fluentcommerce.util.core.LogUtils.logOnce;11importstaticcom.fluentcommerce.util.dynamic.DynamicUtils.getJsonPath;1213@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")21publicclassIfPropertyEqualsimplementsRule{22@Override23publicvoidrun(Context context){24String jsonPath = context.getProp("jsonpath");25String value = context.getProp("value");2627JsonNode result =DynamicUtils.getJsonPath(context, jsonPath);2829if(JsonUtils.marshallAndEquals(value, result)){30forwardInboundEventWithNewName(context, context.getProp("eventName"));31}else{32logOnce(context,IfPropertyEquals.class,"Values '%s' and '%s' are not equal", result, value);33}34}35}
`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.
Note
The GraphQL mutation used to persist the data is selected automatically from the `RulesetContext` based on the entity type (e.g., `updateConsignment`). Custom mutations cannot be specified directly; the mutation is always determined by the Context.
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
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.Data2@lombok.AllArgsConstructor3privatestaticclassSample{4String status;5List<Attribute> attributes;6}78// Create the update object9Sample sample =newSample(10 context.getProp("status"),11Attribute.of("New Attribute","New Value")12);1314// Execute the mutation15context.action().mutation(newDynamicUpdateMutation(context, sample));
Error Handling
The `DynamicUpdateMutation` provides built-in error handling:
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:
1importcom.fluentcommerce.util.dynamic.DynamicUtils;2importcom.fluentretail.rubix.rule.meta.ParamString;3importcom.fluentretail.rubix.rule.meta.RuleInfo;4importcom.fluentretail.rubix.v2.context.Context;5importcom.fluentretail.rubix.v2.rule.Rule;67importjava.util.Map;89@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")15publicclassSetEntityFieldimplementsRule{16@Override17publicvoidrun(Context context){18String fieldName = context.getProp("fieldName");19String fieldValue = context.getProp("fieldValue");2021// Create a simple map with the field update22Map<String,Object> updates =Map.of(fieldName, fieldValue);2324// Use DynamicUtils for cleaner syntax25DynamicUtils.mutate(context, updates);26}27}
This example shows how to update complex nested objects using type-based mutations:
1importcom.fluentcommerce.dto.common.Attribute;2importcom.fluentcommerce.util.dynamic.DynamicUtils;3importcom.fluentretail.rubix.rule.meta.EventInfo;4importcom.fluentretail.rubix.rule.meta.ParamString;5importcom.fluentretail.rubix.rule.meta.RuleInfo;6importcom.fluentretail.rubix.v2.context.Context;7importcom.fluentretail.rubix.v2.rule.Rule;89importjava.util.List;1011@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")19publicclassUpdateOrderWithAttributesimplementsRule{20@Override21publicvoidrun(Context context){22String tag1 = context.getProp("tag1");23String attributeName = context.getProp("attributeName");24String attributeValue = context.getProp("attributeValue");2526// Create a typed update object27OrderUpdate update =newOrderUpdate(28 tag1,29List.of(Attribute.of(attributeName, attributeValue))30);3132// Execute the mutation33 context.action().mutation(newDynamicUpdateMutation(context, update));34}3536@lombok.Data37@lombok.AllArgsConstructor38privatestaticclassOrderUpdate{39privateString tag1;40privateList<Attribute> attributes;41}42}
Best Practices
Use DynamicUtils: Prefer `DynamicUtils.mutate()` for simpler syntax when possible
Type Safety: Use POJO classes for complex updates to ensure field validity
Error Handling: Set `errorOnInvalidVariables` to true for strict validation
Logging: Always log mutations for auditing and debugging purposes
Validation: Validate input values before passing them to mutations
`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.
Note
The GraphQL query used to fetch the data is selected automatically from the `RulesetContext` based on the Entity type (e.g., `consignmentById`). However, you can also specify a preferred Query using a Utility method.
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}
78// 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.
1packagecom.example.rule;23importcom.fluentcommerce.util.core.EventUtils;4importcom.fluentcommerce.util.dynamic.DynamicUtils;5importcom.fluentretail.rubix.rule.meta.*;6importcom.fluentretail.rubix.v2.context.Context;7importcom.fluentretail.rubix.v2.rule.Rule;8importjava.util.List;9importjava.util.stream.Stream;10importjava.util.stream.StreamSupport;1112@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")20publicclassIfAllFulfilmentsInStateimplementsRule{21@Override22publicvoidrun(Context context){23List<String> states = context.getPropList("status",String.class);2425Iterable<Fulfilment> fulfilmentIterable =DynamicUtils.queryList(context,"fulfilments",26Fulfilment.class,null);27Stream<Fulfilment> fulfilmentStream =StreamSupport.stream(fulfilmentIterable.spliterator(),false);2829if(fulfilmentStream.allMatch(f -> states.contains(f.status))){30EventUtils.forwardInboundEventWithNewName(context, context.getProp("eventName"));31}32}3334@lombok.Data35publicstaticclassFulfilment{36String status;37}38}
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.Data2publicclassOrderItem{3String id;4String ref;5Double price;6}78//Get Order Items By Order Id9finalList<OrderItem> orderItems =newArrayList<>();10Iterable<OrderItem> orderItemIterable =DynamicUtils.queryList(context,11"order","items",OrderItem.class,12Collections.singletonMap("id", orderId));13orderItemIterable.forEach(orderItems::add);