---
name: graphql-in-rules-reference
description: "Full API reference for GraphQL in Rubix rules: DynamicUtils methods, Apollo patterns, custom mutation classes, infrastructure classes, common code patterns."
version: 1.0
load: on-demand
relevant-skills: rule-scaffold, custom-code, feature-plan, feature-explain, build, module-scaffold
---

# GraphQL in Rules — API Reference

Companion to `knowledge/platform/graphql-in-rules.md` (decision framework). Load this file when scaffolding or analyzing rule code that uses GraphQL.

---

## 1. DynamicUtils — Full API

Package: `com.fluentcommerce.util.dynamic.DynamicUtils`

### 1.1 Queries

#### Query current entity (POJO response)
```java
import com.fluentcommerce.util.dynamic.DynamicUtils;
import com.fluentcommerce.util.dynamic.DynamicUtils.QueryResult;

QueryResult<OrderQueryResult> result = DynamicUtils.query(context, OrderQueryResult.class);
OrderQueryResult order = result.getResult();
String status = order.getStatus();
```

#### Query current entity (POJO + extra parameters)
```java
// Same as above but with additional query filters
QueryResult<OrderQueryResult> result = DynamicUtils.query(context, OrderQueryResult.class,
    ImmutableMap.of("items", ImmutableMap.of("status", ImmutableList.of("ACTIVE"))));
OrderQueryResult order = result.getResult();
```

#### Query current entity (field paths)
```java
List<String> paths = ImmutableList.of("ref", "status", "items.ref", "items.product.name");
QueryResult<QueryDynamicData> result = DynamicUtils.query(context, paths);
JsonNode root = result.getResult().getRoot();
String status = root.path("status").asText();
ImmutableMap<String, JsonNode> flat = result.getResult().flat();
```

#### Cross-entity query (any root, not just context entity)
```java
Map<String, Object> params = new HashMap<>();
params.put("username", ImmutableList.of(actionedBy));

QueryResult<QueryDynamicData> result = DynamicUtils.query(
    context,
    ImmutableList.of("username", "roles.role.name"),
    "users",        // any GraphQL root query name
    params
);
JsonNode edges = result.getResult().getRoot().path("edges");
```

#### Cross-entity query (typed POJO response)
```java
// Same as above but with typed deserialization instead of raw JsonNode
QueryResult<UserQueryResult> result = DynamicUtils.query(
    context,
    ImmutableList.of("username", "roles.role.name"),
    "users",
    params,
    UserQueryResult.class   // 5th param: response type
);
UserQueryResult user = result.getResult();
```

#### Query with parameters (filters, date ranges)
```java
Map<String, Object> params = new HashMap<>();
params.put("items", ImmutableMap.of(
    "createdOn", ImmutableMap.of("from", "2024-01-01T00:00:00Z", "to", "2024-12-31T23:59:59Z")
));
QueryResult<QueryDynamicData> result = DynamicUtils.query(context, paths, null, params);
```

#### Paginated connection query (auto-pagination)
```java
import com.fluentcommerce.util.dynamic.DynamicUtils.QueryListResult;

QueryListResult<Fulfilment> fulfilments = DynamicUtils.queryList(
    context, "fulfilments", Fulfilment.class, null);
for (Fulfilment f : fulfilments) { /* iterates all pages */ }

// Cross-entity with explicit query name:
QueryListResult<Location> locations = DynamicUtils.queryList(
    context, "locations", "locations", Location.class,
    ImmutableMap.of("status", ImmutableList.of("ACTIVE")));
```

#### Single value lookup (JSON path)
```java
JsonNode value = DynamicUtils.getJsonPath(context, "items.0.product.name");
// WARNING: getJsonPath(context, "event.X") is an anti-pattern — use event.getAttribute() instead
```

### 1.2 Mutations

#### Update current entity (auto-resolves mutation name)
```java
Map<String, Object> values = new LinkedHashMap<>();
values.put("attributes", ImmutableList.of(
    ImmutableMap.of("name", "statusHistory", "type", "OBJECT", "value", historyJson)
));
DynamicUtils.mutate(context, values);
```

#### Update with explicit entity type
```java
DynamicUtils.mutateWithVariables(context, "ORDER", ImmutableMap.of("attributes", attributeList));
```

#### Create any entity
```java
Map<String, Object> input = new LinkedHashMap<>();
input.put("ref", "FUL-" + UUID.randomUUID().toString().substring(0, 8));
input.put("type", "HD");
input.put("order", ImmutableMap.of("id", orderId));
input.put("retailer", ImmutableMap.of("id", retailerId));
DynamicUtils.create(context, "createFulfilment", input);
// With custom response fields:
DynamicUtils.create(context, "createFulfilment", input,
    ImmutableList.of("id", "ref", "status", "type", "fromAddress { ref }"));
```

#### Generic mutation (full control)
```java
DynamicUtils.executeMutation(context, "createComment", "CreateCommentInput",
    ImmutableMap.of("input", ImmutableMap.of("entityType", "ORDER", "entityId", orderId, "text", "Approved")),
    ImmutableList.of("id", "text"));
```

### 1.3 Error Handling
```java
QueryResult<QueryDynamicData> result = DynamicUtils.query(context, paths);
if (result.hasErrors()) {
    JsonNode errors = result.getErrors();
    Set<Integer> missingIndices = result.getMissingProductIndices();
}
```

### 1.4 Path Syntax Rules

| Write this | NOT this | Why |
|---|---|---|
| `items.ref` | `items.edges.node.ref` | Auto-inserts `edges.node` for connections |
| `fulfilments.items.product.name` | `fulfilments.edges.node.items.edges.node.product.name` | All connections auto-expand |
| `attributes.name` | `attributes.byName` | `byName` auto-handled as special case |

### 1.5 POJO / DTO Pattern
```java
@Data
public class OrderQueryResult {
    private String id, ref, status, type;
    private List<OrderItem> items;
    @Data
    public static class OrderItem { private String ref; private int quantity; }
}
// DTO placement: plugins/types/<module-alias>/src/main/java/com/fluentcommerce/dto/
```

---

## 2. Apollo-Generated Classes

### 2.1 How It Works
1. `.graphql` files define queries/mutations
2. Apollo codegen (Node.js 18) generates Java classes from `.graphql` + `schema.json`
3. Generated classes provide type-safe builders + typed accessor methods
4. Execute via `context.api().query()` or `context.action().mutation()`

### 2.2 Query Example
```java
GetCustomerByRefQuery query = GetCustomerByRefQuery.builder().ref(customerRef).build();
GetCustomerByRefQuery.Data data = (GetCustomerByRefQuery.Data) context.api().query(query);
if (data != null && data.customer() != null) {
    String firstName = data.customer().firstName();
}
```

### 2.3 Mutation Example
```java
CreateBillingAccountMutation mutation = CreateBillingAccountMutation.builder()
    .input(CreateBillingAccountInput.builder()
        .ref(billingAccountRef).type("CUSTOMER").name(name)
        .retailer(RetailerId.builder().id(retailerId).build())
        .customer(CustomerKey.builder().ref(customerRef).build())
        .build())
    .build();
context.action().mutation(mutation);
```

### 2.4 Service Layer Pattern (OOTB)
```java
FulfilmentService fulfilmentService = new FulfilmentService(context);
FulfilmentChoice fc = fulfilmentChoiceService.getFulfilmentChoice();
LocationService locationService = new LocationService(context);
List<Location> locations = locationService.loadLocationsForNetwork(networkRef, statuses);
```

### 2.5 Connection to List Helper
```java
import com.fluentcommerce.util.dynamic.common.QueryUtils;
ImmutableList<OrderItem> items = QueryUtils.connectionToList(data.order().items());
ImmutableList<String> itemRefs = QueryUtils.connectionToList(data.order().items(), item -> item.ref());
```

### 2.6 Apollo Codegen Setup
```bash
# Requires Node.js 18. schema.json must be in the project.
# .graphql files in: src/main/graphql/com/fluentcommerce/graphql/
cd plugins/ && mvn clean install -pl rules/<module-alias> -am
```

### 2.7 Null-check + RubixException pattern
```java
GetOrderByIdQuery.Data data = (GetOrderByIdQuery.Data) context.api()
    .query((Query) GetOrderByIdQuery.builder().id(orderId).build());
if (data == null || data.orderById() == null) {
    throw new RubixException(400, String.format("No order found with id %s", orderId));
}
```

### 2.8 Apollo `Input<T>` optionals
```java
CreateFulfilmentInput input = CreateFulfilmentInput.builder()
    .ref("FUL-001").type("HD")                           // required — @Nonnull
    .order(OrderId.builder().id(orderId).build())         // required
    .attributes(attributeList)                            // optional — Input.fromNullable()
    .build();
// Input.absent() = field not sent. Input.fromNullable(null) = field sent as null.
```

### 2.9 .graphql file examples
```graphql
# src/main/graphql/.../GetSettings.graphql
query GetSettings($settingsNames:[String!]!, $locationId:Int=-1, $retailerId:Int=-1) {
    location: settings(name:$settingsNames, context:"AGENT", contextId:[$locationId], first:1000) {
      edges { node { context id name value lobValue } }
    }
    retailer: settings(name:$settingsNames, context:"RETAILER", contextId:[$retailerId], first:1000) {
      edges { node { context id name value lobValue } }
    }
    account: settings(name:$settingsNames, context:"ACCOUNT", first:1000) {
      edges { node { context id name value lobValue } }
    }
}
```

---

## 3. Custom Apollo-Style Mutation Classes (Advanced)

When you need typed responses but don't have Apollo codegen:

```java
public class DynamicCreateVariantProductMutation
    implements Mutation<Data, Data, Variables> {
    @Override
    public String queryDocument() {
        return "mutation CreateVariantProduct($input: CreateVariantProductInput!) { "
             + "r: createVariantProduct(input: $input) { id ref name type status } }";
    }
    // Variables extends Operation.Variables, Data reads via ResponseReader
}
```

| Requirement | Detail |
|---|---|
| `operationId()` | MUST return non-null (`UUID.randomUUID().toString()`). Null = NPE. |
| `Variables.valueMap()` | MUST override if using `QueryDynamicVariables`. Without it, variables silently dropped. |
| `Variables.marshaller()` | Use `MapBasedInputFieldMarshaller` for `Map<String, Object>` serialization. |
| Response alias | Use short alias (e.g., `r:`) to simplify response parsing. |

---

## 4. Infrastructure Classes

| Class | Purpose |
|---|---|
| `DynamicUtils` | Facade — all query/mutation entry points |
| `DynamicEntityQuery` | Query builder — auto-generates GraphQL from context + field paths |
| `DynamicConnectionQuery<T>` | Paginated iterator — auto-cursor management, implements `Iterable<T>` |
| `DynamicUpdateMutation` | Update mutation — auto-resolves name + required params from context |
| `DynamicMutation` | Generic mutation — any create/update/custom mutation |
| `QueryDynamicData` | Response wrapper — `as(Class)`, `get(path)`, `flat()`, `hasErrors()` |
| `QueryDynamicVariables` | Variable container — MUST override `valueMap()` |
| `GraphQLIntrospectionUtils` | Schema introspection — discovers args, types, input fields. Caches results. |
| `QueryUtils` | `connectionToList()` — reflection-based connection-to-list for Apollo responses |
| `JsonUtils` | POJO/JsonNode/Map conversion, path navigation |

---

## 5. Common Patterns

### Query → Mutation → Event (standard rule flow)
```java
QueryResult<OrderQueryResult> qr = DynamicUtils.query(context, OrderQueryResult.class);
OrderQueryResult order = qr.getResult();
Map<String, Object> input = new LinkedHashMap<>();
input.put("ref", order.getRef() + "-CM-" + UUID.randomUUID().toString().substring(0, 5));
input.put("type", "APPEASEMENT");
input.put("order", ImmutableMap.of("id", order.getId()));
DynamicUtils.create(context, "createCreditMemo", input);
context.action().sendEvent(Event.builder().name("ON_APPEASEMENT_CREATED")
    .entityType(context.getEvent().getEntityType())
    .entityRef(context.getEvent().getEntityRef())
    .entityId(context.getEvent().getEntityId()).build());
```

### Update entity attributes
```java
DynamicUtils.mutate(context, ImmutableMap.of("attributes", ImmutableList.of(
    ImmutableMap.of("name", "statusHistory", "type", "OBJECT", "value", historyJson),
    ImmutableMap.of("name", "lastUpdatedBy", "type", "STRING", "value", username)
)));
```

### Settings lookup
```java
import com.fluentcommerce.util.dynamic.common.SettingUtils;
JsonNode config = SettingUtils.getSettingValue(context, "my.setting.key");
String threshold = config.path("maxAmount").asText("100.00");
```
