Skip to content

Commit 7cfa2eb

Browse files
author
Philip Z
committed
examples: demonstrate customer mutation policy
1 parent 2e72c9f commit 7cfa2eb

4 files changed

Lines changed: 147 additions & 3 deletions

File tree

‎examples/order-management/README.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ mvn -q install -DskipTests
88
mvn -q exec:java -pl java-app-console
99
```
1010

11-
The first run creates `.local/order.db`, ensures schema from generated metadata, seeds through generated entities, performs a governed query, and saves an audited preset. The second run demonstrates idempotency.
11+
The first run creates `.local/order.db`, ensures schema from generated metadata,
12+
seeds through generated entities, performs a governed query, and saves an
13+
audited preset. It also installs a customer-owned `OrderMutationPolicy`, denies
14+
an unauthorized high-value order as one complete graph, and queries SQLite to
15+
prove that zero order rows were written. The second run demonstrates
16+
idempotency and repeats the policy proof.
1217

1318
Read `java-app-console/.../OrderManagementApp.java` first (handwritten), then `java-lib-core/lib/.../Q.java`, `CustomerOrderRequest.java`, and `CustomerOrder.java` (generated). Java is the naming and governance gold standard: `comment(...)` may appear anywhere before `purpose(...)`; only the purposed request exposes execute methods.
1419

@@ -19,6 +24,25 @@ Expect one `WEB-2026-001` row dated `2026-08-12` with amount `129.95`. The first
1924
## Customize it
2025

2126
Change the `withOrderNumberContaining` filter, ordering, or projection in the app and rerun. Add business behavior only under `java-app-console`; regenerate everything under `java-lib-core`. This library was generated from the shared Order Management model used by the six-language example suite, but that model and the generator are not runtime prerequisites.
27+
28+
### Customize whole-graph mutation policy
29+
30+
`OrderMutationPolicy.java` is application-owned code. It reviews the immutable
31+
`MutationPlan` after Checker/Fix and graph assembly but before the first
32+
provider write. The example requires a trusted `MutationAuthority` capability
33+
for orders above `10000.00` and returns the stable denial code
34+
`ORDER-HIGH-VALUE-AUTHORITY-REQUIRED` when that authority is absent.
35+
36+
`OrderManagementApp.context(...)` shows the three customer configuration
37+
points:
38+
39+
1. register policy by the stable `CustomerOrder.saveGraph` request key;
40+
2. provide approval for the exact policy identity and fingerprint;
41+
3. install trusted request authority in `UserContext`, outside remote input.
42+
43+
Change the installed permission set to
44+
`Set.of(OrderMutationPolicy.HIGH_VALUE_PERMISSION)` to exercise the allowed
45+
path. Do not place trusted authority in request JSON or generated entities.
2246
### Materialized-list hard limit
2347

2448
`executeForList` protects the service by applying a default hard limit of 10,000 rows. A requested page size above that ceiling fails explicitly. Trusted application code can call `hardLimit(...)` to override the outer-query ceiling. **Caution:** most applications should not override it; do so only for a reviewed, exceptional requirement. This setting does not describe streaming execution.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.teaql.example;
2+
3+
import java.util.Set;
4+
5+
/** Trusted application authority installed in UserContext by server-side code. */
6+
public record MutationAuthority(Set<String> permissions) {
7+
public MutationAuthority {
8+
permissions = Set.copyOf(permissions == null ? Set.of() : permissions);
9+
}
10+
11+
public boolean permits(String permission) {
12+
return permissions.contains(permission);
13+
}
14+
}

‎examples/order-management/java-app-console/src/main/java/com/teaql/example/OrderManagementApp.java‎

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import com.teaql.ordermanagementservice.ordersearchpreset.OrderSearchPreset;
99
import io.teaql.core.DataServiceExecutor;
1010
import io.teaql.core.InternalIdGenerationService;
11+
import io.teaql.core.MutationPolicyApproval;
1112
import io.teaql.core.SmartList;
13+
import io.teaql.core.TeaQLRuntimeException;
1214
import io.teaql.core.UserContext;
1315
import io.teaql.core.meta.EntityMetaFactory;
1416
import io.teaql.core.meta.SimpleEntityMetaFactory;
@@ -24,7 +26,10 @@
2426
import java.sql.DriverManager;
2527
import java.sql.SQLException;
2628
import java.sql.SQLFeatureNotSupportedException;
29+
import java.time.Instant;
2730
import java.time.LocalDate;
31+
import java.util.Optional;
32+
import java.util.Set;
2833
import java.util.concurrent.atomic.AtomicLong;
2934
import java.util.logging.Logger;
3035
import javax.sql.DataSource;
@@ -97,6 +102,8 @@ public static void main(String[] args) throws Exception {
97102
preset.auditAs("Save idempotent quick-start search preset").save(context);
98103
System.out.println("[mutation] saved preset #" + preset.getId());
99104
} else System.out.println("[mutation] preset #" + presets.get(0).getId() + " already exists");
105+
106+
demonstrateMutationPolicy(context, platform);
100107
}
101108

102109
private static UserContext context(DataServiceExecutor executor) {
@@ -105,10 +112,61 @@ private static UserContext context(DataServiceExecutor executor) {
105112
EntityMetaFactory.registerGlobal(metadata);
106113
AtomicLong ids = new AtomicLong(1000);
107114
InternalIdGenerationService idGeneration = (context, entity) -> ids.getAndIncrement();
115+
OrderMutationPolicy orderPolicy = new OrderMutationPolicy();
108116
TeaQLRuntime runtime = TeaQLRuntime.builder().metadata(metadata)
109117
.dataService("default", executor).dataService("sqlite", executor)
110-
.idGenerationService(idGeneration).build();
111-
return new DefaultUserContext(runtime);
118+
.idGenerationService(idGeneration)
119+
.mutationPolicyRegistry(requestKey -> "CustomerOrder.saveGraph".equals(requestKey)
120+
? Optional.of(orderPolicy)
121+
: Optional.empty())
122+
.mutationPolicyApprovalProvider(identity -> identity.equals(orderPolicy.identity())
123+
? Optional.of(new MutationPolicyApproval(
124+
identity,
125+
"example-security-review",
126+
Instant.parse("2026-09-29T12:00:00Z")))
127+
: Optional.empty())
128+
.build();
129+
DefaultUserContext context = new DefaultUserContext(runtime);
130+
context.putAttribute(
131+
MutationAuthority.class.getName(), new MutationAuthority(Set.of()));
132+
return context;
133+
}
134+
135+
private static void demonstrateMutationPolicy(
136+
UserContext context, CommercePlatform platform) {
137+
String rejectedOrderNumber = "WEB-2026-HIGH-VALUE-DENIED";
138+
Customer rejectedCustomer = new Customer()
139+
.updateName("Rejected High Value Customer")
140+
.updateEmail("not-persisted@example.invalid")
141+
.updateCommercePlatform(platform);
142+
CustomerOrder rejectedOrder = new CustomerOrder()
143+
.updateOrderNumber(rejectedOrderNumber)
144+
.updateOrderDate(LocalDate.of(2026, 9, 29))
145+
.updateTotalAmount(new BigDecimal("25000.00"))
146+
.updateStatusToPending()
147+
.updateCustomer(rejectedCustomer)
148+
.updateCommercePlatform(platform);
149+
150+
try {
151+
rejectedOrder.auditAs("Demonstrate customer high-value order policy")
152+
.save(context);
153+
throw new IllegalStateException("The high-value mutation should have been denied");
154+
} catch (TeaQLRuntimeException expected) {
155+
if (!expected.getMessage().contains("ORDER-HIGH-VALUE-AUTHORITY-REQUIRED")) {
156+
throw expected;
157+
}
158+
}
159+
160+
SmartList<CustomerOrder> rejectedRows = Q.customerOrders()
161+
.withOrderNumberIs(rejectedOrderNumber)
162+
.comment("Verify the denied high-value graph wrote no order")
163+
.purpose("Prove customer MutationPolicy denial is atomic")
164+
.executeForList(context);
165+
if (!rejectedRows.isEmpty()) {
166+
throw new IllegalStateException("Denied high-value order was persisted");
167+
}
168+
System.out.println(
169+
"[mutation-policy] denied high-value graph before persistence; verified 0 order rows");
112170
}
113171

114172
private record DriverManagerDataSource(String url) implements DataSource {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.teaql.example;
2+
3+
import io.teaql.core.MutationDecision;
4+
import io.teaql.core.MutationOperationKind;
5+
import io.teaql.core.MutationPlan;
6+
import io.teaql.core.MutationPolicy;
7+
import io.teaql.core.MutationPolicyIdentity;
8+
import io.teaql.core.UserContext;
9+
import java.math.BigDecimal;
10+
import java.util.List;
11+
12+
/** Customer-owned whole-graph policy for the order-management example. */
13+
public final class OrderMutationPolicy implements MutationPolicy {
14+
public static final String HIGH_VALUE_PERMISSION = "order.submitHighValue";
15+
public static final BigDecimal HIGH_VALUE_THRESHOLD = new BigDecimal("10000.00");
16+
public static final MutationPolicyIdentity IDENTITY = new MutationPolicyIdentity(
17+
"example.order-mutation", "1.0.0", "example:order-policy-2026-09-29");
18+
19+
@Override
20+
public MutationPolicyIdentity identity() {
21+
return IDENTITY;
22+
}
23+
24+
@Override
25+
public MutationDecision review(UserContext context, MutationPlan plan) {
26+
boolean containsHighValueOrder = plan.operations().stream()
27+
.filter(operation -> operation.kind() == MutationOperationKind.CREATE
28+
|| operation.kind() == MutationOperationKind.UPDATE)
29+
.map(operation -> operation.changedValues().get("totalAmount"))
30+
.filter(BigDecimal.class::isInstance)
31+
.map(BigDecimal.class::cast)
32+
.anyMatch(amount -> amount.compareTo(HIGH_VALUE_THRESHOLD) > 0);
33+
34+
if (!containsHighValueOrder) {
35+
return MutationDecision.allow();
36+
}
37+
38+
MutationAuthority authority = context.capability(MutationAuthority.class);
39+
if (authority != null && authority.permits(HIGH_VALUE_PERMISSION)) {
40+
return MutationDecision.allow();
41+
}
42+
43+
return MutationDecision.deny(
44+
"ORDER-HIGH-VALUE-AUTHORITY-REQUIRED",
45+
"Orders above 10000.00 require the high-value order authority",
46+
List.of("totalAmount"));
47+
}
48+
}

0 commit comments

Comments
 (0)