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.

158 lines (157 loc) • 7.41 kB
import { type H3Event } from 'h3'; import { type Ctx, type Shape } from './runtime.ts'; type App = { route: (template: string, base?: Shape) => (p: Record<string, unknown>, ctx?: Ctx, shape?: Shape) => Promise<any>; telemetry?: { error: (err: unknown, source: string, callId?: string) => void; }; }; type Params = Record<string, unknown>; export type RouteOptions<U> = Omit<Shape, 'stamp' | 'where'> & { /** the app's own access check, with the URL's parameters; what it returns is handed to the functions below */ guard?: (event: H3Event, params: Record<string, string>) => U | Promise<U>; /** * HTML form semantics for the body: a checkbox left unticked is absent, so * the named fields default to false; an emptied text field means null. */ form?: { checkboxes?: string[]; nullable?: string[]; }; /** answer a failed rule with its first message only, as some forms show */ firstError?: boolean; /** * Which URL parameters name the row (all by default). `false` when they name * something else — `/articles/:id/similar` reads other articles than `:id`. */ pin?: string[] | false; /** the fields a response keeps from each row (`author.name` for a relation's) */ fields?: string[]; /** a read answered with a file: `row => ({ path, name, type })` */ download?: (row: Record<string, any>, p: Params) => { path: string; name?: string; type?: string; inline?: boolean; headers?: Record<string, string>; } | null; /** caller context for the runtime (role, userId), from the guard's result */ ctx?: (user: U) => Ctx; /** conditions the route always applies, besides the row the URL names */ where?: Record<string, unknown> | ((p: Params, user: U) => Record<string, unknown>); /** options that depend on the request: page, search, sort, extra conditions (may be async) */ shape?: (p: Params, user: U, event: H3Event) => Shape | Promise<Shape>; /** the only body fields a caller may write (a reader's form must not set a status) */ only?: string[]; /** * Columns the route writes itself: who, when, a counter's `{ add: 1 }`, or a * stored upload's path. May be async; it runs once the guard has passed. */ stamp?: (p: Params, user: U) => Record<string, unknown> | Promise<Record<string, unknown>>; /** values to use when the request leaves them out (may be async) */ fill?: Record<string, (p: Params, user: U) => unknown>; /** each row on its way out */ row?: (row: Record<string, any>) => Record<string, any>; /** the response, when the front expects something other than the default */ reply?: (result: any, p: Params, user: U, event: H3Event) => unknown; /** the message of a 404 when nothing matched — or its whole `data`, with a `message` */ notFound?: string | Record<string, unknown>; }; /** * An h3 route that is a sentence. * * export default sentenceRoute(app, 'supprime la categorie {id}', { guard: requireAdmin }) * * Parameters come from the route params, the body and the query, in that order * of precedence. Three facts about the request are handed to the runtime * rather than re-read by the model from the sentence: the HTTP method fixes the * operation, an `id` in the URL fixes the row, and a body's fields are written * to the columns they name. See `route` for the template syntax and for how a * sentence is compiled once and reused. */ export declare function sentenceRoute<U = unknown>(app: App, template: string, opts?: RouteOptions<U>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>; /** * A whole API as a table of sentences, served by one catch-all handler. * * // server/api/[...].ts * export default defineRoutes(app, { * 'GET /api/events': 'montre les evenements publies', * 'DELETE /api/admin/events/:id': { say: 'supprime l evenement {id}', guard: 'events' }, * }, { guards: { events: e => requirePerm(e, 'events') } }) * * An entry is a sentence, or the options of `sentenceRoute` plus `say`, with * `guard` naming one of `guards` (or given as a function). `:name` segments are * parameters. Route files the app still has win over the table, so the table * holds what is a sentence and files hold what is code. */ export declare function defineRoutes<U = unknown>(app: App, table: Record<string, string | ((event: H3Event) => unknown) | (RouteOptions<U> & { say: string; guard?: string | RouteOptions<U>['guard']; })>, opts?: { guards?: Record<string, (event: H3Event, params: Record<string, string>) => U | Promise<U>>; defaults?: RouteOptions<U>; /** * Mount as middleware (`server/middleware/`) rather than as a catch-all * route: a request the table does not hold falls through to the app's * files. Nitro answers 405 when a file serves the same path for another * method, which a catch-all route never gets to see. */ passthrough?: boolean; }): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<unknown>>; /** * Files stored by a table's `files` columns, served back: the path is checked to * stay inside `dir`, the type follows the extension, and `guard` may refuse one * (a private photo) by throwing. * * 'GET /uploads/:name': serveFiles('uploads', { guard: (name, event) => ... }) */ export declare function serveFiles(dir: string, opts?: { guard?: (name: string, event: H3Event) => unknown; maxAge?: number; }): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>; /** * A per-client limit on requests, as middleware: `rateLimit({ max: 20, windowMs: 60_000 })`. * In memory, so per process. */ export declare function rateLimit(opts: { max: number; windowMs: number; methods?: string[]; message?: string; }): import("h3").EventHandler<import("h3").EventHandlerRequest, void>; export type FeedItem = { url: string; title?: string; date?: unknown; description?: string; image?: string | null; author?: string | null; category?: string | null; changefreq?: string; priority?: number; }; /** * An RSS feed or a sitemap from rows — the rows a sentence route reads, mapped * to items. `sitemap-news` is Google News' flavour (last two days only). * * 'GET /rss.xml': feed('rss', { title: 'Le Journal', rows: () => latest(), item: a => ({ url: `/articles/${a.slug}`, title: a.title, date: a.published_at }) }) */ export declare function feed(kind: 'rss' | 'sitemap' | 'sitemap-news', o: { rows: (event: H3Event) => Promise<Record<string, any>[]> | Record<string, any>[]; item: (row: Record<string, any>) => FeedItem | null; /** the site's origin; the request's by default */ site?: string | ((event: H3Event) => string); title?: string; description?: string; language?: string; /** pages that are not rows (the home page, static pages), for a sitemap */ extra?: FeedItem[]; maxAge?: number; /** sitemap-news: how many days back (2 by default, 0 for all) */ days?: number; }): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>; /** `show_in_nav` → `showInNav`, for fronts written against an ORM that camelCases. */ export declare const camelRow: (row: Record<string, any>) => { [k: string]: any; }; export {};