# Relationships and API Access Control

## Three-Tier Relationship System

Bangstack uses a three-tier approach for managing relationships between models:

1. **Data Model Layer** - Relationships exist at the database/ORM level
2. **Default API Behavior** - Control what's included in responses by default
3. **Access Control** - Role-based permissions for who can access relationships

This gives you maximum flexibility: relationships exist when you need them, but are controlled by explicit access rules.

---

## Tier 1: Data Model Relationships

### Auto-Generated from Foreign Keys

When you define a foreign key, Bangstack automatically infers relationships:

```yaml
models:
  User:
    fields:
      name: { type: string }

  Task:
    fields:
      title: { type: string }
      assigned_to_id:
        type: uuid
        foreign_key: User
```

**Auto-generated relationships**:
- `Task.assigned_to` → `User` (forward relationship)
- `User.tasks` → `Task[]` (inverse relationship)

Both exist at the ORM level and can be queried internally.

### Explicit Relationships

For complex relationships or more control:

```yaml
relationships:
  - type: one_to_many
    from: User
    to: Task
    foreign_key: assigned_to_id
    inverse: true  # Create inverse relationship (default: true)
    description: Users can be assigned multiple tasks
```

### Disabling Inverse Relationships

```yaml
relationships:
  - type: one_to_many
    from: User
    to: AuditLog
    foreign_key: user_id
    inverse: false  # Don't create User.audit_logs relationship
```

**When to disable inverse**:
- Internal tracking tables (audit logs, analytics events)
- One-way references that don't make sense in reverse
- Performance: avoiding loading large collections

---

## Tier 2: Default API Behavior

### Include by Default vs Opt-In

Control whether relationships are automatically included in API responses:

```yaml
relationships:
  # Always included (good for essential data)
  - type: one_to_many
    from: Project
    to: Task
    api_access:
      include_by_default: true  # GET /projects/:id includes tasks

  # Opt-in via query param (good for large collections)
  - type: one_to_many
    from: User
    to: AuditLog
    api_access:
      include_by_default: false  # Not included unless ?include=audit_logs
      queryable: true  # Can be requested
```

**Generated API behavior**:

```bash
# Default response (no audit logs)
GET /users/john-id
{
  "id": "john-id",
  "name": "John",
  "email": "john@example.com"
}

# Opt-in to include audit logs
GET /users/john-id?include=audit_logs
{
  "id": "john-id",
  "name": "John",
  "email": "john@example.com",
  "audit_logs": [...]
}
```

### Making Relationships Non-Queryable

```yaml
relationships:
  - type: one_to_many
    from: User
    to: InternalMetric
    api_access:
      queryable: false  # Cannot be included via API, only internal queries
```

---

## Tier 3: Role-Based Access Control

### Who Can Read Relationships

```yaml
models:
  User:
    fields:
      role: { type: enum, enum_values: [admin, user, guest] }

relationships:
  # Only admins and superadmins can access audit logs
  - type: one_to_many
    from: User
    to: AuditLog
    api_access:
      read_roles: [admin, superadmin]
      include_by_default: false
```

**Generated API behavior**:

```typescript
// Regular user tries to access
GET /users/john-id?include=audit_logs
→ 403 Forbidden: You don't have permission to include audit_logs

// Admin accesses
GET /users/john-id?include=audit_logs
→ { ..., audit_logs: [...] }  // ✅ Allowed
```

### Field-Level Access Control

Individual fields can also have access restrictions:

```yaml
models:
  User:
    fields:
      email:
        type: string
        api_access:
          read_roles: [owner, admin]  # Only owner or admin can read
          write_roles: [owner]  # Only owner can update

      salary:
        type: decimal
        api_access:
          read_roles: [admin, hr]  # Only admin and HR can see
          write_roles: [admin, hr]

      password_hash:
        type: string
        api_access:
          read_roles: []  # Never readable
          write_roles: []  # Never writable
        exclude_from_forms: true
```

**Generated middleware**:

```typescript
// Filter fields based on user role
function filterFields(obj, user, model) {
  const filtered = {};

  for (const [field, value] of Object.entries(obj)) {
    const fieldSchema = model.fields[field];

    // Check if user has permission to read this field
    if (!fieldSchema.api_access?.read_roles ||
        fieldSchema.api_access.read_roles.includes(user.role) ||
        (fieldSchema.api_access.read_roles.includes('owner') && obj.id === user.id)) {
      filtered[field] = value;
    }
  }

  return filtered;
}
```

---

## Complete Examples

### Example 1: Task Management with Audit Logs

```yaml
models:
  User:
    fields:
      name: { type: string }
      role: { type: enum, enum_values: [admin, user, guest] }

  Task:
    fields:
      title: { type: string }
      assigned_to_id: { type: uuid, foreign_key: User }

  AuditLog:
    fields:
      user_id: { type: uuid, foreign_key: User }
      action: { type: string }
      resource_type: { type: string }
      timestamp: { type: timestamp, system_field: true }

relationships:
  # User → Tasks (public, included by default)
  - type: one_to_many
    from: User
    to: Task
    foreign_key: assigned_to_id
    inverse: true
    api_access:
      include_by_default: true  # Always include user's tasks
      read_roles: []  # Anyone can see (if they have access to the user)

  # User → AuditLogs (admin-only, opt-in)
  - type: one_to_many
    from: User
    to: AuditLog
    foreign_key: user_id
    inverse: true
    api_access:
      include_by_default: false  # Never auto-included
      queryable: true  # Can be requested
      read_roles: [admin, superadmin]  # Only admins
```

**API behavior**:

```bash
# Regular user views their profile
GET /users/john-id
{
  "id": "john-id",
  "name": "John",
  "tasks": [  # ✅ Included by default
    { "id": "task-1", "title": "Review PR" }
  ]
  # No audit_logs (not included by default)
}

# Regular user tries to include audit logs
GET /users/john-id?include=audit_logs
→ 403 Forbidden

# Admin views user profile
GET /users/john-id?include=audit_logs
{
  "id": "john-id",
  "name": "John",
  "tasks": [...],
  "audit_logs": [  # ✅ Admin can access
    { "action": "login", "timestamp": "..." }
  ]
}
```

### Example 2: Sensitive Financial Data

```yaml
models:
  Employee:
    fields:
      name: { type: string }

      email:
        type: string
        api_access:
          read_roles: [owner, hr, admin]
          write_roles: [owner, hr]

      salary:
        type: decimal
        api_access:
          read_roles: [hr, admin]  # Only HR and admin
          write_roles: [hr, admin]

      social_security:
        type: string
        api_access:
          read_roles: [hr]  # Only HR, not even admin
          write_roles: [hr]
        exclude_from_forms: true

  Payroll:
    fields:
      employee_id: { type: uuid, foreign_key: Employee }
      amount: { type: decimal }
      date: { type: date }

relationships:
  - type: one_to_many
    from: Employee
    to: Payroll
    api_access:
      include_by_default: false
      read_roles: [hr, admin]
```

**API behavior**:

```bash
# Employee views own profile
GET /employees/sarah-id  (authenticated as Sarah)
{
  "id": "sarah-id",
  "name": "Sarah",
  "email": "sarah@company.com"  # ✅ Owner can see
  # salary excluded (not HR/admin)
  # social_security excluded
  # payroll excluded (not included by default + no HR role)
}

# HR views employee profile
GET /employees/sarah-id  (authenticated as HR)
{
  "id": "sarah-id",
  "name": "Sarah",
  "email": "sarah@company.com",
  "salary": 75000,  # ✅ HR can see
  "social_security": "***-**-1234"  # ✅ HR can see
}

# HR includes payroll
GET /employees/sarah-id?include=payroll
{
  ...,
  "payroll": [  # ✅ HR can include
    { "amount": 3125, "date": "2025-10-01" },
    { "amount": 3125, "date": "2025-09-01" }
  ]
}
```

---

## Special Access Rules

### Owner Access

Use the special `owner` role to allow users to access their own data:

```yaml
models:
  User:
    fields:
      email:
        type: string
        api_access:
          read_roles: [owner, admin]  # Owner can read their own email
          write_roles: [owner]  # Owner can update their own email
```

**Generated logic**:

```typescript
if (roles.includes('owner') && resource.id === req.user.id) {
  // Allow access
}
```

### Combined Role Rules

```yaml
relationships:
  - type: one_to_many
    from: Project
    to: Task
    api_access:
      read_roles: [owner, admin, team_member]
      # Owner = project owner
      # Admin = system admin
      # team_member = anyone assigned to a task in the project
```

---

## Best Practices

### 1. Use Inverse Relationships Wisely

```yaml
# ✅ Good: Essential relationships
- from: Project
  to: Task
  inverse: true  # Projects need to list their tasks

# ✅ Good: Opt-out for internal data
- from: User
  to: AuditLog
  inverse: false  # Users don't need audit log relationship

# ❌ Avoid: Creating every possible inverse
- from: User
  to: LoginEvent
  inverse: true  # Clutters User model with internal data
```

### 2. Default to Opt-In for Large Collections

```yaml
# ✅ Good: Large collections are opt-in
- from: User
  to: Order
  api_access:
    include_by_default: false  # Orders can be many
    queryable: true

# ❌ Avoid: Auto-including large collections
- from: User
  to: Order
  api_access:
    include_by_default: true  # Could return thousands of orders!
```

### 3. Layer Security from Most to Least Restrictive

```yaml
# Layer 1: Field is excluded from API entirely
password_hash:
  api_access:
    read_roles: []
    write_roles: []

# Layer 2: Field readable only by specific roles
salary:
  api_access:
    read_roles: [hr, admin]

# Layer 3: Relationship accessible but opt-in
audit_logs:
  api_access:
    include_by_default: false
    read_roles: [admin]
```

---

## Summary

**Three-Tier System**:
1. **Data Model** - Relationships exist at ORM level (flexible)
2. **Default API** - Control what's included by default (performance)
3. **Access Control** - Role-based permissions (security)

**Key Properties**:
- `inverse: true|false` - Create inverse relationship
- `include_by_default: true|false` - Auto-include in responses
- `queryable: true|false` - Can be requested via ?include=
- `read_roles: [...]` - Who can read the relationship/field
- `write_roles: [...]` - Who can write the field

This gives you **maximum flexibility** while maintaining **security** and **performance**.
