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.

103 lines (85 loc) • 5.13 kB
# Routes ## A route is a sentence ```ts const del = app.route('delete the recipe {id}', { ops: ['DELETE'] }) const { status, result, plan, callId } = await del({ id: 3 }, { userId: 7, role: 'member' }) ``` - `{k}` is a parameter; `[... {k} ...]` is dropped when `k` is missing; `{k:no|yes}` says a boolean with one of two words. - The first call for each shape of the sentence asks the model. The plan is kept with every value that came from a parameter replaced by a slot; later calls bind new values and never run the model. - A plan holding a value that is neither a parameter nor a word of the template is not kept — asked again next time rather than trusted forever. - Before the model reads a sentence, a misspelt word of the app (a table, a column, a value) or a misspelt verb is corrected, and an opening verb (*delete*, *add*, *change*, *show*, and their French forms) fixes the operation. ## A whole API as a table ```ts export default defineRoutes<Member>(app, { 'GET /api/recipes': { say: 'show the recipes', orderBy: '-date', limit: 12, total: true, shape: p => ({ search: p.q, page: Number(p.page) || 1 }), where: p => (p.course ? { course: p.course } : {}) }, 'POST /api/auth/login': users.login, // any h3 handler sits in the table as it is 'GET /uploads/:name': serveFiles('uploads'), }, { guards: { member }, defaults: { firstError: true }, passthrough: false }) ``` The HTTP method fixes the operation (GET → SELECT, POST → INSERT or CALL, PUT/PATCH → UPDATE, DELETE → DELETE); refusal stays possible. URL segments (`/:id`, `/:slug`) name the row: they replace any condition the model wrote on those columns. The body's fields are written by name. `defineRoutes` options: `guards` (named guards for `guard: 'name'`), `defaults` (options every route gets), `passthrough: true` to mount it as middleware that lets unmatched requests through. ### Route options | option | | |---|---| | `say` | the sentence | | `guard` | `(event, params) => user` — throws to refuse; its user is given to the functions below | | `ctx` | `user => ({ userId, role })`: what roles are checked against | | `where` | conditions pinned by code, `{ col: v }` or `(p, user) => ({...})`. Comparisons: `{ gte }`, `{ lt }`, `{ between: [a, b] }`, `{ ne }`, `{ in: [...] }`, `{ null: true }`; `any: [{...}, {...}]` for OR; `'relation.column'` | | `stamp` | `(p, user) => ({ author_id: user.id })`: columns the route writes itself | | `fill` | defaults for missing parameters: `{ visibility: () => 'public' }` | | `only` | the body fields a caller may send | | `form` | `{ checkboxes: [...], nullable: [...] }` for HTML forms | | `shape` | `(p, user, event) => Shape`: per-request options (below) | | `fields` | keep these fields of each row: `['id', 'title', 'author.name']` | | `row` | `row => row`: map each row | | `reply` | `(result, p, user, event) => body`: the response | | `notFound` | the 404 message (or data object) | | `firstError` | only the first failed rule in `data.errors` | | `pin` | which URL segments name the row (`false`: none) | | `download` | `row => ({ path, name, type, inline })`: send a file | | and every `Shape` option | `orderBy`, `limit`, `one`, `total`… | ### Shape (per request) | option | | |---|---| | `orderBy` | `'-date'`, `['-likes', 'title']`; `'minutes~'` puts empty values last; a declared-values column sorts in declared order | | `limit`, `offset`, `page`, `total` | paging; `total` adds `{ total, page, limit }` | | `one` | a single row, 404 when none | | `search` | full text over `searchable` | | `find`, `similar`, `recommend` | ranking — see [search.md](search.md) | | `lists` | which `lists` to load (`false`: none) | | `counts`, `sums` | `{ pending: { status: 'pending' } }`, `{ revenue: { column: 'amount', where } }` | | `returning` | an INSERT returns its row, a DELETE the rows it removed | | `rows`, `upsert`, `ignore` | bulk inserts: `{ created, updated, skipped }` | | `dedupe` | `{ by: 'email' }`: a DELETE that keeps the first of each | | `before` | `row => extra`: runs after the rules, before the write | | `images` | this route's own image processing | | `ops`, `where`, `values`, `stamp` | as above | ## Answers A route answers `200` with the row (`one`), the rows, `{ rows, total, page, limit }` (`total`), `{ id }` for a creation, `{ success: true }` for an update or delete — or what `reply` returns. Errors: `400` with `data.errors: [{ msg, column }]` for rules, `401`/`403` from guards and roles, `404` when nothing matched, `422` when the request is not something this app does. Headers: `x-talos-plan` (the sentence and its plan), `x-talos-call` (the model call, for [feedback](telemetry.md)). ## Free-text requests ```ts const { status, result, plan, callId } = await app.run('the easy desserts under 30 minutes', ctx, { ops: ['SELECT'], limit: 10 }) ``` `run` asks the model every time (no cache): for a search box, an assistant, an admin console. `decide(sentence, ctx, ops?)` returns the plan without running it; `execute(plan, ms, sentence, ctx, shape)` runs a plan however it was made.