feat: a top-level write option turns writes on for every table; 0.4.0

This commit is contained in:
2026-11-03 06:46:04 +01:00
parent 540351a3c6
commit 67ebb805bf
15 changed files with 91 additions and 31 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom",
"version": "0.3.1",
"version": "0.4.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"],
+1 -1
View File
@@ -2,7 +2,7 @@ export { createREST, type Rest } from './rest.js';
export { nodeListener, type NodeListenerOptions } from './node.js';
export { RestError, badRequest, notFound } from './errors.js';
export { DEFAULT_LIMITS, DEFAULT_SENSITIVE } from './options.js';
export type { Hooks, Limits, ProcedureOptions, RelationOptions, ResourceOptions, RestOptions, RestRequest, RestResponse, WriteOptions } from './options.js';
export type { DefaultWriteOptions, Hooks, Limits, ProcedureOptions, RelationOptions, ResourceOptions, RestOptions, RestRequest, RestResponse, WriteOptions } from './options.js';
export type { Capabilities, Dialect, Tx } from './dialect.js';
export type { Catalog, CatalogColumn, CatalogForeignKey, CatalogFunction, CatalogTable, LogicalType } from './catalog.js';
export type { Column, Junction, Relation, Resource } from './resources.js';
+13
View File
@@ -34,6 +34,14 @@ export interface WriteOptions {
bulk?: boolean;
}
/** What a global `write` allows on each table: whole methods, since a column list belongs to one resource. */
export interface DefaultWriteOptions {
create?: boolean;
update?: boolean;
delete?: boolean;
bulk?: boolean;
}
export interface ResourceOptions {
/** false removes a discovered table or view from the API. */
expose?: boolean;
@@ -92,6 +100,11 @@ export interface RestOptions<C = unknown> {
db: Dialect;
/** Nothing is served until a schema or a table is named here. */
expose: { schemas?: string[]; tables?: string[] };
/**
* Writes on every exposed table, unless its resource sets its own `write`.
* Views stay read-only until their resource allows writes.
*/
write?: boolean | DefaultWriteOptions;
resources?: Record<string, ResourceOptions>;
procedures?: Record<string, ProcedureOptions>;
/** Replaces the default list of sensitive name patterns. */
+3 -2
View File
@@ -92,8 +92,9 @@ export function buildResources(catalog: Catalog, options: RestOptions<unknown>):
const resources = new Map<string, Resource>();
for (const t of tables) {
const name = named.get(t)!;
const o = config[name] ?? {};
if (o.expose === false) continue;
const own = config[name] ?? {};
if (own.expose === false) continue;
const o = own.write === undefined && t.kind === 'table' ? { ...own, write: options.write } : own;
const key = o.key ?? (t.primaryKey.length ? t.primaryKey : t.columns.some((c) => c.name === 'id') ? ['id'] : []);
const w = o.write === true ? { delete: true, bulk: false } : o.write || {};
const columns = columnsOf(t, o, sensitive);
+19 -1
View File
@@ -1,6 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { resources } from './fixture.mjs';
import { buildResources } from '../dist/resources.js';
import { catalog, resources } from './fixture.mjs';
test('only exposed schemas become resources', () => {
const r = resources();
@@ -40,6 +41,23 @@ test('writes are off by default and never reach generated or key columns', () =>
assert.equal(a.write.delete, true);
});
test('a global write applies to every table that does not set its own', () => {
const r = resources({ write: { create: true, update: true, delete: true }, resources: { tags: { write: false }, users: { write: { update: ['name'] } } } });
assert.deepEqual(r.get('todos').write, { create: true, update: true, delete: true, bulk: false });
assert.equal(r.get('todos').columns.get('id').update, false);
assert.deepEqual(r.get('tags').write, { create: false, update: false, delete: false, bulk: false });
assert.deepEqual(r.get('users').write, { create: false, update: true, delete: false, bulk: false });
assert.equal(r.get('users').columns.has('password_hash'), false);
});
test('a global write leaves views read-only', () => {
const c = catalog();
c.tables.push({ ...c.tables.find((t) => t.name === 'tags'), name: 'tag_view', kind: 'view', primaryKey: [] });
const r = buildResources(c, { db: null, expose: { schemas: ['demo'] }, write: true });
assert.equal(r.get('tag_view').write.create, false);
assert.equal(r.get('tags').write.create, true);
});
test('options naming unknown resources or columns fail at load', () => {
assert.throws(() => resources({ resources: { nope: {} } }), /no exposed table/);
assert.throws(() => resources({ resources: { users: { hide: ['nope'] } } }), /no column nope/);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom-express",
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"description": "Express adapter for restloom",
"keywords": [
@@ -36,7 +36,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.3.0"
"@crudy/restloom": "^0.4.0"
},
"peerDependencies": {
"express": "^5.0.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom-fastify",
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"description": "Fastify adapter for restloom",
"keywords": [
@@ -36,7 +36,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.3.0"
"@crudy/restloom": "^0.4.0"
},
"peerDependencies": {
"fastify": "^5.0.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom-mssql",
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"description": "SQL Server dialect for restloom",
"keywords": [
@@ -37,7 +37,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.3.0"
"@crudy/restloom": "^0.4.0"
},
"peerDependencies": {
"mssql": "^12.0.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom-mysql",
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"description": "MySQL dialect for restloom",
"keywords": [
@@ -36,7 +36,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.3.0"
"@crudy/restloom": "^0.4.0"
},
"peerDependencies": {
"mysql2": "^3.11.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom-postgres",
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"description": "Postgres dialect for restloom",
"keywords": ["restloom", "postgres", "postgresql", "rest", "api"],
@@ -18,7 +18,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.3.0"
"@crudy/restloom": "^0.4.0"
},
"peerDependencies": {
"pg": "^8.11.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@crudy/restloom-sqlite",
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"description": "SQLite dialect for restloom",
"keywords": [
@@ -36,7 +36,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@crudy/restloom": "^0.3.0"
"@crudy/restloom": "^0.4.0"
},
"peerDependencies": {
"better-sqlite3": ">=12"