Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ TeaQL applies five safeguards to application operations:
which carries identity, trace, and runtime capabilities.
2. **Declared intent** — reads require `.comment(...).purpose(...)`; writes use
`.auditAs(...)` before an execution terminal becomes available.
3. **Policy gates** — `RequestPolicy` can inspect or reject select, insert,
update, delete, and recover operations.
3. **Policy gates** — `QueryPolicy` reviews reads; `MutationPolicy` reviews one
immutable graph-level `MutationPlan` before the first provider write.
4. **Explicit capabilities** — optional operations such as HTTP tools, dynamic
fields, and business ID generation are supplied through dedicated modules and
registered runtime capabilities.
Expand Down Expand Up @@ -110,9 +110,10 @@ Mutations declare an audit action:
task.auditAs("Move task to Done").save(userContext);
```

Applications can replace runtime services such as `RequestPolicy`,
`RuntimeLogSink`, `DataServiceRegistry`, `InternalIdGenerationService`, and
`EntityMetaFactory` in their integration layer.
Applications can replace runtime services such as `QueryPolicy`, the
`MutationPolicyRegistry`, `MutationPolicyApprovalProvider`, `RuntimeLogSink`,
`DataServiceRegistry`, `InternalIdGenerationService`, and `EntityMetaFactory`
in their integration layer.

Query and Mutation execution logs are enabled by default. The built-in default
sink is safe for ordinary operator output: it includes intent, trace, elapsed
Expand Down
17 changes: 10 additions & 7 deletions docs/2026-06-12-java-data-service-provider-architecture.MD
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ Core request-model concepts:

```text
SearchRequest
MutationRequest
MutationPlan
PersistenceMutation
AggregationRequest
Criteria
Expression
Expand All @@ -113,7 +114,8 @@ TransactionExecutor
SchemaExecutor
DataServiceCapabilities
ExecutionMetadata
RequestPolicy
QueryPolicy
MutationPolicy
InternalIdGenerationService
RuntimeLogSink
TransactionCallback
Expand Down Expand Up @@ -310,7 +312,8 @@ These interfaces and contracts belong in `teaql-core`:

```text
UserContext
RequestPolicy
QueryPolicy
MutationPolicy
InternalIdGenerationService
DataServiceRegistry
DataServiceExecutor
Expand Down Expand Up @@ -398,7 +401,7 @@ It accepts and returns TeaQL concepts:

```text
QueryRequest
MutationRequest
PersistenceMutation
AggregationRequest
SchemaRequest
TransactionCallback
Expand Down Expand Up @@ -532,7 +535,7 @@ public interface QueryExecutor extends DataServiceExecutor {

```java
public interface MutationExecutor extends DataServiceExecutor {
MutationResult mutate(UserContext ctx, MutationRequest request);
MutationResult mutate(UserContext ctx, PersistenceMutation mutation);
}
```

Expand Down Expand Up @@ -682,7 +685,7 @@ TeaQLRuntime runtime = TeaQLRuntime.builder()
.metadata(entityMetaFactory)
.dataService("sql", sqlDataService)
.dataService("memory", memoryDataService)
.requestPolicy(new PurposeRequestPolicy())
.queryPolicy(new PurposeQueryPolicy())
.logSink(LogManager.getInstance())
.build();

Expand Down Expand Up @@ -832,7 +835,7 @@ Target module layout:
teaql-core
TeaQL world model and runtime contracts.
Includes Bean, Entity, EntityProperty, Relation, metadata descriptors,
request models, UserContext, RequestPolicy, Data Service contracts,
request models, UserContext, QueryPolicy, MutationPolicy, Data Service contracts,
InternalIdGenerationService, transaction contracts, and execution log contracts.
No Spring, JDBC, SQL dialect, Android, GraphQL, web rendering, or concrete backend implementation.

Expand Down
26 changes: 25 additions & 1 deletion examples/order-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ mvn -q install -DskipTests
mvn -q exec:java -pl java-app-console
```

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.
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. It also installs a customer-owned `OrderMutationPolicy`, denies
an unauthorized high-value order as one complete graph, and queries SQLite to
prove that zero order rows were written. The second run demonstrates
idempotency and repeats the policy proof.

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.

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

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.

### Customize whole-graph mutation policy

`OrderMutationPolicy.java` is application-owned code. It reviews the immutable
`MutationPlan` after Checker/Fix and graph assembly but before the first
provider write. The example requires a trusted `MutationAuthority` capability
for orders above `10000.00` and returns the stable denial code
`ORDER-HIGH-VALUE-AUTHORITY-REQUIRED` when that authority is absent.

`OrderManagementApp.context(...)` shows the three customer configuration
points:

1. register policy by the stable `CustomerOrder.saveGraph` request key;
2. provide approval for the exact policy identity and fingerprint;
3. install trusted request authority in `UserContext`, outside remote input.

Change the installed permission set to
`Set.of(OrderMutationPolicy.HIGH_VALUE_PERMISSION)` to exercise the allowed
path. Do not place trusted authority in request JSON or generated entities.
### Materialized-list hard limit

`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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.teaql.example;

import java.util.Set;

/** Trusted application authority installed in UserContext by server-side code. */
public record MutationAuthority(Set<String> permissions) {
public MutationAuthority {
permissions = Set.copyOf(permissions == null ? Set.of() : permissions);
}

public boolean permits(String permission) {
return permissions.contains(permission);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import com.teaql.ordermanagementservice.ordersearchpreset.OrderSearchPreset;
import io.teaql.core.DataServiceExecutor;
import io.teaql.core.InternalIdGenerationService;
import io.teaql.core.MutationPolicyApproval;
import io.teaql.core.SmartList;
import io.teaql.core.TeaQLRuntimeException;
import io.teaql.core.UserContext;
import io.teaql.core.meta.EntityMetaFactory;
import io.teaql.core.meta.SimpleEntityMetaFactory;
Expand All @@ -24,7 +26,10 @@
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import javax.sql.DataSource;
Expand Down Expand Up @@ -97,6 +102,8 @@ public static void main(String[] args) throws Exception {
preset.auditAs("Save idempotent quick-start search preset").save(context);
System.out.println("[mutation] saved preset #" + preset.getId());
} else System.out.println("[mutation] preset #" + presets.get(0).getId() + " already exists");

demonstrateMutationPolicy(context, platform);
}

private static UserContext context(DataServiceExecutor executor) {
Expand All @@ -105,10 +112,61 @@ private static UserContext context(DataServiceExecutor executor) {
EntityMetaFactory.registerGlobal(metadata);
AtomicLong ids = new AtomicLong(1000);
InternalIdGenerationService idGeneration = (context, entity) -> ids.getAndIncrement();
OrderMutationPolicy orderPolicy = new OrderMutationPolicy();
TeaQLRuntime runtime = TeaQLRuntime.builder().metadata(metadata)
.dataService("default", executor).dataService("sqlite", executor)
.idGenerationService(idGeneration).build();
return new DefaultUserContext(runtime);
.idGenerationService(idGeneration)
.mutationPolicyRegistry(requestKey -> "CustomerOrder.saveGraph".equals(requestKey)
? Optional.of(orderPolicy)
: Optional.empty())
.mutationPolicyApprovalProvider(identity -> identity.equals(orderPolicy.identity())
? Optional.of(new MutationPolicyApproval(
identity,
"example-security-review",
Instant.parse("2026-09-29T12:00:00Z")))
: Optional.empty())
.build();
DefaultUserContext context = new DefaultUserContext(runtime);
context.putAttribute(
MutationAuthority.class.getName(), new MutationAuthority(Set.of()));
return context;
}

private static void demonstrateMutationPolicy(
UserContext context, CommercePlatform platform) {
String rejectedOrderNumber = "WEB-2026-HIGH-VALUE-DENIED";
Customer rejectedCustomer = new Customer()
.updateName("Rejected High Value Customer")
.updateEmail("not-persisted@example.invalid")
.updateCommercePlatform(platform);
CustomerOrder rejectedOrder = new CustomerOrder()
.updateOrderNumber(rejectedOrderNumber)
.updateOrderDate(LocalDate.of(2026, 9, 29))
.updateTotalAmount(new BigDecimal("25000.00"))
.updateStatusToPending()
.updateCustomer(rejectedCustomer)
.updateCommercePlatform(platform);

try {
rejectedOrder.auditAs("Demonstrate customer high-value order policy")
.save(context);
throw new IllegalStateException("The high-value mutation should have been denied");
} catch (TeaQLRuntimeException expected) {
if (!expected.getMessage().contains("ORDER-HIGH-VALUE-AUTHORITY-REQUIRED")) {
throw expected;
}
}

SmartList<CustomerOrder> rejectedRows = Q.customerOrders()
.withOrderNumberIs(rejectedOrderNumber)
.comment("Verify the denied high-value graph wrote no order")
.purpose("Prove customer MutationPolicy denial is atomic")
.executeForList(context);
if (!rejectedRows.isEmpty()) {
throw new IllegalStateException("Denied high-value order was persisted");
}
System.out.println(
"[mutation-policy] denied high-value graph before persistence; verified 0 order rows");
}

private record DriverManagerDataSource(String url) implements DataSource {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.teaql.example;

import io.teaql.core.MutationDecision;
import io.teaql.core.MutationOperationKind;
import io.teaql.core.MutationPlan;
import io.teaql.core.MutationPolicy;
import io.teaql.core.MutationPolicyIdentity;
import io.teaql.core.UserContext;
import java.math.BigDecimal;
import java.util.List;

/** Customer-owned whole-graph policy for the order-management example. */
public final class OrderMutationPolicy implements MutationPolicy {
public static final String HIGH_VALUE_PERMISSION = "order.submitHighValue";
public static final BigDecimal HIGH_VALUE_THRESHOLD = new BigDecimal("10000.00");
public static final MutationPolicyIdentity IDENTITY = new MutationPolicyIdentity(
"example.order-mutation", "1.0.0", "example:order-policy-2026-09-29");

@Override
public MutationPolicyIdentity identity() {
return IDENTITY;
}

@Override
public MutationDecision review(UserContext context, MutationPlan plan) {
boolean containsHighValueOrder = plan.operations().stream()
.filter(operation -> operation.kind() == MutationOperationKind.CREATE
|| operation.kind() == MutationOperationKind.UPDATE)
.map(operation -> operation.changedValues().get("totalAmount"))
.filter(BigDecimal.class::isInstance)
.map(BigDecimal.class::cast)
.anyMatch(amount -> amount.compareTo(HIGH_VALUE_THRESHOLD) > 0);

if (!containsHighValueOrder) {
return MutationDecision.allow();
}

MutationAuthority authority = context.capability(MutationAuthority.class);
if (authority != null && authority.permits(HIGH_VALUE_PERMISSION)) {
return MutationDecision.allow();
}

return MutationDecision.deny(
"ORDER-HIGH-VALUE-AUTHORITY-REQUIRED",
"Orders above 10000.00 require the high-value order authority",
List.of("totalAmount"));
}
}
25 changes: 25 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/MutationDecision.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.teaql.core;

import java.util.List;

public record MutationDecision(
Verdict verdict, String code, String message, List<String> fieldPaths) {
public enum Verdict { ALLOW, DENY }

public MutationDecision {
fieldPaths = List.copyOf(fieldPaths == null ? List.of() : fieldPaths);
if (verdict == Verdict.DENY && (code == null || code.isBlank())) {
throw new IllegalArgumentException("A denied mutation decision requires a stable code");
}
}

public static MutationDecision allow() {
return new MutationDecision(Verdict.ALLOW, null, null, List.of());
}

public static MutationDecision deny(String code, String message, List<String> fieldPaths) {
return new MutationDecision(Verdict.DENY, code, message, fieldPaths);
}

public boolean allowed() { return verdict == Verdict.ALLOW; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.teaql.core;

public interface MutationExecutor extends DataServiceExecutor {
MutationResult mutate(UserContext context, MutationRequest request);
MutationResult mutate(UserContext context, PersistenceMutation mutation);
}
19 changes: 19 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/MutationOperation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.teaql.core;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public record MutationOperation(
MutationOperationKind kind,
EntityKey entity,
Long originalVersion,
Map<String, Object> changedValues) {
public MutationOperation {
Objects.requireNonNull(kind, "kind");
Objects.requireNonNull(entity, "entity");
changedValues = Collections.unmodifiableMap(new LinkedHashMap<>(
changedValues == null ? Map.of() : changedValues));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.teaql.core;

public enum MutationOperationKind { CREATE, UPDATE, DELETE, RECOVER }
18 changes: 18 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/MutationPlan.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.teaql.core;

import java.util.List;
import java.util.Objects;

public record MutationPlan(
String executionId,
String requestKey,
String rootEntityType,
String auditReason,
List<MutationOperation> operations) {
public MutationPlan {
Objects.requireNonNull(executionId, "executionId");
Objects.requireNonNull(requestKey, "requestKey");
Objects.requireNonNull(rootEntityType, "rootEntityType");
operations = List.copyOf(operations == null ? List.of() : operations);
}
}
6 changes: 6 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/MutationPolicy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.teaql.core;

public interface MutationPolicy {
MutationPolicyIdentity identity();
MutationDecision review(UserContext context, MutationPlan plan);
}
13 changes: 13 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/MutationPolicyApproval.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.teaql.core;

import java.time.Instant;
import java.util.Objects;

public record MutationPolicyApproval(
MutationPolicyIdentity policy, String approvedBy, Instant approvedAt) {
public MutationPolicyApproval {
Objects.requireNonNull(policy, "policy");
Objects.requireNonNull(approvedBy, "approvedBy");
Objects.requireNonNull(approvedAt, "approvedAt");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.teaql.core;

import java.util.Optional;

@FunctionalInterface
public interface MutationPolicyApprovalProvider {
Optional<MutationPolicyApproval> findApproval(MutationPolicyIdentity policy);

static MutationPolicyApprovalProvider none() { return policy -> Optional.empty(); }
}
Loading
Loading