# @taylordb/forms-taylordb
Type: adapter (binds @taylordb/forms-core to @taylordb/query-builder)

## Overview

`defineTaylorForm(taylorSchema)(config)` builds a form definition whose
persistence is wired to a TaylorDB query builder. Auto-generates the
`resolvers` and `session` arguments `createFormsActions` (from
`@taylordb/forms-api`) needs — typical form router drops from ~150 lines
of `update().set({...})` boilerplate to ~10.

Built around the canonical TaylorDB pattern: **one table doubles as the
session record AND the answer storage**. The session id is the row's primary
key. An optional `completedColumn` flips on submission; completed rows are
not resumable on `loadSession`.

## Required: runtime schema

The first argument to `defineTaylorForm` is the runtime `taylorSchema`
(`@taylordb/query-builder` ≥0.18) produced by `defineTaylorSchema({...})`.
This carries every column's runtime descriptor — type, required, mode,
options, linkedTo — so the adapter doesn't have to guess column shapes.

```ts
import { taylorSchema } from '../taylordb/types'

const form = defineTaylorForm(taylorSchema)({
  sharedSteps: [...],
  taylordb: { table: 'candidates', completedColumn: 'submitted' },
})
```

## Field-name vocabulary

Steps use TaylorDB-flavored names instead of forms-core's abstract `{id, type}`:

  - `taylordbFieldName` — the step's id AND its default column on the table.
  - `questionType`      — the handler kind (`'text' | 'email' | 'file_upload' | …`).

Internally translated to `{id, type}` before forms-core sees them, so the
wire format and the client adapters (forms-api / forms-ui) are unchanged.

## Schema-driven behavior

Once the schema is passed, the adapter resolves several things automatically
without per-step config:

  - **Cardinality** for `save` normalization. `select { mode: 'single' }`
    columns → unwrap `[x] → x` and `[] → null`. `select { mode: 'multi' }`
    columns → keep array; `[] → null`. No `cardinality` config knob — derived
    from the schema descriptor at runtime.
  - **Attachment columns** (simple steps): auto `save: 'noop'` and built-in
    load that turns the row's `string[]` paths into `FileAnswer[]`, prefixed
    with `taylordb.mediaHost` (default `https://media.taylordb.ai`).
    `form.createActions(...).uploadFile(ctx, { sessionId, stepId, file, name })`
    uploads bytes through `qb.uploadAttachments`, replaces the attachment
    column, and returns `{ url, name, type, size }` for forms-ui file mappers.
  - **Validation (per-save only)**: `step.validate` is built by
    `createPerSaveStepValidate` → `defaultValidatorForDescriptor` (the only
    validator unless that step/field defines `validate`, which overwrites for
    that step only). Runs on `saveAnswer` / autosave `saveStep`.
    Column `required` in `taylorSchema` ≠ step `optional`.
  - **Table existence** is verified at runtime — `defineTaylorForm` throws
    if `taylordb.table` isn't in the schema.

## Compile-time guarantees

Validates at the call site:

  - Every step's column exists on `schema[table]`.
  - Every `questionType` pairs with the right descriptor type — strict
    per-handler-kind table:

      - text/email/short_text/long_text/deep_dive/website/phone_number
                                                  → `'text'`
      - date                                      → `'date'`
      - number/rating/opinion_scale/nps           → `'number'`
      - yes_no/legal                              → `'checkbox'`
      - dropdown/picture_choice                   → `'select'` mode `'single'`
      - multiple_choice/checkbox/ranking          → `'select'` (either mode)
      - file_upload/multi_format                  → `'attachment'`

  - `table`, `idColumn`, `completedColumn` are typed against
    `schema[table]`'s keys.

Diagnostics land on the offending step literal with text naming the
mismatch (e.g. `Step 'phone': questionType 'file_upload' incompatible with
column 'phone' — expected an 'attachment' field, got 'text'`).

## Escape hatches

  - `taylordb.steps[id].save = 'noop'` — value never written; pairing check
    still verifies column EXISTS but skips the descriptor-type match.
  - `taylordb.steps[id].save = (ctx, sid, value) => ...` — custom save
    runs instead of the auto pipeline.
  - `taylordb.steps[id].column` — override the default column name (string
    for simple steps, `{ fieldName: 'realCol' }` for composite fields).

## Related packages

- `@taylordb/forms-ui` — shared React runtime, inputs, autosave, locales.
- `@taylordb/forms-ui-typeform` — Typeform-style renderer (`Form`, `Question`, …).
- `@taylordb/forms-ui-googleform` — Google Forms-style renderer (`Section`, paging, …).
- `@taylordb/forms-api` — server actions (`createFormsActions`).
- `@taylordb/forms-core` — handlers and `defineForm` (used internally).

On the client, import a renderer package — not `@taylordb/forms-ui` alone — and
pass `form.sharedSteps` from this adapter into `<Form sharedSteps={…}>`. See
`docs/api.md` (Client UI).

## Docs

- API reference: ./docs/api.md
- Errors: ./docs/errors.md
- Migration: ./docs/migration.md
- Client UI wiring: ./docs/api.md#client-ui
- Uploads + tRPC: ./docs/api.md#generated-uploadfile

## Minimal example

```ts
import { taylorSchema } from '../taylordb/types'

const candidateForm = defineTaylorForm(taylorSchema)({
  sharedSteps: [
    { taylordbFieldName: 'name',   questionType: 'text' },
    { taylordbFieldName: 'email',  questionType: 'email' },
    { taylordbFieldName: 'resume', questionType: 'file_upload' },
  ] as const,
  taylordb: {
    table: 'candidates',
    completedColumn: 'submitted',
    initialValues: { submitted: false },
  },
})

// If callbacks need typed answers, pass the answer map explicitly.
type CandidateAnswers = {
  name?: string
  email?: string
}

const typedCandidateForm = defineTaylorForm(taylorSchema)
  .withAnswers<CandidateAnswers>()({
    sharedSteps: [
      { taylordbFieldName: 'name', questionType: 'text' },
      {
        taylordbFieldName: 'email',
        questionType: 'email',
        showWhen: (answers) => answers.name !== undefined,
      },
    ] as const,
    taylordb: { table: 'candidates' },
  })

const actions = candidateForm.createActions<Context>({
  ctxToQB: (ctx) => ctx.queryBuilder,
  emailConfig,
})
// saveAnswer / submitForm → validateStoredStep → sharedSteps[].validate
// (defaultValidatorForDescriptor unless the step defines validate)
// uploadFile → qb.uploadAttachments + attachment column replacement
```

## Upload tRPC pattern

Server upload procedures should accept `multipart/form-data` with:

- `file`: browser `File` / `Blob`
- `sessionId`: autosave session row id
- `stepId`: shared step id / `taylordbFieldName` (for example `resume`)

Then call:

```ts
await actions.uploadFile(ctx, { sessionId, stepId, file, name: file.name })
// -> { url, name, type, size }
```

Do not pass a raw column name or maintain a per-column allowlist in the upload
router. `uploadFile` validates that `stepId` maps to a simple attachment step
and uses the form's `columnFor` / `steps[id].column` config to find the column.

On the client, do NOT hand-write per-step `toApiValue` mappers — pass a single
`uploadFile` to `form.mappers(...)` and it is auto-wired for every
`file_upload` / `multi_format` step:

```ts
const mappers = form.mappers({}, {
  uploadFile: async ({ stepId, file, name }) => {
    const body = new FormData()
    body.set('file', file, name)
    body.set('sessionId', String(client.sessionId))
    body.set('stepId', stepId)
    return trpc.upload.uploadCandidateFile.mutate(body)
  },
})
```

Adding another file question is just: add the step + the `attachment` column.
No mapper change needed.
