# Guards - Field Modification Rules

Control which fields can be modified in CREATE vs UPDATE operations.

## Basic Usage

```typescript
// features/applications/applications.ts
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { defineTable } from '@quickback/compiler';

export const applications = sqliteTable('applications', {
  id: text('id').primaryKey(),
  candidateId: text('candidate_id').notNull(),
  jobId: text('job_id').notNull(),
  stage: text('stage').notNull().default('applied'),
  appliedAt: text('applied_at').notNull(),
  notes: text('notes'),
  organizationId: text('organization_id').notNull(),
  // ── quickback:audit (compiler-managed — edits are validated, not merged) ──
  createdAt: text('created_at').notNull().default('1970-01-01T00:00:00.000Z').$defaultFn(() => new Date().toISOString()),
  modifiedAt: text('modified_at').notNull().default('1970-01-01T00:00:00.000Z').$defaultFn(() => new Date().toISOString()).$onUpdate(() => new Date().toISOString()),
  createdBy: text('created_by'),
  modifiedBy: text('modified_by'),
  deletedAt: text('deleted_at'),
  deletedBy: text('deleted_by'),
});

export default defineTable(applications, {
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  guards: {
    createable: ["candidateId", "jobId", "notes"],
    updatable: ["notes"],
    protected: {
      stage: ["advance-stage", "reject"],
    },
    immutable: ["appliedAt", "candidateId", "jobId"],
  },
  create: { /* ... */ },
  update: { /* ... */ },
  delete: { /* ... */ },
});
```

## Configuration Options

```typescript
guards: {
  // Fields allowed on CREATE
  createable?: string[];

  // Fields allowed on UPDATE/PATCH
  updatable?: string[];

  // Fields only modifiable via specific actions
  protected?: Record<string, string[]>;

  // Fields set on CREATE, never modified after
  immutable?: string[];
}
```

## How It Works

| List | What it controls |
|------|------------------|
| `createable` | Fields allowed in create (POST) request body |
| `updatable` | Fields allowed in update (PATCH) request body |
| `protected` | Fields blocked from direct writes, only modifiable via named actions |
| `immutable` | Fields allowed on create, blocked on all updates |

**Combining lists:**

- `createable` + `updatable` - Most fields go in both (can set on create AND modify later)
- `createable` only - Field is set once, cannot be changed via update
- `protected` - Don't also list in `createable` or `updatable` (they're mutually exclusive)
- `immutable` - Don't also list in `updatable` (contradiction)

```typescript
guards: {
  createable: ["candidateId", "jobId", "notes", "source"],
  updatable: ["notes"],
  // "candidateId", "jobId" are only in createable = set once, can't change via update
  protected: {
    stage: ["advance-stage", "reject"],  // NOT in createable/updatable
  },
  immutable: ["appliedAt"],     // NOT in updatable
}
```

**If a field is not listed anywhere, it cannot be set by the client.**

## System-Managed Fields (Always Protected)

`GUARDS_CONFIG.systemManaged` rejects any field in this set from client input — even with `guards: false`. The set always includes the audit fields:

- `createdAt`, `createdBy`
- `modifiedAt`, `modifiedBy`
- `deletedAt`, `deletedBy`

It also extends with **scope columns** — any firewall predicate whose `equals` references `ctx.*` adds its `field` to systemManaged. Most commonly this comes from `q.scope()`:

```typescript
columns: {
  organizationId: q.scope('organization'),  // → adds 'organizationId' to systemManaged
}
```

The same applies to a hand-rolled firewall:

```typescript
firewall: [
  { field: 'tenantId',    equals: 'ctx.activeOrgId' },        // → systemManaged
  { field: 'workspaceId', equals: 'ctx.activeWorkspaceId' },  // → systemManaged
  { field: 'status',      equals: 'active' },                 // literal, NOT added
]
```

The semantic rule is "if the firewall says this column's value comes from the request context, the client cannot submit it." The value is auto-populated on create / upsert from `ctx`.

### Changeset ops widen the set per-surface

On [changeset](/define/changesets) ops, an owned child's
systemManaged set additionally includes its **spine FK and every `inherit`
column** — the changeset supplies those structurally (the FK from the parent
op's id, inherit columns from the firewall-verified parent row), so a
client-supplied value is a pointer-carrying `GUARD_SYSTEM_MANAGED` rejection
(never silently stripped or stamped-over).

The child's **plain-CRUD guards are unaffected**: a junction that keeps its FK
in `createable` for its raw `POST` route stays correct — the same field
arriving in a changeset op is rejected because that surface stamps it. Two
surfaces, one declared rule each.

## Example

```typescript
guards: {
  createable: ["candidateId", "jobId", "notes"],
  updatable: ["notes"],
  protected: {
    stage: ["advance-stage", "reject"],     // Only these actions can modify stage
  },
  immutable: ["appliedAt", "candidateId", "jobId"],
}
```

## Disabling Guards

```typescript
guards: false  // Only system fields protected
```

When `guards: false` is set, all user-defined fields become writable via direct create/update/upsert operations. This is useful for:
- External sync scenarios where you need full field control
- Batch upsert operations where field restrictions would be limiting
- Simple tables where field-level protection isn't needed

## Upsert with External IDs

When you disable guards AND use client-provided IDs, you unlock upsert operations. This is designed for **syncing data from external systems**.

### Requirements for Upsert

1. `generateId: false` in database config (client provides IDs)
2. `guards: false` in resource definition

### How Upsert Works

```
PUT /resource/:id
├── Record exists? → UPDATE (replace all fields)
└── Record missing? → CREATE with provided ID
```

### Database Config

```typescript
// quickback.config.ts
export default {
  database: {
    generateId: false,  // Client provides IDs (enables upsert)
    // Other options: 'uuid' | 'cuid' | 'nanoid' | 'short' | 'prefixed' | 'serial'
  }
};
```

### Table Definition

```typescript
// features/integrations/ats-imports.ts
import { q, defineTable } from '@quickback/compiler';

export const atsImports = q.table('atsImports', {
  id:             q.id(),
  externalId:     q.text().required(),
  payload:        q.text().optional(),
  organizationId: q.scope('organization'),
  ...q.audit(),
  ...q.softDelete(),
});

export default defineTable(atsImports, {
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  guards: false,       // Disables field restrictions
  upsert: {
    access: { roles: ['hiring-manager', 'sync-service'] }
  },
});
```

## What's Still Protected with guards: false

Even with `guards: false`, system-managed fields are ALWAYS protected. This includes:

- `createdAt`, `createdBy` — set on INSERT only
- `modifiedAt`, `modifiedBy` — auto-updated
- `deletedAt`, `deletedBy` — set on soft delete
- Any column declared via `q.scope()` (or any firewall predicate using `equals: 'ctx.*'`) — auto-populated from request context

So `guards: false` only opens up user-defined business fields. Tenant scope and audit fields stay locked down regardless.

## Ownership Auto-Population

When upsert creates a new record, ownership fields are auto-set from context:

```typescript
// Client sends: PUT /ats-imports/ext-123 { candidateName: "Jane Doe" }
// Server creates:
{
  id: "ext-123",           // Client-provided
  candidateName: "Jane Doe", // Client-provided
  organizationId: ctx.activeOrgId,  // Auto-set from firewall
  createdAt: now,          // Auto-set
  createdBy: ctx.userId,   // Auto-set
  modifiedAt: now,         // Auto-set
  modifiedBy: ctx.userId,  // Auto-set
}
```

## Use Cases for Upsert/External IDs

| Use Case | Why upsert? |
|----------|----------|
| External API sync | External system controls the ID |
| Webhook handlers | Events come with their own IDs |
| Data migration | Preserve IDs from source system |
| Idempotent updates | Safe to retry (no duplicate creates) |
| Bulk upsert | Create or update in one operation |

## ID Generation Options

| `generateId` | Upsert Available? | Notes |
|--------------|----------------|-------|
| `'uuid'` | No | Server generates UUID |
| `'cuid'` | No | Server generates CUID |
| `'nanoid'` | No | Server generates nanoid |
| `'short'` | No | Server generates 6-char alphanumeric ID (e.g. `a3F9xK`) |
| `'prefixed'` | No | Server generates prefixed ID (e.g. `room_abc123`) |
| `'serial'` | No | Database auto-increments |
| `false` | Yes (if guards: false) | Client provides ID |

## Compile-Time Validation

The Quickback compiler validates your guards configuration and will error if:

1. **Field in both `createable` and `protected`** - A field cannot be both client-writable on create and action-only
2. **Field in both `updatable` and `protected`** - A field cannot be both client-writable on update and action-only
3. **Field in both `updatable` and `immutable`** - Contradictory: immutable fields cannot be updated
4. **Field in `protected` doesn't exist in schema** - Referenced field must exist in the table
5. **Field in `createable`/`updatable`/`immutable` doesn't exist in schema** - All referenced fields must exist
