# Views API

Views provide named column projections that limit which fields are returned in API responses. Each view can have its own access control, and all security pillars (firewall, masking) still apply.

## Endpoints

Named views are queried through a dedicated path endpoint:

```
GET /api/v1/{resource}/views/{view-name}
```

If you set `read.defaultView`, bare `GET /api/v1/{resource}` can still resolve
to that view. Named views themselves are addressed via `/views/{view-name}`.

## Example

Given this definition (views live under [`read.views`](/define/read#views-under-read-views)):

```typescript title="quickback/features/candidates/candidates.ts"
import { feature, q } from "@quickback/compiler";

export default feature("candidates", {
  columns: {
    id:             q.id(),
    name:           q.text({ maxLength: 200 }).required(),
    email:          q.text({ maxLength: 320 }).required(),
    phone:          q.text({ maxLength: 32 }).optional(),
    resumeUrl:      q.url({ maxLength: 2048 }).optional(),
    source:         q.enum(['linkedin', 'referral', 'careers-page']).default('referral').required(),
    organizationId: q.scope("organization"),
    ...q.audit(),
    ...q.softDelete(),
  },
  // `email` and `phone` are detected as sensitive, so a decision is required.
  // Here they stay readable to the same roles the `full` view is gated on.
  masking: {
    email: { type: 'email', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },
    phone: { type: 'phone', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },
  },
  read: {
    access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },
    views: {
      pipeline: {
        fields: ['id', 'name', 'source'],
        access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },
      },
      full: {
        fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source'],
        access: { roles: ['owner', 'hiring-manager', 'recruiter'] },
      },
    },
  },
  // ...
});
```

### Request

```bash
curl /api/v1/candidates/views/pipeline \
  -H "Authorization: Bearer <token>"
```

### Response

```json
{
  "data": [
    { "id": "cand_001", "name": "Alice Johnson", "source": "LinkedIn" },
    { "id": "cand_002", "name": "Bob Martinez", "source": "referral" }
  ],
  "view": "pipeline",
  "pagination": {
    "count": 2,
    "page": 1,
    "pageSize": 50,
    "hasMore": false,
    "nextCursor": null,
    "prevCursor": null
  }
}
```

Only the fields specified in the view definition are returned. Requesting the `full` view with an `interviewer` role returns 403.

## Query Parameters

Views support the same query parameters as the list endpoint:

| Parameter | Description | Default |
|-----------|-------------|---------|
| `limit` | Number of records to return (1–100) | `50` |
| `offset` | Number of records to skip | `0` |
| `sort` | Field to sort by | `createdAt` |
| `order` | Sort direction (`asc` or `desc`) | `desc` |
| `{field}` | Filter by exact value | — |
| `{field}.gt` | Greater than | — |
| `{field}.gte` | Greater than or equal | — |
| `{field}.lt` | Less than | — |
| `{field}.lte` | Less than or equal | — |
| `{field}.ne` | Not equal | — |
| `{field}.like` | Pattern match (SQL LIKE) | — |
| `{field}.in` | Match any value in comma-separated list | — |

### Examples

```bash
# Paginated with sorting
GET /api/v1/candidates/views/pipeline?limit=10&offset=0&sort=name&order=asc

# With filters
GET /api/v1/candidates/views/pipeline?source=referral&name.like=%25Johnson%25

# Combined
GET /api/v1/candidates/views/pipeline?source.in=LinkedIn,referral&sort=name&order=asc&limit=25
```

## Security

All security pillars apply to view endpoints:

1. **Authentication** — Required (401 if missing)
2. **Firewall** — Organization/user isolation applied to all results. Only records the user owns or has access to are returned.
3. **Access** — Per-view role check. Each view can require different roles.
4. **Masking** — Applied to all returned fields. If a masked field is in the view's field list, the masking rules still apply based on the user's role.

### Access Control Per View

Different views can require different permission levels:

```typescript
read: {
  access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },
  views: {
    // Available to all org members including interviewers
    pipeline: {
      fields: ['id', 'name', 'source'],
      access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },
    },
    // Restricted view with contact details
    full: {
      fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source'],
      access: { roles: ['owner', 'hiring-manager', 'recruiter'] },
    },
  },
}
```

An `interviewer` requesting the `full` view receives:

```json
{
  "type": "https://quickback.dev/problems/access-role-required",
  "title": "Access role required",
  "status": 403,
  "detail": "Insufficient permissions",
  "instance": "/api/v1/candidates/views/full",
  "layer": "access",
  "code": "ACCESS_ROLE_REQUIRED",
  "details": { "required": ["hiring-manager", "recruiter"], "current": ["interviewer"] }
}
```

### Masking in Views

Masking is applied after field projection. If your masking config hides `phone` from interviewers:

```typescript
masking: {
  phone: { type: 'phone', show: { roles: ['hiring-manager', 'recruiter'] } },
}
```

An `interviewer` requesting a view that includes `phone` will see the masked value (e.g., `***-***-5678`), while a `recruiter` sees the full value.

## See Also

- [Defining Views](/define/views) — How to configure views in defineTable
- [Query Parameters](/api/query-params) — Full query parameter reference
- [Access Control](/define/access) — Role-based access configuration
