UNPKG

@athenox/talos

Version:

Talos: a backend you describe instead of write. Declare a schema, write the rules in plain language, map URLs to sentences; a small built-in model turns them into query plans.

96 lines (77 loc) • 5.18 kB
# 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.