Context
ctx: everything a handler, a hook and a middleware get about their request.
.("/teas/:id", { : .({ : .() }) }, () => {
.("cache-control", "max-age=60");
return findTea(..);
});Every handler gets the context of its request. It can be taken apart,
({ params, header }) => …, and every part of it still works: the functions on it need no
this. Nothing on it is made before it is asked for, so what a handler does not use costs
nothing.
params, query, headers, body
.("/teas/:shop", { : .({ : .() }), : NewTea }, ({ , }) =>
store.add(., ),
);The input, checked and typed by the route's schemas. Without a schema they are the request's own values, as text.
rawHeaders
({ }) => ["sec-fetch-site"]The request's headers as they arrived, names in lower case, even where a route's header
schema cut ctx.headers down to its contract. For hooks and plugins that must see a header
no route lists. It is the request's own object, not a copy: read it, do not change it. New
in 0.7.0.
method, path, url, httpVersion
({ , , , }) => `${} ${} on ${.host} over HTTP/${}`The request line. url is a URL, made only when it is read; its scheme is
protocol. httpVersion is "1.1", "2.0" or "1.0", and "1.1"
where the platform does not say (app.fetch). httpVersion is new in 0.8.0.
secure, protocol
({ , }) => ("sid", id, { })
({ , }) => `${protocol}://${headers.host}/teas`Whether the request came over TLS, and "https" or "http" accordingly: the TLS socket
under listen, the URL's scheme under app.fetch, secure on an adapter's request. With
trustProxy, what the trusted proxies forward in
x-forwarded-proto or forwarded's proto=. New in 0.7.0.
id
({ }) => logger.info({ }, "tea added")The request id: the one in x-request-id when it looks safe, a fresh one otherwise. It goes
back out as a header and sits in every log line and in every problem document, so a bug
report points at the right line.
ip
({ }) => ({ : })The client's address, as the socket or the platform says it. Behind a proxy that is the
proxy, unless the app says trustProxy: then it is the client's
address from x-forwarded-for or forwarded, believed only as far as the proxies you
named.
cookies
({ }) => sessions.get(.sid)The request's cookies, by name, decoded. Read on first use.
signal
.("/report", { : 600_000 }, ({ }) => db.query(REPORT, { }));An AbortSignal that aborts when the client goes away before the answer is out, or when the
route's timeout runs out. Hand it to the database driver or to fetch, and work nobody
waits for any more stops.
state
.(async (, ) => {
.. = await userFrom(..authorization);
await ();
});Free space for middleware to hand things on to the handler. For values that are the same on every request, a decoration is cheaper.
import { } from "@vxnsin/inkan";
export const = <>("tenant");
.(() => .(, lookUp()));
.("/me", () => .()?.name); // typed: Tenant | undefinedstateKey<T>(name) is a typed slot in ctx.state, under a symbol of its own: two plugins
that pick the same name never meet, no string key reaches it, and ctx.state stays a plain
record for everyone else. Make it once, at the top of a module, and export it to whoever
reads it. Experimental, new in 0.8.0.
route
({ }) => metrics.count(?.path) // "/teas/:id", not "/teas/7"The route that matched, as it was written: its method, its path, the security it asks
for (its own or its group's, empty for none) and its meta. One
object per route, made before the first request, so hooks can read it for free.
req, res
Node's request and response, when the request came through a socket. Writing to res
yourself is allowed; inkan then leaves the answer alone.
status()
({ }) => {
(202);
return { : true };
};The status a plain value answers with.
header()
({ }) => {
("cache-control", "no-store");
return data;
};A header on the answer.
reply()
.("/teas", { : NewTea, : { 201: Tea } }, ({ , }) => {
const = store.add();
return (201, , { : `/teas/${.id}` });
});Another status, with extra headers if needed. It is typed to the statuses the contract
lists, so reply(418, …) on a route that never promised a teapot does not compile.
return (200, body, { "set-cookie": ["a=1; Path=/", "b=2; Path=/; HttpOnly"] });A list for set-cookie goes out as a line per entry, next to the cookies
setCookie() made, so a proxy can pass an upstream's cookies on as
they came. A list for any other header is joined with ", ". The headers' type is exported
as ReplyHeaders. New in 0.7.0.
text()
({ }) => ("pong")Plain text. The status is the one status() set, or the usual one, unless given:
text("gone", 410).
html()
import { } from "@vxnsin/inkan";
.("/tea/:name", ({ , : }) => (`<h1>${.}</h1>`));
.("/hi/:name", ({ }) => `<p>Hello, ${.}</p>`); // the same, returned as it isHTML. The html tag escapes every value put into it, so what a user typed cannot become a
script on the page. Lists are joined, and null, undefined and false write nothing:
`<ul>${teas.map(() => `<li>${.name}</li>`)}</ul>${admin && `<a href="/admin">admin</a>`}`raw(markup) lets markup you made yourself through unescaped. Never use it for anything a
user sent.
render()
.((, { }) => `<!doctype html><title>${}</title><main>${}</main>`);
.("/", ({ }) => (`<p>Tea, checked.</p>`, { : "Home" }));HTML in the layout of the route's scope, with the props handed to the layout. A plugin can have a layout of its own; the closest one wins. Without a layout, the content goes out as it is.
redirect()
({ }) => ("/new") // 302
({ }) => redirect("/new", 301) // or 303, 307, 308Sends the client elsewhere. A location with characters a header cannot carry is percent-encoded first.
notFound()
({ , }) => findTea(.id) ?? (`No tea ${.id}`)Ends the request with a 404 problem, the same one an unknown route gets, through
onProblem like any other. To end with another problem, throw one:
throw problem(409, "taken", "That name is taken").
setCookie(), clearCookie()
({ }) => {
("sid", session.id, { : 60 * 60 * 24, : true });
return { : true };
};
({ }) => ("sid");A cookie on the answer; every cookie goes out as a Set-Cookie of its own.
| option | default | |
|---|---|---|
maxAge, expires | until the browser closes | seconds, or a date; maxAge: 0 deletes it |
path | / | |
domain | ||
secure | false | only over HTTPS; turn it on behind TLS |
httpOnly | true | out of reach of the page's scripts |
sameSite | "Lax" | "Strict", "Lax" or "None", which needs secure |
partitioned |
csv()
.("/export", ({ , }) =>
(rowsFromDb(), { : "teas.csv", : ";", : true }),
);Rows as a CSV file, written as they come: an array, or a generator over a database cursor, so a million rows never sit in memory at once.
| option | default | |
|---|---|---|
columns | the keys of the first row | a list of keys, or key → heading |
separator | , | ; for a German Excel |
bom | false | true so Excel reads the file as UTF-8 |
filename | offered for download under this name | |
guardFormulas | true | a cell that starts with =, +, - or @ gets a ', so a spreadsheet shows it instead of running it |
Decorations
.("db", openDatabase());
.("/teas", ({ }) => .teas.all());What decorate() puts on the context of a scope is there too,
typed.