feat: one install and one import per database; server dialects open their pool from a URL; nodeListener answers a throwing context; 0.5.0

This commit is contained in:
2026-11-03 13:06:04 +01:00
parent 1391c21c1f
commit e7637c3704
24 changed files with 194 additions and 227 deletions
+5 -5
View File
@@ -32,19 +32,19 @@ Your backend stays in charge:
| `@crudy/restloom-express` | Express handler |
```sh
npm install @crudy/restloom @crudy/restloom-postgres pg
npm install @crudy/restloom-postgres
```
A database package brings the core and its driver, and exports the whole core.
## Use
```ts
import http from 'node:http';
import pg from 'pg';
import { createREST, nodeListener } from '@crudy/restloom';
import { postgres } from '@crudy/restloom-postgres';
import { createREST, nodeListener, postgres } from '@crudy/restloom-postgres';
const rest = createREST({
db: postgres(new pg.Pool()),
db: postgres(process.env.DATABASE_URL),
expose: { schemas: ['app'] },
resources: { todos: { write: true } },
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom",
"version": "0.4.0",
"version": "0.5.0",
"type": "module",
"description": "A REST API generated from your database's catalog: discovery, a curl-friendly query grammar, relations of every kind, safe defaults",
"keywords": ["rest", "api", "postgrest", "sql", "database", "crud", "generator"],
+10 -2
View File
@@ -1,9 +1,10 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import { RestError } from './errors.js';
import type { RestResponse } from './options.js';
import type { Rest } from './rest.js';
export interface NodeListenerOptions<C> {
/** Builds the context hooks receive, e.g. the authenticated user. */
/** Builds the context hooks receive, e.g. the authenticated user. Throw a RestError to refuse the request. */
context?(req: IncomingMessage): C | Promise<C>;
/** Path the API is mounted under, e.g. /api. */
prefix?: string;
@@ -47,6 +48,13 @@ export function nodeListener<C>(rest: Rest<C>, options: NodeListenerOptions<C> =
const status = (err as { status?: number }).status ?? 400;
return send(res, { status, headers: {}, body: { error: status === 413 ? 'body_too_large' : 'bad_body' } });
}
let context: C;
try {
context = (await options.context?.(req)) as C;
} catch (err) {
const refusal = err instanceof RestError ? err : new RestError(500, 'internal');
return send(res, { status: refusal.status, headers: {}, body: { error: refusal.code, ...(refusal.detail ? { message: refusal.detail } : {}) } });
}
const headers = Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v]));
send(res, await rest.handle({
method: req.method ?? 'GET',
@@ -54,7 +62,7 @@ export function nodeListener<C>(rest: Rest<C>, options: NodeListenerOptions<C> =
query: url.searchParams,
headers,
body,
context: (await options.context?.(req)) as C,
context,
}));
};
}
+24 -1
View File
@@ -1,7 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { createREST, nodeListener } from '../dist/index.js';
import { createREST, nodeListener, RestError } from '../dist/index.js';
import { fakeDialect } from './fake-dialect.mjs';
test('the node listener serves under its prefix and refuses bad bodies', async () => {
@@ -24,3 +24,26 @@ test('Prefer: count=exact counts as count=exact does', async () => {
const res = await rest.handle({ method: 'GET', path: '/items', query: new URLSearchParams(), headers: { prefer: 'count=exact' }, context: undefined });
assert.equal(res.headers['content-range'], '0-0/1');
});
test('a context that throws answers the request instead of stopping the process', async () => {
const rest = createREST({ db: fakeDialect(), expose: { schemas: ['app'] } });
const server = http.createServer(nodeListener(rest, {
context(req) {
if (req.headers.authorization === 'bad') throw new RestError(401, 'unauthenticated');
if (req.headers.authorization === 'broken') throw new Error('database down');
return {};
},
})).listen(0);
const base = `http://127.0.0.1:${server.address().port}`;
try {
const refused = await fetch(`${base}/items`, { headers: { authorization: 'bad' } });
assert.equal(refused.status, 401);
assert.deepEqual(await refused.json(), { error: 'unauthenticated' });
const broken = await fetch(`${base}/items`, { headers: { authorization: 'broken' } });
assert.equal(broken.status, 500);
assert.deepEqual(await broken.json(), { error: 'internal' });
assert.equal((await fetch(`${base}/items`)).status, 200);
} finally {
server.close();
}
});