# The schema

```ts
defineApp({ schema: { tables: Table[], functions?: Fn[] }, prompt, store?, seed?, actions?, telemetry? })
```

The model reads the tables' `as`, their logical columns, declared values and
`says`; everything else is for the runtime. Columns in the options below are
**physical** unless marked *logical*.

## What the model reads

| field | |
|---|---|
| `as` | the table's name for the model (`recipes`) |
| `table` | the physical table |
| `columns` | *logical* → physical: `{ author: 'author_id' }` |
| `enums` | declared values of a *logical* column: `{ course: ['main', 'dessert'] }`. A value named in a request is matched to its column, and a value is kept in telemetry (other values are masked) |
| `flags` | booleans as two words: `{ visible: ['hidden', 'visible'] }` — 0/1 in the database |
| `says` | other names for the table, in any language: `['dishes', 'recettes']` |
| `columnSays` | other names for a *logical* column: `{ minutes: ['time', 'duration'] }` |

`checkVocab` (default on) warns once about schema words the model does not
know; they still work — the model points at them — but a known word reads
better. `addSurfaceForms({ published: ['publié', 'publiés'] })` teaches the
forms of a declared value.

## Reading

| field | |
|---|---|
| `relations` | rows this one points at, returned as an object: `{ author: { table: 'users', on: 'author_id', columns: { id: 'id', name: 'name' } } }`. Filterable as `'author.name'` in `where` |
| `lists` | rows pointing at this one, returned as an array: `{ comments: { table: 'comments', on: 'recipe_id', columns: {...}, orderBy: 'created_at', where?, relations?, limit? } }` |
| `many` | many-to-many through a join table: `{ tags: { through: 'recipe_tags', from: 'recipe_id', to: 'tag_id', table: 'tags', columns: {...} } }`; written from `tagsIds` in the body |
| `tallies` | a count per row: `{ likes: { table: 'likes', on: 'recipe_id', where? } }` — usable in `orderBy` |
| `computed` | fields made from the row: `{ avatar: r => gravatar(r.email) }` |
| `hidden` | never returned: `['password']`, or a relation's `'author.email'` |
| `searchable` | *logical* columns a route's `search` looks in |
| `view` | read from a view instead of the table |
| `types` | how a column reads back: `'boolean' \| 'date' \| 'json' \| 'number'` (and, for Talos's own database, is created) |
| `json` | columns holding JSON, parsed on the way out |
| `read` / `write` | per-column converters (what `fromDrizzle` fills from the ORM) |

## Writing

| field | |
|---|---|
| `rules` | per *logical* column: `required` (`true` or `'insert'`), `min`, `max` (length or value), `email`, `pattern`, `trim`, `unique` (or its message), `uniqueWith`, `message`. A failed rule is a 400 with `data.errors: [{ msg, column }]` |
| `defaults` | values computed when a write leaves a column out: `{ insert: { created_at: () => new Date() }, update: { updated_at: ... } }` |
| `hash` | replaced by its hash after the rules: `{ password: hashPassword }` |
| `slugs` | `{ slug: 'title' }`, or `{ slug: { from: 'title', keep: true } }` to set it once |
| `files` | uploaded files — see [files.md](files.md) |
| `infer` | a column filled by meaning when left empty: `{ category: { from: ['title', 'body'], labels: { sport: 'match, stade', culture: 'concert, expo' } } }` |
| `writable` | the only columns a caller may write |
| `children` | rows elsewhere pointing here, on delete: `[{ table: 'likes', on: 'recipe_id', cascade: true }]`, or `restrict` with a `message` |
| `after` | declared functions to run after a write: `{ insert: ['notifyModerators'] }` |

## Who may do what

```ts
roles: {
  read?: string[]              // default: everyone
  write?: string[]             // insert, update, delete
  create?: string[], remove?: string[]
  columns?: { status: ['editor'] }   // who may write one column
}
ownerColumn: 'user_id', ownerFrom: 'userId'   // rows belong to the caller: pinned on every read and write
```

The caller's role is the `role` of the context (`ctx`) a route passes; without
one it is `guest`. The model is never asked whether something is allowed:
over six training runs its refusal of off-topic requests reached 100 % while its
refusal of *unauthorised* ones stalled at 61 %. Permission is code.

## Functions

Side effects the prose may name — sending a newsletter, rebuilding an index:

```ts
defineApp({
  schema: { tables, functions: [{ name: 'sendNewsletter', params: ['subject'], required: ['subject'], roles: ['admin'], says: ['send the newsletter'] }] },
  actions: { sendNewsletter: async ({ args, ctx }) => { /* … */ return { sent: 120 } } },
})
```

A plan `CALL`s a function by its exact name only; a missing required argument
is a 400, not a call.

## Talos's own database

Without `store`, the tables above are created in `.talos/data.db` (`TALOS_DB`):
every physical column, relation key, file column and image variant; `id` as the
integer primary key; the first declared value as default; `types` numbers as
numeric. Columns are added on later starts, never dropped. `seed: { users: [...] }`
fills empty tables, with logical or physical names, hashed and defaulted.
