chore: crush git history - reborn from consolidation on 2026-03-10
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# 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!
|
||||
Reference in New Issue
Block a user