feat: writes, procedures, Fastify and Express adapters, documentation

This commit is contained in:
2026-11-02 01:25:49 +01:00
parent 4b276f21e3
commit 6801f257fb
35 changed files with 3109 additions and 182 deletions
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@crudy/restloom-express",
"version": "0.1.0",
"type": "module",
"description": "Express adapter for restloom",
"keywords": [
"restloom",
"express",
"rest",
"api"
],
"author": "Marc Villain",
"homepage": "https://restloom.crudy.fr",
"repository": {
"type": "git",
"url": "git+https://github.com/crudylabs/restloom.git",
"directory": "packages/express"
},
"license": "Apache-2.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"LICENSE",
"README.md"
],
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsc -p .",
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.1.0"
},
"peerDependencies": {
"express": "^5.0.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^5.9.0",
"express": "^5.0.0",
"@types/express": "^5.0.0"
}
}
+33
View File
@@ -0,0 +1,33 @@
import type { Rest } from '@crudy/restloom';
import type { NextFunction, Request, Response } from 'express';
export interface RestloomExpressOptions<C> {
/** Builds the context hooks receive, e.g. the authenticated user. */
context?(req: Request): C | Promise<C>;
}
/**
* An Express handler answering every path it is mounted on with restloom.
* Mount it after a JSON body parser: `app.use('/api', express.json(), restloom(rest))`.
*/
export function restloom<C>(rest: Rest<C>, options: RestloomExpressOptions<C> = {}) {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const url = new URL(req.url, 'http://localhost');
const headers = Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v]));
const out = await rest.handle({
method: req.method,
path: url.pathname || '/',
query: url.searchParams,
headers,
body: req.body,
context: (await options.context?.(req)) as C,
});
res.status(out.status).set(out.headers);
if (out.body === undefined) res.end();
else res.json(out.body);
} catch (err) {
next(err);
}
};
}
+27
View File
@@ -0,0 +1,27 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { createREST } from '@crudy/restloom';
import { restloom } from '../dist/index.js';
import { fakeDialect } from '../../core/test/fake-dialect.mjs';
const require = createRequire(import.meta.url);
const express = require('express');
test('serves restloom where it is mounted', async () => {
const rest = createREST({ db: fakeDialect(), expose: { schemas: ['app'] } });
const app = express();
app.use('/api', express.json(), restloom(rest));
const server = app.listen(0);
const port = server.address().port;
try {
const res = await fetch(`http://127.0.0.1:${port}/api/items?select=id`);
assert.equal(res.status, 200);
assert.deepEqual(await res.json(), [{ id: 1, name: 'one' }]);
assert.equal(res.headers.get('content-range'), '0-0/*');
const write = await fetch(`http://127.0.0.1:${port}/api/items`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"name":"x"}' });
assert.equal(write.status, 405);
} finally {
server.close();
}
});
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src"]
}