/** * What Naru hands a site's own code (SDK 1.1.0): queries in * `_naru/queries/`, mutations in `_naru/mutations/` and routes in * `_naru/routes/`, run on Naru over the tables `_naru/migrations/*.sql` * create. For type checking only; nothing here exists at run time. * * ```ts * import type { Query, Mutation } from "./functions.d.ts"; * export const posts: Query<{ board: string }> = async (ctx, { board }) => * (await ctx.sql`SELECT id, title FROM posts WHERE board = ${board}`).rows; * ``` * * The code runs in QuickJS, not a browser or Node. Its globals are the * language's own (Object, Array, JSON, Math, Date, RegExp, Map, Set, WeakMap, * WeakSet, WeakRef, Promise, Proxy, Reflect, Symbol, BigInt, Iterator, * ArrayBuffer, DataView, the typed arrays, the Error types, the URI * functions, parseInt and the like) and: URL and URLSearchParams, as * browsers have them; structuredClone, for primitives, plain objects and * arrays, Date, RegExp, Map, Set, ArrayBuffer and Uint8Array (anything else * throws a DataCloneError); Math.random; Date.now, the server's clock (prefer * `ctx.now`; local time is the server's, so use UTC methods); * performance.now, queueMicrotask and DOMException. There is no Intl * (toLocaleString formats as English), TextEncoder, TextDecoder, atob, btoa, * crypto, console, fetch, setTimeout or other timers, WebAssembly, require or * process, whatever your editor's lib types say. * * Written by hand beside the generated naru.d.ts, whose `Json` this repeats * so this file stands on its own. * @packageDocumentation */ export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; /** The failure codes `ctx.fail` takes, as pages receive them in `error.code`. */ export type ErrorCode = | "CONFLICT" | "QUOTA_EXCEEDED" | "AUTH_REQUIRED" | "ACCESS_DENIED" | "NOT_FOUND" | "RATE_LIMITED" | "INVALID_REQUEST" | "UNAVAILABLE"; /** * The member who called: their ID on this site, the same on every visit. * What they may do is the site's to keep, in its own tables. */ export interface CallingMember { readonly id: string; } /** A value site SQL binds or returns (a boolean binds as 1 or 0). */ export type SqlValue = string | number | boolean | Uint8Array | null; /** What `ctx.sql` answers: the statement's rows, and how many it changed. */ export interface SqlResult { rows: Row[]; changes: number; } /** * A query's or route's `ctx`: who calls, and the site's own tables through * `sql`. Anyone may call one, signed in or not, so its code decides what * each caller may see. A query's `sql` only reads. A call runs in one * transaction, for tens of milliseconds in 32 MiB, makes at most 100 * database operations, 1,000 URL parses and 2,000 Math.random() draws, * reads at most 500,000 rows in all, and returns at most 1 MiB of JSON. Every method's promise must be * used (awaited, returned or chained), or the call fails. */ export interface QueryCtx { /** The signed-in member, or null for the owner and visitors. */ readonly member: CallingMember | null; /** The site's owner called. A visitor, signed in to nothing, has neither. */ readonly owner: boolean; /** The site's owner's Naru login, whoever calls: the site's own name. */ readonly ownerName: string; /** The call's time in milliseconds, fixed for the whole call. */ readonly now: number; /** * Refuses the request: nothing the call wrote is kept, and the page's * `error.message` is `message` (1–1000 characters) as written, so write it * for visitors. `details`, at most 4 KiB of JSON, arrives as * `error.details`. */ fail(code: ErrorCode, message: string, details?: Json): never; /** * One statement of SQL against the site's own tables, which its * `_naru/migrations/*.sql` create. As a tag, ``ctx.sql`... ${value}` `` * binds every interpolated value as a parameter; given text and values, * it runs a query builder's compiled statement. SELECT, INSERT, UPDATE and * DELETE run; a query only reads. Naru's own tables, pragmas, triggers, * transactions and recursion are refused, but Naru's view * `_naru_uploads (id, url, member, name, type, size, created_at)`, the * site's finished uploads (`member` null for the owner's), may be read * and joined. A statement is stopped once * SQLite works on one step of it for 3 seconds; one stopped 3 times within * a minute is paused for 5 minutes. It binds at most 100 values, returns at most 1,000 rows and 1 MiB, BLOB columns as * Uint8Arrays, and counts as one database operation; a call's statements * read at most 500,000 rows. A value a UNIQUE column already holds fails * the call with CONFLICT, `details.unique` naming the column; any other * mistake SQLite refuses, with its message. * * To stay fast, read a table whole at most once and reach every other * table by key (join on an indexed column): * * - A list as one value: `WHERE id IN (SELECT value FROM json_each(?))`, * bound as `JSON.stringify(ids)`. * - A row's JSON column: `FROM visits, json_each(visits.hours) j`. * - An ordered aggregate per row: `(SELECT json_group_array(title) FROM * (SELECT title FROM titles WHERE ch = c.id ORDER BY ord))`, with an * index on `titles (ch, ord)`. * - Past 1,000 rows: `WHERE id > ? ORDER BY id LIMIT 1000`, then again * after the last `id`. */ sql>( strings: TemplateStringsArray, ...values: SqlValue[] ): Promise>; sql>( text: string, values?: SqlValue[], ): Promise>; /** * Guests' passwords, kept by the site as hashes. `verify` allows ten wrong * passwords an hour for each `key` (such as "post:42"), whoever tries, * then fails with RATE_LIMITED. A call makes at most 3 password checks. */ passwords: { verify(password: string, hash: string, key: string): Promise; }; } /** A mutation's `ctx`: a query's, writing, and notifying members. */ export interface MutationCtx extends QueryCtx { passwords: QueryCtx["passwords"] & { /** A hash of `password` (1–256 characters) to keep in a table. */ hash(password: string): Promise; }; /** * Notifies these members (at most 1,000 a call) in the Naru app, once the * mutation returns: on their devices if they allowed the site there, or in * the app's inbox until they do. Unknown, pending and banned members are * skipped, and the site is not told who has the app. `path` (default "/") * is a page of this site, opened when the notification is tapped. A title * longer than 100 characters or a body longer than 500 is cut to fit. * Notifications are best effort: one Naru cannot queue is dropped, and the * mutation goes on. Member ids that are not ids, a blank title or another * site's address still throw. */ notify( members: string[], message: { title: string; body?: string; path?: string }, ): Promise; } /** `export async function name(ctx, args)` in `_naru/queries/` or `_naru/mutations/`. */ export type Query = ( ctx: QueryCtx, args: Args, ) => Result | Promise; export type Mutation = ( ctx: MutationCtx, args: Args, ) => Result | Promise;