Files

254 lines
7.5 KiB
Markdown
Raw Permalink Normal View History

# 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 `controls` with 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:
```apostl
'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:
```apostl
'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:
```apostl
'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 `tenant` exists 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:
```typescript
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):
```typescript
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:
```typescript
// ❌ WRONG: Imperative API
const report = await fastify.apophis.validateHypermedia({
checkLinks: true,
checkDescriptors: true
})
```
We use declarative APOSTL contracts:
```apostl
// ✅ 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
```typescript
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
```typescript
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
```typescript
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:
```typescript
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!