feat: Prefer count header, procedures outside exposed schemas; 0.2.0

This commit is contained in:
2026-11-02 01:50:34 +01:00
parent 31f97fd392
commit 8735abfe79
18 changed files with 73 additions and 38 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom",
"version": "0.1.0",
"version": "0.2.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"],
+5 -2
View File
@@ -23,8 +23,11 @@ export interface Dialect {
readonly name: string;
readonly capabilities: Capabilities;
readonly renderer: SqlRenderer;
/** Reads the tables, views and functions of the given schemas and tables. */
introspect(scope: { schemas: string[]; tables: string[] }): Promise<Catalog>;
/**
* Reads the tables and views of the given schemas and tables, and the
* functions of the given schemas plus the named ones (schema.name).
*/
introspect(scope: { schemas: string[]; tables: string[]; functions: string[] }): Promise<Catalog>;
/** Runs fn in one transaction with a statement timeout; read-only when asked. */
transaction<T>(fn: (tx: Tx) => Promise<T>, options: { timeoutMs: number; readOnly: boolean }): Promise<T>;
/** The JSON value of a statement's `r` column as a JavaScript value: drivers that parse JSON return it as is, others parse text. */
+2
View File
@@ -30,6 +30,8 @@ export async function readRows(db: Dialect, tx: Tx, resource: Resource, request:
scope: (r: Resource) => Filter[], id?: string, extra: Filter[] = []): Promise<RestResponse> {
const plan = parseRead(resource, request.query, limits);
plan.where.push(...extra);
const prefer = /\bcount=(exact|estimated)\b/.exec(request.headers.prefer ?? '');
if (plan.count === 'none' && prefer) plan.count = prefer[1] as 'exact' | 'estimated';
if (id !== undefined) {
plan.where.unshift(keyFilter(resource, id));
plan.limit = 1;
+2 -1
View File
@@ -43,7 +43,8 @@ export function createREST<C = unknown>(options: RestOptions<C>): Rest<C> {
const db = options.db;
let loading: Promise<Api> | null = null;
const load = () => (loading ??= db.introspect({ schemas: options.expose.schemas ?? [], tables: options.expose.tables ?? [] })
const functions = Object.values(options.procedures ?? {}).map((p) => p.function).filter((f) => f.includes('.'));
const load = () => (loading ??= db.introspect({ schemas: options.expose.schemas ?? [], tables: options.expose.tables ?? [], functions })
.then((catalog: Catalog) => ({
resources: buildResources(catalog, options as RestOptions<unknown>),
procedures: buildProcedures(catalog, options.procedures ?? {}),
+6
View File
@@ -18,3 +18,9 @@ test('the node listener serves under its prefix and refuses bad bodies', async (
server.close();
}
});
test('Prefer: count=exact counts as count=exact does', async () => {
const rest = createREST({ db: fakeDialect(), expose: { schemas: ['app'] } });
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');
});