1977 lines
103 KiB
Markdown
1977 lines
103 KiB
Markdown
# GraphQL Advanced Capability Expansion Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Stable GraphQL API 실행 플랫폼 위에 Persisted Operation, WebSocket·SSE Subscription, optional replay, Federation Subgraph, Code Generation과 제한된 Spring Data 호환 기능을 추가하고 RSocket·HTTP GET·Incremental Delivery를 격리된 Experimental capability로 검증한다.
|
||
|
||
**Architecture:** Advanced capability는 `modules/graphql-advanced`에만 존재하며 Stable module의 public contract를 소비하되 Stable starter의 transitive dependency가 되지 않는다. 모든 capability는 `backend.graphql.advanced.*` feature flag와 capability별 release evidence를 요구하며, 장기 연결·schema composition·provider-specific behavior가 Stable query/mutation path를 오염시키지 않도록 분리한다.
|
||
|
||
**Tech Stack:** Stable GraphQL platform, Java 21, Spring Boot 4.1 BOM, Spring for GraphQL 2.0, Boot-managed GraphQL Java v25, GraphQL Java DataLoader, Spring WebFlux WebSocket, Spring GraphQL SSE/RSocket, federation-jvm, Micrometer, JUnit 5, Reactor Test.
|
||
|
||
## Global Constraints
|
||
|
||
- 이 계획은 Stable 구현 계획 Task `1–48`과 Stable Release Gate가 통과한 후 시작한다.
|
||
- Advanced module root는 `modules/graphql-advanced`이다.
|
||
- Root package는 `io.backend.skeleton.graphql.advanced`이다.
|
||
- 모든 capability는 `backend.graphql.advanced.*` 아래의 명시적 feature flag를 요구한다.
|
||
- Stable starter는 Advanced module에 compile·runtime dependency를 갖지 않는다.
|
||
- Persisted Operation은 Parse cache 및 Response cache와 다른 기능이다.
|
||
- WebSocket protocol은 `graphql-transport-ws`만 지원하고 `subscriptions-transport-ws`를 신규 지원하지 않는다.
|
||
- GraphQL Subscription은 Messaging의 ACK·offset·replay·DLQ를 대체하지 않는다.
|
||
- Slow consumer 기본 정책은 무음 event drop이 아니라 종료다.
|
||
- SSE는 Distinct Connection subscription transport이며 Query·Mutation response mode가 아니다.
|
||
- Replay extension의 durability·offset guarantee는 Messaging platform이 소유한다.
|
||
- Single Executable Schema는 계속 Stable 기본값이다.
|
||
- Federation은 Subgraph capability만 이 저장소에 구현하고 Router/Supergraph 운영은 별도 프로젝트가 소유한다.
|
||
- Client/transport DTO code generation은 허용하지만 Domain Entity·Use Case·Repository generation은 금지한다.
|
||
- Spring Data automatic GraphQL repository exposure는 allowlist compatibility module로만 제공한다.
|
||
- RSocket, HTTP GET, Incremental Delivery는 승격 ADR 전까지 Experimental이다.
|
||
- Advanced capability가 Stable HTTP POST, error, security, cost, DataLoader와 observability guardrail을 우회해서는 안 된다.
|
||
- 모든 task는 red-green TDD와 독립 commit으로 끝난다.
|
||
|
||
---
|
||
|
||
## Advanced Module Map
|
||
|
||
```text
|
||
modules/graphql-advanced/
|
||
├── graphql-advanced-bootstrap
|
||
├── graphql-persisted-operation
|
||
├── graphql-websocket
|
||
├── graphql-subscription
|
||
├── graphql-sse
|
||
├── graphql-dataloader-chaining
|
||
├── graphql-federation
|
||
├── graphql-codegen
|
||
├── graphql-spring-data-compat
|
||
├── graphql-rsocket
|
||
├── graphql-http-draft
|
||
└── graphql-incremental-delivery
|
||
```
|
||
|
||
## Capability Classification
|
||
|
||
| Capability | Initial grade | Promotion evidence |
|
||
|---|---|---|
|
||
| Persisted Operation | Advanced Stable | durable registry, block propagation, schema/usage gate |
|
||
| WebSocket Subscription | Advanced Stable | auth, backpressure, soak, cancellation, drain |
|
||
| SSE Subscription | Advanced | HTTP/2 connection scale, proxy behavior, auth |
|
||
| Messaging-backed Replay | Advanced Extension | snapshot/live gap, history loss, authorization |
|
||
| Chained DataLoader | Advanced | dispatch/query-count regression |
|
||
| Federation Subgraph | Advanced | composition, router integration, failure/latency |
|
||
| Client Codegen | Optional Stable Tooling | generated-source compatibility |
|
||
| Spring Data Compat | Restricted Advanced | allowlist, projection, pagination/query guard |
|
||
| RSocket | Experimental | explicit consumers and transport evidence |
|
||
| HTTP GET | Experimental | draft, cache, CSRF and URL disclosure evidence |
|
||
| Incremental Delivery | Experimental | engine/transport interoperability and client negotiation |
|
||
|
||
## Delivery Phases
|
||
|
||
| Phase | Tasks | Result |
|
||
|---|---:|---|
|
||
| Boundary | 1 | Stable/Advanced dependency and feature flag isolation |
|
||
| Persisted Operations | 2–4 | Registry, execution lookup and G4 admin |
|
||
| Live Transport | 5–10 | WebSocket, auth, backpressure, ordering, SSE, replay |
|
||
| Data·Schema Extensions | 11–15 | Chained loader, Federation, Codegen, Spring Data compat |
|
||
| Experimental Transport | 16–18 | RSocket, HTTP GET, Incremental Delivery |
|
||
| Promotion | 19 | Capability-specific release and promotion gate |
|
||
|
||
---
|
||
|
||
### Task 1: Advanced Module Boundary와 Feature Flag
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedCapability.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedFeatureFlags.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuard.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedDependencyRules.java`
|
||
- Test: `modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuardTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Stable GraphQL API 실행 플랫폼의 public contract와 Spring Boot environment.
|
||
- Produces: Advanced·Experimental 모듈이 Stable starter에 자동 유입되지 않도록 하는 dependency·feature flag 경계.
|
||
|
||
**Implementation requirements:**
|
||
- 모든 Advanced capability는 `backend.graphql.advanced.*` 아래의 명시적 flag를 요구한다.
|
||
- Stable starter는 advanced module에 compile/runtime dependency를 갖지 않는다.
|
||
- Advanced module은 Stable public types를 소비할 수 있지만 Stable module을 수정하지 않는다.
|
||
- Experimental capability는 production에서 별도 승인 profile 없이는 시작되지 않는다.
|
||
- Capability 상태는 `ADVANCED_STABLE`, `EXPERIMENTAL`, `DISABLED`로 구분한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlAdvancedModuleGuardTest {
|
||
@org.junit.jupiter.api.Test
|
||
void disabledCapabilityCannotStart() {
|
||
var flags = GraphQlAdvancedFeatureFlags.disabled();
|
||
var guard = new GraphQlAdvancedModuleGuard(flags);
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> guard.requireEnabled(
|
||
GraphQlAdvancedCapability.PERSISTED_OPERATION))
|
||
.isInstanceOf(
|
||
GraphQlAdvancedCapabilityDisabledException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.bootstrap.GraphQlAdvancedModuleGuardTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public enum GraphQlAdvancedCapability {
|
||
PERSISTED_OPERATION,
|
||
WEBSOCKET_SUBSCRIPTION,
|
||
SSE_SUBSCRIPTION,
|
||
FEDERATION_SUBGRAPH,
|
||
CODE_GENERATION,
|
||
SPRING_DATA_COMPAT,
|
||
RSOCKET,
|
||
HTTP_GET,
|
||
INCREMENTAL_DELIVERY
|
||
}
|
||
|
||
public record GraphQlAdvancedFeatureFlags(
|
||
java.util.Set<GraphQlAdvancedCapability> enabled) {
|
||
|
||
public static GraphQlAdvancedFeatureFlags disabled() {
|
||
return new GraphQlAdvancedFeatureFlags(
|
||
java.util.Set.of());
|
||
}
|
||
|
||
public boolean isEnabled(
|
||
GraphQlAdvancedCapability capability) {
|
||
return enabled.contains(capability);
|
||
}
|
||
}
|
||
|
||
public final class GraphQlAdvancedModuleGuard {
|
||
private final GraphQlAdvancedFeatureFlags flags;
|
||
|
||
public GraphQlAdvancedModuleGuard(
|
||
GraphQlAdvancedFeatureFlags flags) {
|
||
this.flags = flags;
|
||
}
|
||
|
||
public void requireEnabled(
|
||
GraphQlAdvancedCapability capability) {
|
||
if (!flags.isEnabled(capability)) {
|
||
throw new GraphQlAdvancedCapabilityDisabledException(
|
||
capability.name());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.bootstrap.GraphQlAdvancedModuleGuardTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedCapability.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedFeatureFlags.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuard.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedDependencyRules.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuardTest.java'
|
||
git commit -m "build: isolate graphql advanced modules"
|
||
```
|
||
|
||
### Task 2: Persisted Operation Model과 Registry
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationId.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperation.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationStatus.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistry.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/InMemoryGraphQlPersistedOperationRegistry.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationConflictException.java`
|
||
- Test: `modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistryTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Stable schema contract hash, operation name, canonical document hash와 client profile.
|
||
- Produces: 승인된 operation document의 versioned registry와 ACTIVE·DEPRECATED·BLOCKED lifecycle.
|
||
|
||
**Implementation requirements:**
|
||
- Registry record에 operation ID, operation name, SHA-256 document hash, canonical document, schema hash, allowed client profiles, maximum complexity와 variable bytes를 보존한다.
|
||
- 같은 operation ID로 다른 document를 등록하면 conflict다.
|
||
- BLOCKED operation은 cache에 남아 있어도 실행되지 않는다.
|
||
- Raw variables와 credential을 registry에 저장하지 않는다.
|
||
- Persistent implementation SPI를 제공하되 Stable DB 선택을 강제하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlPersistedOperationRegistryTest {
|
||
@org.junit.jupiter.api.Test
|
||
void sameIdWithDifferentDocumentIsConflict() {
|
||
var registry =
|
||
new InMemoryGraphQlPersistedOperationRegistry();
|
||
registry.register(GraphQlPersistedOperation.active(
|
||
"get-order-v1", "GetOrder", "sha256:a",
|
||
"query GetOrder { order { id } }", "schema-a"));
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> registry.register(
|
||
GraphQlPersistedOperation.active(
|
||
"get-order-v1", "GetOrder", "sha256:b",
|
||
"query GetOrder { order { status } }",
|
||
"schema-a")))
|
||
.isInstanceOf(
|
||
GraphQlPersistedOperationConflictException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationRegistryTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public enum GraphQlPersistedOperationStatus {
|
||
ACTIVE,
|
||
DEPRECATED,
|
||
BLOCKED
|
||
}
|
||
|
||
public record GraphQlPersistedOperation(
|
||
GraphQlPersistedOperationId id,
|
||
String operationName,
|
||
String documentHash,
|
||
String canonicalDocument,
|
||
String schemaContractHash,
|
||
java.util.Set<String> allowedClientProfiles,
|
||
long maximumComplexity,
|
||
int maximumVariablesBytes,
|
||
GraphQlPersistedOperationStatus status) {
|
||
|
||
public static GraphQlPersistedOperation active(
|
||
String id,
|
||
String operationName,
|
||
String documentHash,
|
||
String canonicalDocument,
|
||
String schemaHash) {
|
||
return new GraphQlPersistedOperation(
|
||
new GraphQlPersistedOperationId(id),
|
||
operationName, documentHash, canonicalDocument,
|
||
schemaHash, java.util.Set.of("FIRST_PARTY"),
|
||
10_000, 65_536,
|
||
GraphQlPersistedOperationStatus.ACTIVE);
|
||
}
|
||
}
|
||
|
||
public interface GraphQlPersistedOperationRegistry {
|
||
void register(GraphQlPersistedOperation operation);
|
||
java.util.Optional<GraphQlPersistedOperation> find(
|
||
GraphQlPersistedOperationId id);
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationRegistryTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationId.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperation.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationStatus.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistry.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/InMemoryGraphQlPersistedOperationRegistry.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationConflictException.java' 'modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistryTest.java'
|
||
git commit -m "feat: add graphql persisted operation registry"
|
||
```
|
||
|
||
### Task 3: Persisted Operation Lookup과 Execution Interceptor
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRequest.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationLookup.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptor.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRejectedException.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedPreparsedBridge.java`
|
||
- Test: `modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptorTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Persisted registry, Stable request context, client policy, schema hash와 preparsed cache.
|
||
- Produces: Operation ID로 canonical document를 복원하고 client·schema·variables·complexity를 재검증하는 interceptor.
|
||
|
||
**Implementation requirements:**
|
||
- Client가 operation ID와 임의 query를 동시에 보내면 hash 일치 여부를 검증하거나 profile에 따라 거부한다.
|
||
- Registry의 allowed client profile과 현재 client가 일치해야 한다.
|
||
- Schema contract hash가 현재 schema와 맞지 않으면 실행하지 않는다.
|
||
- Persisted record의 maximum complexity·variables limit와 현재 client policy 중 더 엄격한 값을 사용한다.
|
||
- Lookup 성공이 authorization 성공을 의미하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlPersistedOperationInterceptorTest {
|
||
@org.junit.jupiter.api.Test
|
||
void blockedOperationIsRejectedBeforeExecution() {
|
||
var operation = GraphQlPersistedOperation.active(
|
||
"dangerous-v1", "Dangerous", "sha256:a",
|
||
"query Dangerous { expensive }", "schema-a");
|
||
operation = new GraphQlPersistedOperation(
|
||
operation.id(), operation.operationName(),
|
||
operation.documentHash(), operation.canonicalDocument(),
|
||
operation.schemaContractHash(),
|
||
operation.allowedClientProfiles(),
|
||
operation.maximumComplexity(),
|
||
operation.maximumVariablesBytes(),
|
||
GraphQlPersistedOperationStatus.BLOCKED);
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> GraphQlPersistedOperationPolicy.requireActive(
|
||
operation))
|
||
.isInstanceOf(
|
||
GraphQlPersistedOperationRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationInterceptorTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public final class GraphQlPersistedOperationPolicy {
|
||
public static GraphQlPersistedOperation requireActive(
|
||
GraphQlPersistedOperation operation) {
|
||
if (operation.status()
|
||
!= GraphQlPersistedOperationStatus.ACTIVE) {
|
||
throw new GraphQlPersistedOperationRejectedException(
|
||
"persisted operation is not active");
|
||
}
|
||
return operation;
|
||
}
|
||
|
||
private GraphQlPersistedOperationPolicy() {}
|
||
}
|
||
|
||
public record GraphQlPersistedOperationRequest(
|
||
GraphQlPersistedOperationId operationId,
|
||
String suppliedDocumentHash,
|
||
int variablesBytes) {
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationInterceptorTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRequest.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationLookup.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptor.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationPolicy.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRejectedException.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedPreparsedBridge.java' 'modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptorTest.java'
|
||
git commit -m "feat: execute registered graphql operations"
|
||
```
|
||
|
||
### Task 4: G4 Persisted Operation Admin·Usage·Block Plane
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminService.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationUsage.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAudit.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationBlockCommand.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGate.java`
|
||
- Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminAuthorization.java`
|
||
- Test: `modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGateTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Persisted registry, schema usage observation, admin actor·audit port.
|
||
- Produces: Operation 등록·deprecate·incident block·remove를 감사 가능한 G4 관리 작업으로 제공.
|
||
|
||
**Implementation requirements:**
|
||
- 일반 GraphQL resolver나 application credential은 admin service에 접근하지 않는다.
|
||
- Block는 즉시 실행 경로에 반영되며 cache를 우회하지 않는다.
|
||
- Removal은 최근 usage, schema compatibility와 owner 승인 evidence를 요구한다.
|
||
- 모든 변경에 operator, reason, before/after, timestamp와 trace ID를 감사한다.
|
||
- Raw variables와 credential은 audit에 포함하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlPersistedOperationRemovalGateTest {
|
||
@org.junit.jupiter.api.Test
|
||
void recentUsageBlocksRemoval() {
|
||
var gate = new GraphQlPersistedOperationRemovalGate(
|
||
java.time.Duration.ofDays(30));
|
||
var usage = new GraphQlPersistedOperationUsage(
|
||
java.time.Instant.now().minus(
|
||
java.time.Duration.ofDays(1)), 12);
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> gate.verify(usage, java.time.Instant.now()))
|
||
.isInstanceOf(
|
||
GraphQlPersistedOperationRemovalRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.admin.GraphQlPersistedOperationRemovalGateTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlPersistedOperationUsage(
|
||
java.time.Instant lastUsedAt,
|
||
long executions) {
|
||
}
|
||
|
||
public final class GraphQlPersistedOperationRemovalGate {
|
||
private final java.time.Duration quietPeriod;
|
||
|
||
public GraphQlPersistedOperationRemovalGate(
|
||
java.time.Duration quietPeriod) {
|
||
this.quietPeriod = quietPeriod;
|
||
}
|
||
|
||
public void verify(
|
||
GraphQlPersistedOperationUsage usage,
|
||
java.time.Instant now) {
|
||
if (usage.executions() > 0
|
||
&& usage.lastUsedAt().plus(quietPeriod)
|
||
.isAfter(now)) {
|
||
throw new
|
||
GraphQlPersistedOperationRemovalRejectedException(
|
||
"persisted operation used within quiet period");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.admin.GraphQlPersistedOperationRemovalGateTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminService.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationUsage.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAudit.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationBlockCommand.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGate.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminAuthorization.java' 'modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGateTest.java'
|
||
git commit -m "feat: add graphql operation admin plane"
|
||
```
|
||
|
||
### Task 5: `graphql-transport-ws` WebSocket Protocol Adapter
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocol.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProperties.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketHandlerFactory.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketLifecycle.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketConnectionId.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolError.java`
|
||
- Test: `modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Spring GraphQL WebSocket handler, Stable execution service와 advanced feature guard.
|
||
- Produces: `graphql-transport-ws` connection_init·subscribe·complete lifecycle와 bounded connection policy.
|
||
|
||
**Implementation requirements:**
|
||
- 과거 `subscriptions-transport-ws`를 지원 protocol로 광고하지 않는다.
|
||
- connection init timeout, idle timeout, maximum connection age와 maximum subscriptions per connection을 설정한다.
|
||
- Protocol error는 해당 operation 또는 connection scope에 맞게 종료한다.
|
||
- 서버 shutdown에서 신규 subscribe를 거부하고 기존 stream을 drain한다.
|
||
- Connection ID와 operation ID를 metric label로 사용하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlWebSocketProtocolTest {
|
||
@org.junit.jupiter.api.Test
|
||
void connectionInitAfterDeadlineIsRejected() {
|
||
var lifecycle = new GraphQlWebSocketLifecycle(
|
||
java.time.Duration.ofSeconds(5),
|
||
java.time.Instant.parse("2026-08-12T00:00:00Z"));
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> lifecycle.onConnectionInit(
|
||
java.time.Instant.parse(
|
||
"2026-08-12T00:00:06Z")))
|
||
.isInstanceOf(GraphQlWebSocketProtocolError.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.websocket.GraphQlWebSocketProtocolTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public enum GraphQlWebSocketProtocol {
|
||
GRAPHQL_TRANSPORT_WS("graphql-transport-ws");
|
||
|
||
private final String subProtocol;
|
||
|
||
GraphQlWebSocketProtocol(String subProtocol) {
|
||
this.subProtocol = subProtocol;
|
||
}
|
||
|
||
public String subProtocol() {
|
||
return subProtocol;
|
||
}
|
||
}
|
||
|
||
public final class GraphQlWebSocketLifecycle {
|
||
private final java.time.Duration initTimeout;
|
||
private final java.time.Instant connectedAt;
|
||
|
||
public GraphQlWebSocketLifecycle(
|
||
java.time.Duration initTimeout,
|
||
java.time.Instant connectedAt) {
|
||
this.initTimeout = initTimeout;
|
||
this.connectedAt = connectedAt;
|
||
}
|
||
|
||
public void onConnectionInit(java.time.Instant now) {
|
||
if (now.isAfter(connectedAt.plus(initTimeout))) {
|
||
throw new GraphQlWebSocketProtocolError(
|
||
"connection_init timeout");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.websocket.GraphQlWebSocketProtocolTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocol.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProperties.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketHandlerFactory.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketLifecycle.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketConnectionId.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolError.java' 'modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolTest.java'
|
||
git commit -m "feat: add graphql websocket protocol"
|
||
```
|
||
|
||
### Task 6: WebSocket Authentication Expiry·Revocation Lifecycle
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptor.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketPrincipal.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCredentialExpiry.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketRevocationSignal.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlSubscriptionAuthorizationPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCloseReason.java`
|
||
- Test: `modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptorTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: connection_init payload, security adapter, actor·tenant context, credential expiry와 revocation signal.
|
||
- Produces: Connection과 subscription에 actor context를 고정하고 expiry·revocation 시 fail-closed 종료하는 lifecycle.
|
||
|
||
**Implementation requirements:**
|
||
- connection_init payload의 credential을 metric·log에 남기지 않는다.
|
||
- 인증된 actor·tenant를 이후 operation context에 전달한다.
|
||
- Credential expiry가 오면 connection을 종료하고 silent refresh를 임의 구현하지 않는다.
|
||
- Resource 권한이 변할 수 있는 sensitive subscription은 event delivery 전 재검증 profile을 지원한다.
|
||
- 한 connection에서 tenant context를 operation마다 바꿀 수 없다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlWebSocketAuthenticationInterceptorTest {
|
||
@org.junit.jupiter.api.Test
|
||
void expiredCredentialClosesConnection() {
|
||
var expiry = new GraphQlWebSocketCredentialExpiry(
|
||
java.time.Instant.parse("2026-08-12T00:00:00Z"));
|
||
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
expiry.isExpired(
|
||
java.time.Instant.parse(
|
||
"2026-08-12T00:00:01Z")))
|
||
.isTrue();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.security.GraphQlWebSocketAuthenticationInterceptorTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlWebSocketCredentialExpiry(
|
||
java.time.Instant expiresAt) {
|
||
|
||
public boolean isExpired(java.time.Instant now) {
|
||
return !now.isBefore(expiresAt);
|
||
}
|
||
}
|
||
|
||
public record GraphQlWebSocketPrincipal(
|
||
String actorFingerprint,
|
||
String tenantFingerprint,
|
||
java.time.Instant expiresAt) {
|
||
}
|
||
|
||
public enum GraphQlWebSocketCloseReason {
|
||
AUTHENTICATION_FAILED,
|
||
CREDENTIAL_EXPIRED,
|
||
AUTHORIZATION_REVOKED,
|
||
SERVER_DRAINING
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.security.GraphQlWebSocketAuthenticationInterceptorTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptor.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketPrincipal.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCredentialExpiry.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketRevocationSignal.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlSubscriptionAuthorizationPolicy.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCloseReason.java' 'modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptorTest.java'
|
||
git commit -m "feat: secure graphql websocket lifecycle"
|
||
```
|
||
|
||
### Task 7: Subscription Source SPI와 Bounded Backpressure
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionSource.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionEvent.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionContext.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSlowConsumerPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDispatcher.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionTermination.java`
|
||
- Test: `modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicyTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Messaging/Application Publisher source, actor context, Reactor Publisher와 WebSocket/SSE transport.
|
||
- Produces: 내구성 보장을 과장하지 않는 bounded live stream과 slow-consumer termination.
|
||
|
||
**Implementation requirements:**
|
||
- Messaging ACK·offset·DLQ·replay를 GraphQL subscription이 재구현하지 않는다.
|
||
- Default slow-consumer 정책은 silent drop이 아니라 connection/operation 종료다.
|
||
- Maximum buffered events와 maximum event bytes를 설정한다.
|
||
- Subscriber cancel이 upstream source에 전파된다.
|
||
- Integration Event를 GraphQL DTO로 변환하고 원본 broker message를 그대로 노출하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlSubscriptionBufferPolicyTest {
|
||
@org.junit.jupiter.api.Test
|
||
void defaultPolicyTerminatesSlowConsumer() {
|
||
var policy = GraphQlSubscriptionBufferPolicy.defaultPolicy();
|
||
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
policy.slowConsumerPolicy())
|
||
.isEqualTo(
|
||
GraphQlSlowConsumerPolicy.TERMINATE);
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
policy.maximumBufferedEvents()).isGreaterThan(0);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionBufferPolicyTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public enum GraphQlSlowConsumerPolicy {
|
||
TERMINATE,
|
||
DROP_ALLOWED_FOR_TELEMETRY
|
||
}
|
||
|
||
public record GraphQlSubscriptionBufferPolicy(
|
||
int maximumBufferedEvents,
|
||
long maximumBufferedBytes,
|
||
GraphQlSlowConsumerPolicy slowConsumerPolicy) {
|
||
|
||
public static GraphQlSubscriptionBufferPolicy defaultPolicy() {
|
||
return new GraphQlSubscriptionBufferPolicy(
|
||
128, 1_048_576,
|
||
GraphQlSlowConsumerPolicy.TERMINATE);
|
||
}
|
||
}
|
||
|
||
public interface GraphQlSubscriptionSource<E> {
|
||
org.reactivestreams.Publisher<E> open(
|
||
GraphQlSubscriptionContext context);
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionBufferPolicyTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionSource.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionEvent.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionContext.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicy.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSlowConsumerPolicy.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDispatcher.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionTermination.java' 'modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicyTest.java'
|
||
git commit -m "feat: add bounded graphql subscriptions"
|
||
```
|
||
|
||
### Task 8: Subscription Ordering·Cancellation·Shutdown Profiles
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionOrderingProfile.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionCancellation.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDrainCoordinator.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionState.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionMetrics.java`
|
||
- Test: `modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicyTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Subscription source, GraphQL Java subscription execution strategy와 transport lifecycle.
|
||
- Produces: `LOW_LATENCY`·`ORDERED` profile, cancellation과 graceful shutdown drain.
|
||
|
||
**Implementation requirements:**
|
||
- `ORDERED`는 source order를 보존하지만 head-of-line blocking과 buffer 비용을 명시한다.
|
||
- `LOW_LATENCY`는 completion 순서가 source 순서와 다를 수 있음을 계약한다.
|
||
- Ordering profile은 operation catalog에 등록한다.
|
||
- Shutdown에서 신규 subscription을 거부하고 bounded time 동안 기존 subscription을 drain한다.
|
||
- Cancellation은 resolver nested publisher와 upstream source 모두에 전파한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlSubscriptionExecutionPolicyTest {
|
||
@org.junit.jupiter.api.Test
|
||
void orderedProfileRequestsGraphQlJavaOrderingFlag() {
|
||
var policy = GraphQlSubscriptionExecutionPolicy.ordered();
|
||
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
policy.profile())
|
||
.isEqualTo(
|
||
GraphQlSubscriptionOrderingProfile.ORDERED);
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
policy.keepSourceOrder()).isTrue();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionExecutionPolicyTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public enum GraphQlSubscriptionOrderingProfile {
|
||
LOW_LATENCY,
|
||
ORDERED
|
||
}
|
||
|
||
public record GraphQlSubscriptionExecutionPolicy(
|
||
GraphQlSubscriptionOrderingProfile profile,
|
||
boolean keepSourceOrder) {
|
||
|
||
public static GraphQlSubscriptionExecutionPolicy ordered() {
|
||
return new GraphQlSubscriptionExecutionPolicy(
|
||
GraphQlSubscriptionOrderingProfile.ORDERED, true);
|
||
}
|
||
|
||
public static GraphQlSubscriptionExecutionPolicy lowLatency() {
|
||
return new GraphQlSubscriptionExecutionPolicy(
|
||
GraphQlSubscriptionOrderingProfile.LOW_LATENCY,
|
||
false);
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionExecutionPolicyTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionOrderingProfile.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicy.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionCancellation.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDrainCoordinator.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionState.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionMetrics.java' 'modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicyTest.java'
|
||
git commit -m "feat: add graphql subscription ordering profiles"
|
||
```
|
||
|
||
### Task 9: SSE Distinct Connection Subscription Adapter
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseProperties.java`
|
||
- Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHandlerFactory.java`
|
||
- Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHeartbeat.java`
|
||
- Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseTermination.java`
|
||
- Test: `modules/graphql-advanced/graphql-sse/src/test/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicyTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Spring `GraphQlSseHandler`, Stable execution service, subscription buffer and auth policy.
|
||
- Produces: POST JSON + `Accept: text/event-stream` 기반 Distinct Connection subscription transport.
|
||
|
||
**Implementation requirements:**
|
||
- SSE를 Query·Mutation의 일반 response mode로 사용하지 않고 subscription-only로 제한한다.
|
||
- Connection당 하나의 subscription이라는 Distinct Connection 특성을 문서·metric에 반영한다.
|
||
- Heartbeat, idle timeout, maximum duration과 proxy buffering 요구를 설정한다.
|
||
- WebSocket과 동일한 actor·tenant·authorization·cost policy를 사용한다.
|
||
- HTTP/2 사용 여부와 연결 규모를 load test로 검증한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlSseConnectionPolicyTest {
|
||
@org.junit.jupiter.api.Test
|
||
void acceptsOnlySubscriptionOperation() {
|
||
var policy = GraphQlSseConnectionPolicy.standard();
|
||
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
policy.supports("subscription")).isTrue();
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
policy.supports("query")).isFalse();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-sse:test --tests 'io.backend.skeleton.graphql.advanced.sse.GraphQlSseConnectionPolicyTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public final class GraphQlSseConnectionPolicy {
|
||
public static GraphQlSseConnectionPolicy standard() {
|
||
return new GraphQlSseConnectionPolicy();
|
||
}
|
||
|
||
public boolean supports(String operationType) {
|
||
return "subscription".equals(operationType);
|
||
}
|
||
}
|
||
|
||
public record GraphQlSseProperties(
|
||
java.time.Duration heartbeatInterval,
|
||
java.time.Duration idleTimeout,
|
||
java.time.Duration maximumDuration) {
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-sse:test --tests 'io.backend.skeleton.graphql.advanced.sse.GraphQlSseConnectionPolicyTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseProperties.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHandlerFactory.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicy.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHeartbeat.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseTermination.java' 'modules/graphql-advanced/graphql-sse/src/test/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicyTest.java'
|
||
git commit -m "feat: add graphql sse subscription transport"
|
||
```
|
||
|
||
### Task 10: Messaging-backed Subscription Replay Extension
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSubscriptionCursor.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayPosition.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplaySource.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoff.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayAuthorization.java`
|
||
- Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayGapException.java`
|
||
- Test: `modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoffTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Messaging replay/offset capability, snapshot query, actor authorization와 signed cursor.
|
||
- Produces: GraphQL 표준 보장과 분리된 snapshot + live handoff 및 explicit replay cursor extension.
|
||
|
||
**Implementation requirements:**
|
||
- GraphQL Core에 resume 표준이 있다고 광고하지 않는다.
|
||
- Replay durability와 offset 보장은 Messaging platform이 소유한다.
|
||
- Snapshot sequence와 live source 시작 position 사이에 gap·duplicate가 없도록 handoff를 검증한다.
|
||
- Cursor는 actor/client·subscription profile에 bind하고 서명한다.
|
||
- Replay window 밖의 cursor는 history-lost error로 반환한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlSnapshotLiveHandoffTest {
|
||
@org.junit.jupiter.api.Test
|
||
void gapBetweenSnapshotAndLivePositionIsRejected() {
|
||
var handoff = new GraphQlSnapshotLiveHandoff(
|
||
new GraphQlReplayPosition(10),
|
||
new GraphQlReplayPosition(12));
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
handoff::verifyContiguous)
|
||
.isInstanceOf(GraphQlReplayGapException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.replay.GraphQlSnapshotLiveHandoffTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlReplayPosition(long sequence) {
|
||
public GraphQlReplayPosition {
|
||
if (sequence < 0) {
|
||
throw new IllegalArgumentException(
|
||
"sequence cannot be negative");
|
||
}
|
||
}
|
||
}
|
||
|
||
public record GraphQlSnapshotLiveHandoff(
|
||
GraphQlReplayPosition snapshotPosition,
|
||
GraphQlReplayPosition liveStartPosition) {
|
||
|
||
public void verifyContiguous() {
|
||
if (liveStartPosition.sequence()
|
||
> snapshotPosition.sequence() + 1) {
|
||
throw new GraphQlReplayGapException(
|
||
"snapshot and live stream contain a gap");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.replay.GraphQlSnapshotLiveHandoffTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSubscriptionCursor.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayPosition.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplaySource.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoff.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayAuthorization.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayGapException.java' 'modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoffTest.java'
|
||
git commit -m "feat: add graphql subscription replay extension"
|
||
```
|
||
|
||
### Task 11: GraphQL Java 25 Chained DataLoader Opt-in
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDataLoaderPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderDependencyGraph.java`
|
||
- Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetector.java`
|
||
- Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDispatchConfigurer.java`
|
||
- Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedLoaderMetrics.java`
|
||
- Test: `modules/graphql-advanced/graphql-dataloader-chaining/src/test/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetectorTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Stable request-scoped DataLoader registry와 GraphQL Java 25 chained dispatch capability.
|
||
- Produces: 명시적 dependency graph를 가진 opt-in chained loader execution과 cycle·dispatch regression gate.
|
||
|
||
**Implementation requirements:**
|
||
- Stable default에서는 chained dispatch를 활성화하지 않는다.
|
||
- 각 loader dependency를 manifest에 선언한다.
|
||
- Dependency cycle이 있으면 startup 실패다.
|
||
- Dispatch 순서 변경이 query count·batch size·result ordering에 미치는 영향을 contract test로 검증한다.
|
||
- Request scope, actor·tenant isolation과 maximum batch size 규칙을 그대로 유지한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlDataLoaderCycleDetectorTest {
|
||
@org.junit.jupiter.api.Test
|
||
void detectsLoaderDependencyCycle() {
|
||
var graph = new GraphQlDataLoaderDependencyGraph()
|
||
.dependsOn("orders", "customers")
|
||
.dependsOn("customers", "orders");
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> new GraphQlDataLoaderCycleDetector().verify(graph))
|
||
.isInstanceOf(
|
||
GraphQlDataLoaderDependencyCycleException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-dataloader-chaining:test --tests 'io.backend.skeleton.graphql.advanced.chaining.GraphQlDataLoaderCycleDetectorTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public final class GraphQlDataLoaderDependencyGraph {
|
||
private final java.util.Map<String, java.util.Set<String>> edges =
|
||
new java.util.LinkedHashMap<>();
|
||
|
||
public GraphQlDataLoaderDependencyGraph dependsOn(
|
||
String loader, String dependency) {
|
||
edges.computeIfAbsent(loader,
|
||
ignored -> new java.util.LinkedHashSet<>())
|
||
.add(dependency);
|
||
return this;
|
||
}
|
||
|
||
java.util.Map<String, java.util.Set<String>> edges() {
|
||
return java.util.Collections.unmodifiableMap(edges);
|
||
}
|
||
}
|
||
|
||
public record GraphQlChainedDataLoaderPolicy(
|
||
boolean enabled,
|
||
int maximumDepth) {
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-dataloader-chaining:test --tests 'io.backend.skeleton.graphql.advanced.chaining.GraphQlDataLoaderCycleDetectorTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDataLoaderPolicy.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderDependencyGraph.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetector.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDispatchConfigurer.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedLoaderMetrics.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/test/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetectorTest.java'
|
||
git commit -m "feat: add opt in chained graphql dataloaders"
|
||
```
|
||
|
||
### Task 12: Federation Subgraph Schema와 Entity Resolver
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationProperties.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationSchemaFactory.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityKey.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolver.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationBatchResolver.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationCapability.java`
|
||
- Test: `modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolverTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Stable SDL assembly, federation-jvm integration, Application query services와 DataLoader.
|
||
- Produces: 별도 opt-in Subgraph schema, `@key` entity mapping과 batch reference resolution.
|
||
|
||
**Implementation requirements:**
|
||
- Single executable schema가 기본이며 federation flag가 없으면 federation wiring을 등록하지 않는다.
|
||
- Entity reference resolver가 Repository를 직접 호출하지 않고 Application query service를 사용한다.
|
||
- Entity key는 versioned contract이며 변경 시 breaking review를 요구한다.
|
||
- Batch entity resolution은 요청 단위 DataLoader와 tenant context를 사용한다.
|
||
- Federation router나 supergraph 운영은 이 모듈이 소유하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlFederationEntityResolverTest {
|
||
@org.junit.jupiter.api.Test
|
||
void rejectsRepresentationMissingDeclaredKey() {
|
||
var key = new GraphQlFederationEntityKey(
|
||
"Order", java.util.List.of("id"));
|
||
var resolver = new GraphQlFederationEntityResolver(key);
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> resolver.validateRepresentation(
|
||
java.util.Map.of("__typename", "Order")))
|
||
.isInstanceOf(
|
||
GraphQlFederationRepresentationException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.federation.GraphQlFederationEntityResolverTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlFederationEntityKey(
|
||
String typeName,
|
||
java.util.List<String> fields) {
|
||
public GraphQlFederationEntityKey {
|
||
fields = java.util.List.copyOf(fields);
|
||
if (fields.isEmpty()) {
|
||
throw new IllegalArgumentException(
|
||
"federation entity key cannot be empty");
|
||
}
|
||
}
|
||
}
|
||
|
||
public final class GraphQlFederationEntityResolver {
|
||
private final GraphQlFederationEntityKey key;
|
||
|
||
public GraphQlFederationEntityResolver(
|
||
GraphQlFederationEntityKey key) {
|
||
this.key = key;
|
||
}
|
||
|
||
public void validateRepresentation(
|
||
java.util.Map<String, Object> representation) {
|
||
for (String field : key.fields()) {
|
||
if (!representation.containsKey(field)) {
|
||
throw new
|
||
GraphQlFederationRepresentationException(
|
||
"missing federation entity key field");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.federation.GraphQlFederationEntityResolverTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationProperties.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationSchemaFactory.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityKey.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolver.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationBatchResolver.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationCapability.java' 'modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolverTest.java'
|
||
git commit -m "feat: add graphql federation subgraph"
|
||
```
|
||
|
||
### Task 13: Federation Composition·Deployment·Failure Gate
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlSubgraphContract.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionResult.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGate.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationUsageReport.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationLatencyBudget.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationDeploymentOrder.java`
|
||
- Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationReleaseEvidence.java`
|
||
- Test: `modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGateTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Subgraph SDL, entity keys, router composition result, usage·latency·failure evidence.
|
||
- Produces: Composition CI, cross-subgraph dependency·latency·partial failure 검증과 배포 순서 gate.
|
||
|
||
**Implementation requirements:**
|
||
- Composition 성공만으로 release를 승인하지 않고 entity key, owner, downstream dependency, latency budget을 검증한다.
|
||
- Cross-subgraph N+1과 per-entity downstream call을 performance gate에서 탐지한다.
|
||
- Subgraph schema가 router보다 먼저 또는 호환되지 않는 순서로 배포되지 않도록 deployment order를 검증한다.
|
||
- 부분 장애에서 nullable boundary와 error ownership을 contract test로 검증한다.
|
||
- Router 설정과 운영은 별도 프로젝트의 evidence로 입력받는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlFederationCompositionGateTest {
|
||
@org.junit.jupiter.api.Test
|
||
void compositionWithoutLatencyEvidenceIsRejected() {
|
||
var evidence = new GraphQlFederationReleaseEvidence(
|
||
true, true, false, true);
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> new GraphQlFederationCompositionGate()
|
||
.verify(evidence))
|
||
.isInstanceOf(
|
||
GraphQlFederationReleaseRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.composition.GraphQlFederationCompositionGateTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlFederationReleaseEvidence(
|
||
boolean compositionPassed,
|
||
boolean entityContractsPassed,
|
||
boolean latencyPassed,
|
||
boolean failureContractsPassed) {
|
||
}
|
||
|
||
public final class GraphQlFederationCompositionGate {
|
||
public void verify(
|
||
GraphQlFederationReleaseEvidence evidence) {
|
||
if (!evidence.compositionPassed()
|
||
|| !evidence.entityContractsPassed()
|
||
|| !evidence.latencyPassed()
|
||
|| !evidence.failureContractsPassed()) {
|
||
throw new
|
||
GraphQlFederationReleaseRejectedException(
|
||
"federation composition evidence incomplete");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.composition.GraphQlFederationCompositionGateTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlSubgraphContract.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionResult.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGate.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationUsageReport.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationLatencyBudget.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationDeploymentOrder.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationReleaseEvidence.java' 'modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGateTest.java'
|
||
git commit -m "chore: add graphql federation composition gate"
|
||
```
|
||
|
||
### Task 14: Client·Transport DTO Code Generation
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlCodegenProfile.java`
|
||
- Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlScalarMapping.java`
|
||
- Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundary.java`
|
||
- Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlClientOperationGenerator.java`
|
||
- Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlTransportTypeGenerator.java`
|
||
- Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedCompatibilityGate.java`
|
||
- Test: `modules/graphql-advanced/graphql-codegen/src/test/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundaryTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: SDL, operation documents, scalar manifest와 selected codegen engine.
|
||
- Produces: Client request/response model과 transport-only type을 생성하되 domain·repository를 생성하지 않는 tooling.
|
||
|
||
**Implementation requirements:**
|
||
- Domain Entity, Application Use Case interface와 Repository를 생성하지 않는다.
|
||
- Generated source는 별도 directory·package에 두고 사람이 작성한 domain code를 덮어쓰지 않는다.
|
||
- Custom scalar, nullability, union, interface와 oneOf mapping을 명시한다.
|
||
- Operation document를 schema와 함께 compile-time 검증한다.
|
||
- Schema additive change가 generated client의 exhaustive enum/union source compatibility를 깨는지 별도 보고한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlGeneratedSourceBoundaryTest {
|
||
@org.junit.jupiter.api.Test
|
||
void domainAndRepositoryGenerationAreForbidden() {
|
||
var boundary = GraphQlGeneratedSourceBoundary.standard();
|
||
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
boundary.isAllowed("CLIENT_RESPONSE")).isTrue();
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
boundary.isAllowed("DOMAIN_ENTITY")).isFalse();
|
||
org.assertj.core.api.Assertions.assertThat(
|
||
boundary.isAllowed("REPOSITORY")).isFalse();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-codegen:test --tests 'io.backend.skeleton.graphql.advanced.codegen.GraphQlGeneratedSourceBoundaryTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public final class GraphQlGeneratedSourceBoundary {
|
||
private static final java.util.Set<String> ALLOWED =
|
||
java.util.Set.of(
|
||
"CLIENT_REQUEST",
|
||
"CLIENT_RESPONSE",
|
||
"TRANSPORT_INPUT",
|
||
"TRANSPORT_OUTPUT");
|
||
|
||
public static GraphQlGeneratedSourceBoundary standard() {
|
||
return new GraphQlGeneratedSourceBoundary();
|
||
}
|
||
|
||
public boolean isAllowed(String generatedType) {
|
||
return ALLOWED.contains(generatedType);
|
||
}
|
||
}
|
||
|
||
public record GraphQlScalarMapping(
|
||
String scalarName,
|
||
String javaType,
|
||
String codecId) {
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-codegen:test --tests 'io.backend.skeleton.graphql.advanced.codegen.GraphQlGeneratedSourceBoundaryTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlCodegenProfile.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlScalarMapping.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundary.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlClientOperationGenerator.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlTransportTypeGenerator.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedCompatibilityGate.java' 'modules/graphql-advanced/graphql-codegen/src/test/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundaryTest.java'
|
||
git commit -m "build: add graphql client code generation"
|
||
```
|
||
|
||
### Task 15: Allowlisted Spring Data GraphQL Compatibility
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposure.java`
|
||
- Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryAllowlist.java`
|
||
- Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryArgumentPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryPaginationPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryProjectionPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidator.java`
|
||
- Test: `modules/graphql-advanced/graphql-spring-data-compat/src/test/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidatorTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Spring `@GraphQlRepository`, Querydsl/QBE repository, schema coordinate와 explicit allowlist.
|
||
- Produces: 자동 DataFetcher 기능을 제한된 compatibility path로 제공하고 persistence model 자동 노출을 차단.
|
||
|
||
**Implementation requirements:**
|
||
- 등록되지 않은 repository는 GraphQL DataFetcher로 자동 노출하지 않는다.
|
||
- 허용 filter, sort, projection과 maximum page size를 coordinate별로 정의한다.
|
||
- 기본 offset pagination 20개 동작을 암묵적으로 사용하지 않고 explicit policy를 요구한다.
|
||
- Entity/Document를 output으로 직접 반환하지 않고 승인된 projection만 허용한다.
|
||
- Stable resolver·Application Service 경계를 대체하는 주류 API로 문서화하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlRepositoryExposureValidatorTest {
|
||
@org.junit.jupiter.api.Test
|
||
void unregisteredRepositoryIsRejected() {
|
||
var validator =
|
||
new GraphQlRepositoryExposureValidator(
|
||
GraphQlRepositoryAllowlist.empty());
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> validator.verify(
|
||
new GraphQlRepositoryExposure(
|
||
"OrderRepository", "Query.orders")))
|
||
.isInstanceOf(
|
||
GraphQlRepositoryExposureRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-spring-data-compat:test --tests 'io.backend.skeleton.graphql.advanced.compat.GraphQlRepositoryExposureValidatorTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlRepositoryExposure(
|
||
String repositoryName,
|
||
String schemaCoordinate) {
|
||
}
|
||
|
||
public final class GraphQlRepositoryAllowlist {
|
||
private final java.util.Set<String> repositoryNames;
|
||
|
||
private GraphQlRepositoryAllowlist(
|
||
java.util.Set<String> repositoryNames) {
|
||
this.repositoryNames =
|
||
java.util.Set.copyOf(repositoryNames);
|
||
}
|
||
|
||
public static GraphQlRepositoryAllowlist empty() {
|
||
return new GraphQlRepositoryAllowlist(
|
||
java.util.Set.of());
|
||
}
|
||
|
||
public boolean contains(String repositoryName) {
|
||
return repositoryNames.contains(repositoryName);
|
||
}
|
||
}
|
||
|
||
public final class GraphQlRepositoryExposureValidator {
|
||
private final GraphQlRepositoryAllowlist allowlist;
|
||
|
||
public GraphQlRepositoryExposureValidator(
|
||
GraphQlRepositoryAllowlist allowlist) {
|
||
this.allowlist = allowlist;
|
||
}
|
||
|
||
public void verify(GraphQlRepositoryExposure exposure) {
|
||
if (!allowlist.contains(exposure.repositoryName())) {
|
||
throw new
|
||
GraphQlRepositoryExposureRejectedException(
|
||
exposure.repositoryName());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-spring-data-compat:test --tests 'io.backend.skeleton.graphql.advanced.compat.GraphQlRepositoryExposureValidatorTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposure.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryAllowlist.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryArgumentPolicy.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryPaginationPolicy.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryProjectionPolicy.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidator.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/test/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidatorTest.java'
|
||
git commit -m "feat: add graphql spring data compat guard"
|
||
```
|
||
|
||
### Task 16: RSocket GraphQL Experimental Transport
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketProperties.java`
|
||
- Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketHandlerFactory.java`
|
||
- Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketAuthentication.java`
|
||
- Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketErrorMapper.java`
|
||
- Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketCapability.java`
|
||
- Test: `modules/graphql-advanced/graphql-rsocket/src/test/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicyTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Spring GraphQL RSocket handler, Stable execution service와 experimental feature approval.
|
||
- Produces: 내부 시스템용 request-response Query/Mutation과 request-stream Subscription experimental transport.
|
||
|
||
**Implementation requirements:**
|
||
- RSocket는 public default transport가 아니다.
|
||
- Query·Mutation은 request-response, Subscription은 request-stream으로만 매핑한다.
|
||
- Stable HTTP/WebSocket과 동일한 actor·tenant·cost·error policy를 적용한다.
|
||
- Route·metadata MIME type·authentication을 allowlist한다.
|
||
- Production 활성화에는 explicit consumer, load·failure evidence와 owner가 필요하다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlRSocketRoutePolicyTest {
|
||
@org.junit.jupiter.api.Test
|
||
void rejectsUnknownRoute() {
|
||
var policy = new GraphQlRSocketRoutePolicy(
|
||
java.util.Set.of("graphql"));
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> policy.requireAllowed("admin.raw"))
|
||
.isInstanceOf(
|
||
GraphQlRSocketRouteRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-rsocket:test --tests 'io.backend.skeleton.graphql.advanced.rsocket.GraphQlRSocketRoutePolicyTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public final class GraphQlRSocketRoutePolicy {
|
||
private final java.util.Set<String> allowedRoutes;
|
||
|
||
public GraphQlRSocketRoutePolicy(
|
||
java.util.Set<String> allowedRoutes) {
|
||
this.allowedRoutes =
|
||
java.util.Set.copyOf(allowedRoutes);
|
||
}
|
||
|
||
public String requireAllowed(String route) {
|
||
if (!allowedRoutes.contains(route)) {
|
||
throw new GraphQlRSocketRouteRejectedException(route);
|
||
}
|
||
return route;
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-rsocket:test --tests 'io.backend.skeleton.graphql.advanced.rsocket.GraphQlRSocketRoutePolicyTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketProperties.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicy.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketHandlerFactory.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketAuthentication.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketErrorMapper.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketCapability.java' 'modules/graphql-advanced/graphql-rsocket/src/test/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicyTest.java'
|
||
git commit -m "feat: add experimental graphql rsocket"
|
||
```
|
||
|
||
### Task 17: GraphQL over HTTP GET Draft Profile
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetProfile.java`
|
||
- Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetRequestParser.java`
|
||
- Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCachePolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCsrfPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpDraftCompatibilityReport.java`
|
||
- Test: `modules/graphql-advanced/graphql-http-draft/src/test/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicyTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: GraphQL over HTTP Stage 2 Draft, Stable HTTP V1 policy와 experimental flag.
|
||
- Produces: Query-only GET parsing, cache·CSRF 정책과 draft compatibility report.
|
||
|
||
**Implementation requirements:**
|
||
- Mutation과 Subscription을 GET으로 허용하지 않는다.
|
||
- Query, operationName, variables, extensions의 URI 크기와 encoding을 제한한다.
|
||
- Cookie credential 사용 시 CSRF와 cache behavior를 명시적으로 검증한다.
|
||
- Raw query와 variables가 access log, referer 또는 metric에 노출될 위험을 문서·테스트한다.
|
||
- Draft 변화가 Stable POST contract를 변경하지 않는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlHttpGetOperationPolicyTest {
|
||
@org.junit.jupiter.api.Test
|
||
void mutationOverGetIsRejected() {
|
||
var policy = GraphQlHttpGetOperationPolicy.queryOnly();
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> policy.verify("mutation"))
|
||
.isInstanceOf(
|
||
GraphQlHttpGetRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-http-draft:test --tests 'io.backend.skeleton.graphql.advanced.get.GraphQlHttpGetOperationPolicyTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public final class GraphQlHttpGetOperationPolicy {
|
||
public static GraphQlHttpGetOperationPolicy queryOnly() {
|
||
return new GraphQlHttpGetOperationPolicy();
|
||
}
|
||
|
||
public void verify(String operationType) {
|
||
if (!"query".equals(operationType)) {
|
||
throw new GraphQlHttpGetRejectedException(
|
||
"GET supports query operations only");
|
||
}
|
||
}
|
||
}
|
||
|
||
public record GraphQlHttpGetProfile(
|
||
int maximumUriBytes,
|
||
boolean sharedCacheAllowed,
|
||
boolean csrfRequired) {
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-http-draft:test --tests 'io.backend.skeleton.graphql.advanced.get.GraphQlHttpGetOperationPolicyTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetProfile.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetRequestParser.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCachePolicy.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCsrfPolicy.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicy.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpDraftCompatibilityReport.java' 'modules/graphql-advanced/graphql-http-draft/src/test/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicyTest.java'
|
||
git commit -m "feat: add experimental graphql http get"
|
||
```
|
||
|
||
### Task 18: Incremental Delivery Experimental Profile
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryCapability.java`
|
||
- Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryProfile.java`
|
||
- Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalPatch.java`
|
||
- Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalTransportPolicy.java`
|
||
- Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCancellation.java`
|
||
- Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGate.java`
|
||
- Test: `modules/graphql-advanced/graphql-incremental-delivery/src/test/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGateTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: 실험 기능을 지원하는 GraphQL Java/Spring 조합, multipart/mixed 또는 streaming transport capability.
|
||
- Produces: `@defer`·`@stream` 계열 기능을 Stable contract와 분리한 version-gated experimental profile.
|
||
|
||
**Implementation requirements:**
|
||
- September 2025 Stable schema contract에 존재하지 않거나 구현 조합이 안정화되지 않은 기능을 자동 활성화하지 않는다.
|
||
- 초기 result와 후속 patch의 error·path·ordering·cancellation 계약을 별도로 검증한다.
|
||
- Stable HTTP response byte budget과 timeout을 우회하지 않는다.
|
||
- Client capability negotiation 없이 incremental response를 반환하지 않는다.
|
||
- Compatibility gate가 실패하면 일반 non-incremental execution으로 silent fallback하지 않고 설정 오류로 차단한다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlIncrementalCompatibilityGateTest {
|
||
@org.junit.jupiter.api.Test
|
||
void unsupportedRuntimeCannotEnableIncrementalDelivery() {
|
||
var gate = new GraphQlIncrementalCompatibilityGate();
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> gate.verify(
|
||
new GraphQlIncrementalDeliveryCapability(
|
||
false, false)))
|
||
.isInstanceOf(
|
||
GraphQlIncrementalDeliveryRejectedException.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-incremental-delivery:test --tests 'io.backend.skeleton.graphql.advanced.incremental.GraphQlIncrementalCompatibilityGateTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlIncrementalDeliveryCapability(
|
||
boolean engineSupported,
|
||
boolean transportSupported) {
|
||
}
|
||
|
||
public final class GraphQlIncrementalCompatibilityGate {
|
||
public void verify(
|
||
GraphQlIncrementalDeliveryCapability capability) {
|
||
if (!capability.engineSupported()
|
||
|| !capability.transportSupported()) {
|
||
throw new
|
||
GraphQlIncrementalDeliveryRejectedException(
|
||
"incremental delivery runtime unsupported");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-incremental-delivery:test --tests 'io.backend.skeleton.graphql.advanced.incremental.GraphQlIncrementalCompatibilityGateTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryCapability.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryProfile.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalPatch.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalTransportPolicy.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCancellation.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGate.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/test/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGateTest.java'
|
||
git commit -m "feat: add experimental graphql incremental delivery"
|
||
```
|
||
|
||
### Task 19: Advanced Capability Promotion·Soak·Release Gate
|
||
|
||
**Files:**
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseEvidence.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGate.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedPromotionDecision.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedSoakScenario.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedCompatibilityMatrix.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedRunbookIndex.java`
|
||
- Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseFailure.java`
|
||
- Test: `modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGateTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Stable Task 1–48 release evidence와 Advanced capability별 composition·soak·security·compatibility evidence.
|
||
- Produces: Advanced Stable 승격과 Experimental 유지·철회를 결정하는 capability별 release gate.
|
||
|
||
**Implementation requirements:**
|
||
- Stable Task 1–48의 release gate가 통과하지 않으면 Advanced 계획을 시작하지 않는다.
|
||
- WebSocket·SSE는 장기 connection soak, slow consumer, auth expiry, cancellation, shutdown drain evidence를 요구한다.
|
||
- Federation은 composition, router integration, cross-subgraph latency와 partial failure evidence를 요구한다.
|
||
- Persisted Operation은 registry durability, block propagation, usage와 schema compatibility evidence를 요구한다.
|
||
- RSocket·HTTP GET·Incremental Delivery는 명시적 승격 ADR 전까지 Experimental로 남는다.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```java
|
||
class GraphQlAdvancedReleaseGateTest {
|
||
@org.junit.jupiter.api.Test
|
||
void advancedCannotReleaseBeforeStableBaseline() {
|
||
var evidence = new GraphQlAdvancedReleaseEvidence(
|
||
false, true, true, true, true);
|
||
|
||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||
() -> new GraphQlAdvancedReleaseGate().verify(evidence))
|
||
.isInstanceOf(GraphQlAdvancedReleaseFailure.class)
|
||
.hasMessageContaining("stable");
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test and verify the failure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.release.GraphQlAdvancedReleaseGateTest'
|
||
```
|
||
|
||
Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly.
|
||
|
||
- [ ] **Step 3: Implement the smallest complete production contract**
|
||
|
||
```java
|
||
public record GraphQlAdvancedReleaseEvidence(
|
||
boolean stableBaselinePassed,
|
||
boolean capabilityContractsPassed,
|
||
boolean securityPassed,
|
||
boolean soakPassed,
|
||
boolean compatibilityPassed) {
|
||
}
|
||
|
||
public final class GraphQlAdvancedReleaseGate {
|
||
public void verify(GraphQlAdvancedReleaseEvidence evidence) {
|
||
if (!evidence.stableBaselinePassed()) {
|
||
throw new GraphQlAdvancedReleaseFailure(
|
||
"stable graphql baseline must pass first");
|
||
}
|
||
if (!evidence.capabilityContractsPassed()
|
||
|| !evidence.securityPassed()
|
||
|| !evidence.soakPassed()
|
||
|| !evidence.compatibilityPassed()) {
|
||
throw new GraphQlAdvancedReleaseFailure(
|
||
"advanced graphql evidence incomplete");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract.
|
||
|
||
- [ ] **Step 4: Run the focused test and the owning suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.release.GraphQlAdvancedReleaseGateTest'
|
||
./gradlew graphqlAdvancedTest
|
||
```
|
||
|
||
Expected: PASS for the focused test and the aggregate suite.
|
||
|
||
- [ ] **Step 5: Commit the independently reviewable change**
|
||
|
||
```bash
|
||
git add 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseEvidence.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGate.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedPromotionDecision.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedSoakScenario.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedCompatibilityMatrix.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedRunbookIndex.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseFailure.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGateTest.java'
|
||
git commit -m "chore: add graphql advanced release gate"
|
||
```
|