@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
TypeScript
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 {};