7.5 KiB
Feedback for Apophis Team: Cross-Route Relationships and Hypermedia Validation
From: Arbiter Team (Multi-tenant identity platform with LDF+Action hypermedia architecture) Date: 2026-04-26 Status: ✅ IMPLEMENTED in v2.1 — All P0/P1 features complete
1. Executive Summary
The Gap (v2.0): Apophis validated routes as independent entities. Real-world APIs have relationships:
- Parent-child: Tenant owns Applications, Application owns Users
- Hypermedia links: Resources expose
controlswith URLs to related resources - Cascade behavior: Deleting a parent should make children inaccessible
- Path correlation: Child routes use parent IDs from path parameters
The Solution (v2.1): All cross-route validation is now expressed through APOSTL formulas using extension predicates. No imperative APIs or special endpoints.
2. What Was Implemented
2.1 Extension Predicate: route_exists() ✅
Check that hypermedia links resolve to registered routes:
'route_exists(this).controls.self.href == true'
'route_exists(this).controls.tenant.href == true'
'route_exists(this).controls.applications.href == true'
File: src/extensions/relationships.ts
Tests: src/test/relationships.test.ts, src/test/cross-operation-support.test.ts
2.2 Extension Predicate: relationship_valid() ✅
Validate parent-child consistency:
'relationship_valid("parent", request_params(this).tenantId, response_body(this).tenantId) == true'
File: src/extensions/relationships.ts
Tests: src/test/relationships.test.ts
2.3 Extension Predicate: cascade_valid() ✅
Verify cascade after DELETE:
'cascade_valid("tenant", request_params(this).id, ["application", "user"]) == true'
File: src/extensions/relationships.ts
Tests: src/test/relationships.test.ts
2.4 Automatic Path Substitution in Stateful Tests ✅
When generating commands for routes with path params (e.g., :tenantId):
- Checks if resource type
tenantexists in state - If yes, substitutes with a known ID from state
- If no, falls back to arbitrary generation
File: src/domain/request-builder.ts (enhanced substitutePathParams())
Tests: src/test/stateful-runner.test.ts
2.5 Cascade Validator ✅
After DELETE commands, automatically discovers child routes and verifies they return 404:
const validator = createCascadeValidator(routes)
const report = await validator.validateAfterDelete(
'/tenants/tenant:acme',
{ id: 'tenant:acme' },
{ maxDepth: 2 }
)
File: src/test/cascade-validator.ts
Tests: src/test/cascade-validator.test.ts
2.6 Hypermedia Link Extraction ✅
Utility for extracting links from response bodies (controls, _links, links array):
const links = extractLinks(response.body, 'GET /users/:id')
// Returns: [{ route: 'GET /users/:id', control: 'self', href: '/users/123' }, ...]
File: src/test/hypermedia-validator.ts
Tests: src/test/hypermedia-validator.test.ts
3. Design Philosophy: APOSTL-First
We rejected the imperative API approach. Instead of:
// ❌ WRONG: Imperative API
const report = await fastify.apophis.validateHypermedia({
checkLinks: true,
checkDescriptors: true
})
We use declarative APOSTL contracts:
// ✅ CORRECT: Declarative contracts
'route_exists(this).controls.self.href == true'
'route_exists(this).controls.tenant.href == true'
Why?
- Contracts are evaluated during all test phases (petit, stateful, runtime)
- No special endpoints or hooks needed
- Consistent with APOPHIS's design philosophy
- Self-documenting in route schemas
4. Usage Examples
4.1 Hypermedia Controls
fastify.get('/tenants/:id', {
schema: {
'x-category': 'observer',
'x-ensures': [
'route_exists(this).controls.self.href == true',
'route_exists(this).controls.applications.href == true',
],
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
controls: {
type: 'object',
properties: {
self: { type: 'object', properties: { href: { type: 'string' } } },
applications: { type: 'object', properties: { href: { type: 'string' } } },
},
},
},
},
},
},
})
4.2 Parent-Child Validation
fastify.post('/tenants/:tenantId/applications', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'response_body(this).tenantId == request_params(this).tenantId',
'response_code(GET /tenants/{request_params(this).tenantId}/applications/{response_body(this).id}) == 200',
],
},
})
4.3 Cascade Validation
fastify.delete('/tenants/:id', {
schema: {
'x-category': 'destructor',
'x-ensures': [
'cascade_valid("tenant", request_params(this).id, ["application", "user"]) == true',
],
},
})
5. Test Results
| Feature | Tests | Status |
|---|---|---|
route_exists() predicate |
5 tests | ✅ Passing |
relationship_valid() predicate |
2 tests | ✅ Passing |
cascade_valid() predicate |
2 tests | ✅ Passing |
| Path substitution | 1 test | ✅ Passing |
| Cascade validator | 6 tests | ✅ Passing |
| Hypermedia extraction | 9 tests | ✅ Passing |
| Total | 487 tests | ✅ All passing |
6. What We Learned
6.1 APOSTL is Sufficient
We initially proposed imperative APIs (validateHypermedia(), x-relationships annotations). Through implementation, we discovered that APOSTL predicates are more powerful and consistent:
- Composability:
route_exists()can be combined with any other APOSTL expression - Test coverage: Works in petit, stateful, and runtime validation without extra code
- Clarity: Contracts are self-documenting in route schemas
6.2 Extension Predicates are the Right Abstraction
The extension system (predicates + headers + hooks) provides exactly the right level of flexibility:
- Domain-specific: Each predicate solves one problem well
- Composable: Multiple extensions work together
- Testable: Pure functions with clear inputs/outputs
6.3 State Tracking is Key
Automatic path substitution requires tracking resource state across test commands. The ModelState with ResourceHierarchy provides the right structure:
interface ModelState {
resources: Map<string, Map<string, ResourceHierarchy>>
// resourceType → resourceId → { id, type, parentId, parentType, ... }
}
7. Remaining Work (Out of Scope for v2.1)
| Feature | Status | Reason |
|---|---|---|
x-relationships schema annotation |
❌ Not implemented | Replaced by APOSTL predicates |
| Full graph traversal | ❌ Out of scope | Complex graph algorithms belong in application tests |
| Database foreign key validation | ❌ Out of scope | Apophis shouldn't access databases directly |
| Cross-service link validation | ❌ Out of scope | Microservice links require running external services |
8. References
- Implementation:
src/extensions/relationships.ts - Route Matcher:
src/infrastructure/route-matcher.ts - Cascade Validator:
src/test/cascade-validator.ts - Hypermedia Validator:
src/test/hypermedia-validator.ts - Tests:
src/test/relationships.test.ts,src/test/cross-operation-support.test.ts - Extension System:
docs/extensions/EXTENSION-PLUGIN-SYSTEM.md
Contact: Arbiter Team — We'd love to hear how these features work for your use cases!