Files
apophis-fastify/docs/attic/root-history/FEEDBACK-cross-route-relationships.md

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 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:

'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 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:

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

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!