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.

140 lines (110 loc) • 5.34 kB
# Getting started ## 1. Describe the app One `defineApp` holds everything the backend knows: the schema, the rules in prose, and optionally the database. ```ts // server/utils/talos.ts import { defineApp, hashPassword } from '@athenox/talos' export const app = defineApp({ prompt: ` show the recipes, the cooks and the comments. a member may add a recipe, and change or delete their own. a member may comment. an admin may do anything.`, schema: { tables: [ { as: 'recipes', table: 'recipes', says: ['dishes', 'recettes'], columns: { id: 'id', title: 'title', body: 'body', course: 'course', minutes: 'minutes', author: 'author_id', date: 'created_at' }, enums: { course: ['main', 'starter', 'dessert'] }, types: { minutes: 'number' }, defaults: { insert: { created_at: () => new Date().toISOString() } }, relations: { author: { table: 'users', on: 'author_id', columns: { id: 'id', name: 'name' } } }, rules: { title: { required: 'insert', trim: true, min: 3, max: 120, message: 'A title of 3 to 120 characters' } }, roles: { write: ['member', 'admin'] }, }, { as: 'users', table: 'users', columns: { id: 'id', name: 'name', email: 'email', password: 'password', role: 'role' }, enums: { role: ['member', 'admin'] }, hash: { password: hashPassword }, hidden: ['password', 'email'], roles: { write: ['admin'] }, }, ], }, seed: { users: [{ name: 'Ada', email: 'ada@example.com', password: 'change-me-please', role: 'admin' }], }, }) ``` **The prompt** is read by the model with every request. Write it the way you would explain the app to a colleague: what can be shown, who may do what. It guides the plan; it does not grant anything — permissions are the schema's `roles` and your route guards. **The schema** is described in [schema.md](schema.md). Logical column names (the keys of `columns`) are what the model reads; physical names are what the database has. Keep them equal and in English when you can (`title`, not `titre`): rows come back with physical names, and the model knows English schema words best. Put the words people actually use in `says` and `columnSays`. ## 2. Pick a database **None.** Leave `store` out: Talos opens `.talos/data.db` (or `TALOS_DB`), creates every table and column the schema names, adds new ones on the next start and never drops anything. `id` is the integer primary key, a column with declared values defaults to the first one (`role: ['member', 'admin']` → `member`), and `seed` rows go into tables that are still empty — hashed and defaulted as the schema says. `await app.ready` resolves once the file is open. **Yours.** Pass a store: ```ts import { DatabaseSync } from 'node:sqlite' import mysql from 'mysql2/promise' import { sqliteStore, mysqlStore, syncSchema } from '@athenox/talos' defineApp({ ..., store: sqliteStore(new DatabaseSync('app.db')) }) // or better-sqlite3 defineApp({ ..., store: mysqlStore(mysql.createPool(process.env.DATABASE_URL)) }) ``` With a Drizzle schema, `fromDrizzle(schema)` gives the tables and `syncSchema(store, schema)` creates what is missing (see [reference.md](reference.md)). ## 3. Serve it ### Nuxt / Nitro ```ts // server/api/[...].ts import { defineRoutes } from '@athenox/talos' export default defineRoutes(app, { 'GET /api/recipes': { say: 'show the recipes', orderBy: '-date', limit: 20, total: true }, 'GET /api/recipes/:id': { say: 'show the recipe {id}', one: true, notFound: 'No such recipe' }, 'POST /api/recipes': { say: 'add a recipe', guard: member, ctx: asMember, stamp: (_, me) => ({ author_id: me.id }) }, 'PUT /api/recipes/:id': { say: 'change the recipe {id}', guard: member, ctx: asMember, where: own }, 'DELETE /api/recipes/:id': { say: 'delete the recipe {id}', guard: member, ctx: asMember, where: own }, }) ``` `member`, `asMember` and `own` come from [accounts](accounts.md): ```ts export const member = users.guard() // the signed-in account, or 401 export const asMember = (me) => ({ userId: me.id, role: me.role }) // what Talos checks roles against export const own = (_, me) => (me.role === 'admin' ? {} : { author_id: me.id }) // rows that are theirs ``` In `nuxt.config.ts`, keep Talos out of the bundle (it loads its model from its own folder): ```ts nitro: { externals: { external: ['@athenox/talos', 'onnxruntime-node', 'sharp'] } } ``` ### Plain h3 / Node ```ts import { createServer } from 'node:http' import { createApp, toNodeListener } from 'h3' const server = createApp() server.use(defineRoutes(app, { /* the same table */ })) createServer(toNodeListener(server)).listen(3000) ``` ## 4. Check what it understood Every answer carries the plan in its `x-talos-plan` header and the model call in `x-talos-call`: ``` x-talos-plan: delete the recipe 12 => DELETE recipes FILTER id VAL 12 (compiled) ``` `(compiled)` means the plan was replayed without the model. The first call of a route shows the model's plan — read it once when you add a route. ## Next - [Routes](routes.md): templates, every option, free-text requests - [Files and images](files.md): uploads - [Telemetry and feedback](telemetry.md)