Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2121d2f246 | ||
|
|
597e6bd109 | ||
|
|
e7637c3704 | ||
|
|
1391c21c1f | ||
|
|
2530941f22 | ||
|
|
667df8325a | ||
|
|
8f2cea5567 | ||
|
|
0e7705bb96 | ||
|
|
67ebb805bf | ||
|
|
540351a3c6 |
@@ -0,0 +1,55 @@
|
||||
# Stages every @crudy/restloom* package to npmjs on version tags
|
||||
# (v<major>.<minor>.<patch>) through npm trusted publishing: GitHub's OIDC
|
||||
# token stands in for an npm token, and npm attaches a provenance statement
|
||||
# naming this repository and commit. A staged version goes live once a
|
||||
# maintainer approves it with 2FA (npm stage approve, or npmjs.com). The
|
||||
# organisation only allows its own actions, so checkout and Node setup are
|
||||
# plain shell. The core goes first: every other package depends on it.
|
||||
name: publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_VERSION: v24.12.0
|
||||
steps:
|
||||
- name: Check out the tag
|
||||
run: |
|
||||
case "$GITHUB_REF_NAME" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "not a version tag: $GITHUB_REF_NAME" >&2; exit 1 ;; esac
|
||||
git init -q .
|
||||
git fetch -q --depth 1 "https://github.com/$GITHUB_REPOSITORY" "refs/tags/$GITHUB_REF_NAME:refs/tags/$GITHUB_REF_NAME"
|
||||
git checkout -q "refs/tags/$GITHUB_REF_NAME"
|
||||
- name: Install Node
|
||||
run: |
|
||||
base="https://nodejs.org/dist/$NODE_VERSION"
|
||||
file="node-$NODE_VERSION-linux-x64.tar.xz"
|
||||
curl -fsSLO "$base/$file"
|
||||
curl -fsSL "$base/SHASUMS256.txt" | grep " $file\$" | sha256sum -c -
|
||||
mkdir -p "$RUNNER_TEMP/node"
|
||||
tar -xJf "$file" -C "$RUNNER_TEMP/node" --strip-components=1
|
||||
rm "$file"
|
||||
echo "$RUNNER_TEMP/node/bin" >> "$GITHUB_PATH"
|
||||
- name: Install npm with staged publishing
|
||||
run: npm install -g npm@11.21.1
|
||||
- run: npm ci --no-audit --no-fund
|
||||
- run: npm test
|
||||
- name: Publish
|
||||
run: |
|
||||
for dir in core postgres mysql mssql sqlite fastify express; do
|
||||
NAME=$(node -p "require('./packages/$dir/package.json').name")
|
||||
VERSION=$(node -p "require('./packages/$dir/package.json').version")
|
||||
[ "v$VERSION" = "$GITHUB_REF_NAME" ] || { echo "tag $GITHUB_REF_NAME does not match $NAME $VERSION" >&2; exit 1; }
|
||||
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
|
||||
echo "$NAME@$VERSION is already published"
|
||||
else
|
||||
(cd "packages/$dir" && npm stage publish)
|
||||
fi
|
||||
done
|
||||
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.0] - 2026-11-03
|
||||
|
||||
### Added
|
||||
- Every database package exports the whole core, so one install and one import are enough: `import { createREST, nodeListener, postgres } from '@crudy/restloom-postgres'`.
|
||||
- `postgres`, `mysql` and `mssql` take a connection URL or the driver's pool options and open the pool themselves; a pool you already have still works.
|
||||
|
||||
### Changed
|
||||
- `pg`, `mysql2` and `mssql` are dependencies of their packages again, installed with them. `better-sqlite3` stays an optional peer of the SQLite package, which also runs on sql.js in a browser.
|
||||
|
||||
### Fixed
|
||||
- A `nodeListener` context that throws answers the request, with the `RestError` it threw or `500 internal`, instead of stopping the process.
|
||||
|
||||
## [0.4.0] - 2026-11-03
|
||||
|
||||
### Added
|
||||
- A top-level `write` option turns writes on for every exposed table at once: `write: true`, or `{ create, update, delete, bulk }`. A resource's own `write` replaces it, and views stay read-only unless their resource allows writes.
|
||||
- Every write example in the docs states its status, and the security page describes sensitive columns and how to serve one.
|
||||
|
||||
## [0.3.1] - 2026-11-03
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -37,19 +37,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 } },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as pg from '../packages/postgres/dist/index.js';
|
||||
import * as my from '../packages/mysql/dist/index.js';
|
||||
import * as ms from '../packages/mssql/dist/index.js';
|
||||
import * as lite from '../packages/sqlite/dist/index.js';
|
||||
|
||||
// One install and one import: each database package brings the core with it,
|
||||
// and the server databases open their own connection from a URL.
|
||||
test('every database package exports the core', () => {
|
||||
for (const mod of [pg, my, ms, lite]) {
|
||||
assert.equal(typeof mod.createREST, 'function');
|
||||
assert.equal(typeof mod.nodeListener, 'function');
|
||||
assert.equal(typeof mod.RestError, 'function');
|
||||
}
|
||||
});
|
||||
|
||||
test('postgres and mysql open a pool from a URL without connecting yet', async () => {
|
||||
assert.equal(pg.postgres('postgres://user:secret@127.0.0.1:1/app').name, 'postgres');
|
||||
assert.equal(my.mysql('mysql://user:secret@127.0.0.1:1/app').name, 'mysql');
|
||||
});
|
||||
|
||||
test('postgres serves from a URL', { skip: !process.env.RESTLOOM_PG_URL && 'RESTLOOM_PG_URL is not set' }, async () => {
|
||||
const db = pg.postgres(process.env.RESTLOOM_PG_URL);
|
||||
const rows = await db.transaction((tx) => tx.query('SELECT 1 AS one', []), { timeoutMs: 5000, readOnly: true });
|
||||
assert.deepEqual(rows, [{ one: 1 }]);
|
||||
});
|
||||
@@ -9,9 +9,9 @@ import { sqlite } from '../packages/sqlite/dist/index.js';
|
||||
// The table in docs/dialects.md is what readers compare databases by, on the
|
||||
// site too. It must say what each dialect's capabilities say.
|
||||
const DIALECTS = {
|
||||
'@crudy/restloom-postgres': postgres({}),
|
||||
'@crudy/restloom-mysql': mysql({}),
|
||||
'@crudy/restloom-mssql': mssql({}),
|
||||
'@crudy/restloom-postgres': postgres({ connect() {} }),
|
||||
'@crudy/restloom-mysql': mysql({ getConnection() {} }),
|
||||
'@crudy/restloom-mssql': mssql({ request() {} }),
|
||||
'@crudy/restloom-sqlite': sqlite({ pragma() {} }),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** The docs pages whose examples are conformance cases, and the options every database runs them with. */
|
||||
export const PAGES = ['query-grammar.md', 'writes.md'];
|
||||
export const PAGES = ['query-grammar.md', 'writes.md', 'security.md'];
|
||||
|
||||
const PROCEDURES = {
|
||||
'todos/{todo}/complete': { function: 'demo.complete_todo' },
|
||||
|
||||
+59
-3
@@ -3,20 +3,43 @@
|
||||
The core answers a plain request object, so it fits any framework:
|
||||
|
||||
```ts
|
||||
import { createREST, postgres } from '@crudy/restloom-postgres';
|
||||
|
||||
const rest = createREST({
|
||||
db: postgres(process.env.DATABASE_URL),
|
||||
expose: { schemas: ['app'] },
|
||||
});
|
||||
|
||||
const response = await rest.handle({ method, path, query, headers, body, context });
|
||||
// { status, headers, body }
|
||||
```
|
||||
|
||||
The examples below use this `rest`.
|
||||
|
||||
`handle` never throws: every failure is a response. `path` is the path below where the API is mounted (`/todos/4`), `query` a `URLSearchParams`, `body` the parsed JSON.
|
||||
|
||||
## Node
|
||||
## Node, without a framework
|
||||
|
||||
`nodeListener` from the core serves `node:http` with no other dependency. It reads JSON bodies up to 1 MiB (`maxBodyBytes`).
|
||||
`nodeListener` from the core serves `node:http` with nothing else installed. It mounts the API under `prefix` and answers `404 not_found` outside it, reads JSON bodies up to 1 MiB (`maxBodyBytes`), and builds each request's context with `context`. A `context` that throws a `RestError` refuses the request with it; any other error answers `500 internal`:
|
||||
|
||||
```ts
|
||||
http.createServer(nodeListener(rest, { prefix: '/api', context: (req) => userFrom(req) })).listen(3000);
|
||||
import http from 'node:http';
|
||||
import { nodeListener } from '@crudy/restloom-postgres';
|
||||
|
||||
const api = nodeListener(rest, { prefix: '/api', context: (req) => userFrom(req) });
|
||||
http.createServer(api).listen(3000);
|
||||
```
|
||||
|
||||
To serve other pages from the same server, hand only the API's paths to it:
|
||||
|
||||
```ts
|
||||
import http from 'node:http';
|
||||
|
||||
http.createServer((req, res) => (req.url?.startsWith('/api/') ? api(req, res) : site(req, res))).listen(3000);
|
||||
```
|
||||
|
||||
Without HTTP at all, call `rest.handle` directly: [From your own code](#from-your-own-code).
|
||||
|
||||
## Fastify
|
||||
|
||||
```sh
|
||||
@@ -24,8 +47,10 @@ npm install @crudy/restloom-fastify
|
||||
```
|
||||
|
||||
```ts
|
||||
import Fastify from 'fastify';
|
||||
import { restloom } from '@crudy/restloom-fastify';
|
||||
|
||||
const app = Fastify();
|
||||
await app.register(restloom(rest, { context: (req) => req.user }), { prefix: '/api' });
|
||||
```
|
||||
|
||||
@@ -38,11 +63,42 @@ npm install @crudy/restloom-express
|
||||
```
|
||||
|
||||
```ts
|
||||
import express from 'express';
|
||||
import { restloom } from '@crudy/restloom-express';
|
||||
|
||||
const app = express();
|
||||
app.use('/api', express.json(), restloom(rest, { context: (req) => req.user }));
|
||||
```
|
||||
|
||||
## From your own code
|
||||
|
||||
Your backend never needs to call its own API over HTTP. `rest.handle` runs a request in the same process, through the same checks, hooks and row security as a client's, and gives the answer back as data:
|
||||
|
||||
```ts
|
||||
const { status, body: todos } = await rest.handle({
|
||||
method: 'GET',
|
||||
path: '/todos',
|
||||
query: new URLSearchParams('select=id,name,owner(name)&status=eq.open'),
|
||||
headers: {},
|
||||
context: currentUser,
|
||||
});
|
||||
```
|
||||
|
||||
A write passes its body as an object, and header names in lower case:
|
||||
|
||||
```ts
|
||||
const { status, body } = await rest.handle({
|
||||
method: 'POST',
|
||||
path: '/tags',
|
||||
query: new URLSearchParams('select=id,name'),
|
||||
headers: { prefer: 'return=representation' },
|
||||
body: { id: 8, name: 'garden' },
|
||||
context: currentUser,
|
||||
});
|
||||
```
|
||||
|
||||
A call from your code is not trusted more than a client's: `authorize` and `scope` receive its `context` like any other. Pass the user you act for, or a context your hooks recognise as a background job. Check `status` rather than catching: `handle` never throws, and an error answers its code in `body.error`.
|
||||
|
||||
## Your own routes
|
||||
|
||||
A host that registers every route itself, to attach its own permissions or documentation to each, calls `rest.handle` from those routes. That is how crudy mounts restloom: each declared route checks its permission, then hands the request to restloom.
|
||||
|
||||
+10
-6
@@ -9,6 +9,8 @@ A dialect is the package that knows one database: how to read its catalog, how t
|
||||
| `@crudy/restloom-mssql` | SQL Server 2022+ | `JSON_OBJECT`, `STRING_AGG` | `OUTPUT` | no | security policies | scalar and table functions |
|
||||
| `@crudy/restloom-sqlite` | SQLite 3.44+ (better-sqlite3) | `json_object`, `json_group_array` | `RETURNING` | no | no | none |
|
||||
|
||||
The PostgreSQL, MySQL and SQL Server packages bring their driver and open their own connection from a URL. To tune it, pass the driver's pool options instead, or a pool you already have: a `pg` Pool, a `mysql2/promise` Pool or a connected `mssql` ConnectionPool. The SQLite package takes a database object, so it runs on `better-sqlite3` in Node or on sql.js in a browser; install the one you use.
|
||||
|
||||
Every dialect passes the same conformance suite, so a request answers the same rows on each. Regular expressions (`match`, `imatch`) need Postgres or MySQL, and array filters (`ov`, `cs`, `cd`) Postgres array columns; elsewhere they answer `unsupported`. Where else the dialects differ, the sections below say how.
|
||||
|
||||
## Postgres
|
||||
@@ -16,7 +18,7 @@ Every dialect passes the same conformance suite, so a request answers the same r
|
||||
```ts
|
||||
import { postgres } from '@crudy/restloom-postgres';
|
||||
|
||||
const db = postgres(pool);
|
||||
const db = postgres(process.env.DATABASE_URL);
|
||||
```
|
||||
|
||||
- **Transactions:** every request is one transaction, `READ ONLY` for reads, with `SET LOCAL statement_timeout` from `limits.timeoutMs`.
|
||||
@@ -27,10 +29,9 @@ const db = postgres(pool);
|
||||
## MySQL
|
||||
|
||||
```ts
|
||||
import { createPool } from 'mysql2/promise';
|
||||
import { mysql } from '@crudy/restloom-mysql';
|
||||
|
||||
const db = mysql(createPool({ uri: process.env.DATABASE_URL }));
|
||||
const db = mysql(process.env.DATABASE_URL);
|
||||
```
|
||||
|
||||
- **Schemas:** MySQL's schemas are its databases: `expose: { schemas: ['shop'] }`.
|
||||
@@ -43,10 +44,9 @@ const db = mysql(createPool({ uri: process.env.DATABASE_URL }));
|
||||
## SQL Server
|
||||
|
||||
```ts
|
||||
import sql from 'mssql';
|
||||
import { mssql } from '@crudy/restloom-mssql';
|
||||
|
||||
const db = mssql(await new sql.ConnectionPool(process.env.DATABASE_URL).connect());
|
||||
const db = mssql(process.env.DATABASE_URL);
|
||||
```
|
||||
|
||||
- **Version:** SQL Server 2022 or later, for `JSON_OBJECT`.
|
||||
@@ -58,6 +58,10 @@ const db = mssql(await new sql.ConnectionPool(process.env.DATABASE_URL).connect(
|
||||
|
||||
## SQLite
|
||||
|
||||
```sh
|
||||
npm install @crudy/restloom-sqlite better-sqlite3
|
||||
```
|
||||
|
||||
```ts
|
||||
import Database from 'better-sqlite3';
|
||||
import { sqlite } from '@crudy/restloom-sqlite';
|
||||
@@ -84,7 +88,7 @@ class Renderer extends PostgresRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
const db = postgres(pool, { renderer: new Renderer() });
|
||||
const db = postgres(process.env.DATABASE_URL, { renderer: new Renderer() });
|
||||
```
|
||||
|
||||
The pieces are listed in [writing a dialect](./writing-a-dialect.md).
|
||||
|
||||
+15
-13
@@ -2,41 +2,43 @@
|
||||
|
||||
## Install
|
||||
|
||||
Install the core, the package for your database, and its driver:
|
||||
Install the package for your database. It brings the core and the database driver with it, except for SQLite, which also needs `better-sqlite3`:
|
||||
|
||||
```sh
|
||||
npm install @crudy/restloom @crudy/restloom-postgres pg
|
||||
npm install @crudy/restloom-postgres
|
||||
```
|
||||
|
||||
| Database | Package | Driver |
|
||||
|---|---|---|
|
||||
| PostgreSQL 13+ | `@crudy/restloom-postgres` | `pg` |
|
||||
| MySQL 8.0+ | `@crudy/restloom-mysql` | `mysql2` |
|
||||
| SQL Server 2022+ | `@crudy/restloom-mssql` | `mssql` |
|
||||
| SQLite 3.44+ | `@crudy/restloom-sqlite` | `better-sqlite3` |
|
||||
| Database | Package |
|
||||
|---|---|
|
||||
| PostgreSQL 13+ | `@crudy/restloom-postgres` |
|
||||
| MySQL 8.0+ | `@crudy/restloom-mysql` |
|
||||
| SQL Server 2022+ | `@crudy/restloom-mssql` |
|
||||
| SQLite 3.44+ | `@crudy/restloom-sqlite` |
|
||||
|
||||
Each of them exports the whole core, so everything is imported from that one package. The examples in these pages use `@crudy/restloom-postgres`; with another database, import from its package instead.
|
||||
|
||||
## Serve a schema
|
||||
|
||||
```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({ connectionString: process.env.DATABASE_URL })),
|
||||
db: postgres(process.env.DATABASE_URL),
|
||||
expose: { schemas: ['app'] },
|
||||
});
|
||||
|
||||
http.createServer(nodeListener(rest, { prefix: '/api' })).listen(3000);
|
||||
```
|
||||
|
||||
Every table and view of `app` is now a resource, read-only:
|
||||
`nodeListener` comes with the core, so no framework is needed. Every table and view of `app` is now a resource, read-only:
|
||||
|
||||
```sh
|
||||
curl -g 'http://localhost:3000/api/todos?select=id,name,owner(name)&status=eq.open'
|
||||
```
|
||||
|
||||
Add `write: true` beside `expose` to create, update and delete rows in every table too. Do it once your `authorize` hook decides who may write: [Writes](./writes.md#writes-on-every-table).
|
||||
|
||||
## Next
|
||||
|
||||
- [Discovery and overrides](./discovery.md): what becomes a resource, which columns are hidden, how relations are named.
|
||||
|
||||
+44
-2
@@ -5,6 +5,7 @@ restloom decides what can be asked. Your backend decides who may ask it, through
|
||||
## What restloom enforces
|
||||
|
||||
- **Exposure:** only the schemas and tables you name are served, and only their visible columns.
|
||||
- **Sensitive columns:** a column whose name looks like a secret is hidden without being asked, see below.
|
||||
- **Bound values:** every value is a bound parameter, checked against its column's type before any SQL runs. Names come from the catalog and are quoted; nothing from a request is written into SQL.
|
||||
- **Bounded requests:** every request is limited:
|
||||
|
||||
@@ -21,11 +22,52 @@ A request over a limit is refused, never quietly cut down.
|
||||
|
||||
- **Errors:** they answer stable codes. A database message never reaches the client. Unexpected errors answer `internal` and reach your `onError` hook.
|
||||
|
||||
## Sensitive columns
|
||||
|
||||
restloom reads every column name when it reads the catalog. A name made of a word such as `password`, `hash`, `salt`, `secret`, `token`, `api_key`, `private_key`, `otp`, `mfa` or `recovery`, alone or between underscores, marks the column as sensitive: `password_hash` and `reset_token` are hidden, `tokenizer` is not. The full list is in [Columns](./discovery.md#columns).
|
||||
|
||||
A hidden column is not served at all. It cannot be read, filtered, sorted or written, `select=*` leaves it out, and naming it answers as a column that does not exist, so a client cannot even learn it is there:
|
||||
|
||||
```http
|
||||
GET /users?select=name,password_hash
|
||||
```
|
||||
|
||||
Answer: 400
|
||||
|
||||
```json
|
||||
{ "error": "unknown_column", "message": "password_hash" }
|
||||
```
|
||||
|
||||
The check runs on names, so it guards against the column nobody thought about when the table grew. It does not know what a column holds: hide a sensitive column with a harmless name yourself, with `hide`.
|
||||
|
||||
To serve a column the check hides on purpose, name it in `show`. It then behaves as any other column, and is writable only if `write` lists it:
|
||||
|
||||
```ts
|
||||
resources: {
|
||||
files: { show: ['sha256_hash'] },
|
||||
users: { hide: ['phone'] },
|
||||
}
|
||||
```
|
||||
|
||||
`sensitive` replaces the whole pattern, for every resource. Start from `DEFAULT_SENSITIVE` to add words rather than lose the defaults:
|
||||
|
||||
```ts
|
||||
import { createREST, DEFAULT_SENSITIVE } from '@crudy/restloom-postgres';
|
||||
|
||||
createREST({
|
||||
db, // from a database package, see Dialects
|
||||
expose: { schemas: ['app'] },
|
||||
sensitive: new RegExp(`${DEFAULT_SENSITIVE.source}|(^|_)(iban|ssn)(_|$)`, 'i'),
|
||||
});
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
```ts
|
||||
import { createREST, RestError } from '@crudy/restloom-postgres';
|
||||
|
||||
createREST({
|
||||
db,
|
||||
db, // from a database package, see Dialects
|
||||
expose: { schemas: ['app'] },
|
||||
hooks: {
|
||||
authorize(request, target) { /* throw a RestError to refuse */ },
|
||||
@@ -47,7 +89,7 @@ createREST({
|
||||
|
||||
## Row security
|
||||
|
||||
With Postgres, row security is the strongest perimeter: the database itself hides the rows a session may not see. Connect as a role without `BYPASSRLS`, and set what your policies read in the `transaction` hook:
|
||||
Row security needs PostgreSQL, or SQL Server with security policies (see [Dialects](./dialects.md#sql-server)). With Postgres, it is the strongest perimeter: the database itself hides the rows a session may not see. Connect as a role without `BYPASSRLS`, and set what your policies read in the `transaction` hook:
|
||||
|
||||
```ts
|
||||
async transaction(tx, context) {
|
||||
|
||||
+45
-1
@@ -1,6 +1,6 @@
|
||||
# Writes and procedures
|
||||
|
||||
Writes are off until a resource enables them. The examples on this page run with these options, against the [demo schema](../conformance/fixtures/postgres.sql):
|
||||
Writes are off until you turn them on, for every table at once or one resource at a time. The examples on this page run with these options, against the [demo schema](../conformance/fixtures/postgres.sql):
|
||||
|
||||
```ts
|
||||
resources: {
|
||||
@@ -23,6 +23,28 @@ procedures: {
|
||||
| DELETE | `/tags/4` | deletes one row | 204, or 200 with the row |
|
||||
| PATCH, DELETE | `/steps?priority=lt.3` | by filter, only where `bulk: true` | 204, or 200 with the rows |
|
||||
|
||||
## Writes on every table
|
||||
|
||||
For an API that writes everywhere, set `write` once, beside `expose`:
|
||||
|
||||
```ts
|
||||
import { createREST } from '@crudy/restloom-postgres';
|
||||
|
||||
createREST({
|
||||
db, // from a database package, see Dialects
|
||||
expose: { schemas: ['app'] },
|
||||
write: true,
|
||||
resources: {
|
||||
audit_log: { write: false },
|
||||
users: { write: { update: ['name'] } },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
It applies to every exposed table; views stay read-only until their resource allows writes, since many views cannot be written. A resource that sets its own `write` uses it instead: `write: false` keeps one table read-only, a column list narrows another. `write: { create: true, update: true, delete: true, bulk: true }` adds updates and deletes by filter everywhere.
|
||||
|
||||
What is never writable stays so: hidden and sensitive columns, generated columns, and keys on update; a bulk request still needs a filter. Who may write is decided by your `authorize` hook, which receives every write with its action: see [Security](./security.md#hooks).
|
||||
|
||||
## Getting the rows back
|
||||
|
||||
Without a `Prefer` header a write answers with no body. `Prefer: return=representation` answers the written rows, read back with the request's `select`, so embeds work too.
|
||||
@@ -56,6 +78,8 @@ POST /tags
|
||||
[{ "id": 7, "name": "a" }, { "name": "b" }]
|
||||
```
|
||||
|
||||
Answer: 400
|
||||
|
||||
```json
|
||||
{ "error": "mixed_columns" }
|
||||
```
|
||||
@@ -68,6 +92,8 @@ POST /todos
|
||||
{ "name": "book dentist", "status": "open", "pinned": false }
|
||||
```
|
||||
|
||||
Answer: 400
|
||||
|
||||
```json
|
||||
{ "error": "not_writable", "message": "pinned" }
|
||||
```
|
||||
@@ -101,6 +127,8 @@ A row still referenced by another is not deleted:
|
||||
DELETE /tags/3
|
||||
```
|
||||
|
||||
Answer: 409
|
||||
|
||||
```json
|
||||
{ "error": "still_referenced" }
|
||||
```
|
||||
@@ -109,6 +137,8 @@ DELETE /tags/3
|
||||
DELETE /todos/1
|
||||
```
|
||||
|
||||
Answer: 405
|
||||
|
||||
```json
|
||||
{ "error": "method_not_allowed" }
|
||||
```
|
||||
@@ -130,6 +160,8 @@ Answer: 200
|
||||
DELETE /steps
|
||||
```
|
||||
|
||||
Answer: 400
|
||||
|
||||
```json
|
||||
{ "error": "filter_required" }
|
||||
```
|
||||
@@ -144,6 +176,8 @@ POST /tags
|
||||
{ "id": 1, "name": "duplicate key" }
|
||||
```
|
||||
|
||||
Answer: 409
|
||||
|
||||
```json
|
||||
{ "error": "duplicate" }
|
||||
```
|
||||
@@ -154,12 +188,16 @@ POST /steps
|
||||
{ "id": 9, "title": "Call the clinic", "todo_id": 99, "priority": 1, "due_on": "2026-04-01" }
|
||||
```
|
||||
|
||||
Answer: 409
|
||||
|
||||
```json
|
||||
{ "error": "reference_missing" }
|
||||
```
|
||||
|
||||
## Procedures
|
||||
|
||||
PostgreSQL, MySQL and SQL Server run procedures; SQLite has no functions to call. A MySQL function returns a single value, and a SQL Server function cannot change data.
|
||||
|
||||
A procedure calls a database function at a path you choose. A `{name}` in the path passes that segment as the argument of the same name; the body's keys pass the others. Arguments are checked by name and type, and one with a default may be left out.
|
||||
|
||||
A procedure is a POST unless declared `method: 'GET'`. A GET procedure takes its arguments from the query string (`/search?q=milk&limit=10`), each once, and runs in a read-only transaction: use it for a function without side effects.
|
||||
@@ -170,6 +208,8 @@ POST /todos/3/complete
|
||||
{ "note": "bought a new one" }
|
||||
```
|
||||
|
||||
Answer: 200
|
||||
|
||||
```json
|
||||
3
|
||||
```
|
||||
@@ -180,6 +220,8 @@ A function returning a set answers an array of its rows. File taxes belongs to A
|
||||
POST /tags/home/todos
|
||||
```
|
||||
|
||||
Answer: 200
|
||||
|
||||
```json
|
||||
[
|
||||
{ "id": 2, "name": "file taxes", "status": "open", "owner_id": 1, "estimate": 1, "pinned": true, "created_at": "2026-01-05T09:30:00+00:00" },
|
||||
@@ -193,6 +235,8 @@ POST /todos/3/complete
|
||||
{ "why": "too busy" }
|
||||
```
|
||||
|
||||
Answer: 400
|
||||
|
||||
```json
|
||||
{ "error": "unknown_argument", "message": "why" }
|
||||
```
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
A dialect implements one interface:
|
||||
|
||||
```ts
|
||||
import type { Catalog, RestError, SqlRenderer, Tx } from '@crudy/restloom';
|
||||
|
||||
interface Dialect {
|
||||
name: string;
|
||||
capabilities: { returning: boolean; estimatedCount: boolean; rowSecurity: boolean; procedures: boolean };
|
||||
|
||||
Generated
+16
-136
@@ -22,7 +22,6 @@
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.9.0.tgz",
|
||||
"integrity": "sha512-6933vNLqh06RR7rumnrq3UZIeBtRLeBHDPMrieDPIljiCaMMwt0nRw++nd3TOxiohtasNtm29rC+b/9ZVKCOqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
@@ -40,7 +39,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz",
|
||||
"integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -53,7 +51,6 @@
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz",
|
||||
"integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
@@ -68,7 +65,6 @@
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.1.tgz",
|
||||
"integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
@@ -87,7 +83,6 @@
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
|
||||
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -103,7 +98,6 @@
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.7.0.tgz",
|
||||
"integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -116,7 +110,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz",
|
||||
"integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -126,7 +119,6 @@
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz",
|
||||
"integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
@@ -145,7 +137,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz",
|
||||
"integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -158,7 +149,6 @@
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz",
|
||||
"integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
@@ -173,7 +163,6 @@
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.1.tgz",
|
||||
"integrity": "sha512-HHuuFrmN1uuF9D5A6aAo5eHzNFNX4Q+P4SksZqDKse5YaJFFy/zDLG3wXHEV2FVGgetZ3QM8jGURNBtnQYf1NQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -197,7 +186,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.1.tgz",
|
||||
"integrity": "sha512-SxpomJRTBfDTQnuii3w10WnZNQJGPNi15tRPdPUQz7BqyB3yk976knEueS7R0H9kSLtXbP0wJZXRh0EbRTQ0lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure-rest/core-client": "^2.3.3",
|
||||
@@ -217,7 +205,6 @@
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.2.tgz",
|
||||
"integrity": "sha512-VmUSLbXRAbSzDD8grXHGPaknYs0SKr3yuf6U+d4XMpX4XuVYskNqbTTwXce0zR1LyxfTZm9rWEBcvs3vdYwCmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure-rest/core-client": "^2.3.3",
|
||||
@@ -240,7 +227,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz",
|
||||
"integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typespec/ts-http-runtime": "^0.3.0",
|
||||
@@ -254,7 +240,6 @@
|
||||
"version": "5.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.27.0.tgz",
|
||||
"integrity": "sha512-i2CKKyuCuOZEBZJiAoFK/viYZAP6CRaqqlkeHl3ViteDHOUE/tkI/Slq1cdGnwhkIB+k1OE0g00QGl9O0gAFNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "16.15.0"
|
||||
@@ -267,7 +252,6 @@
|
||||
"version": "16.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.15.0.tgz",
|
||||
"integrity": "sha512-lwdLb1B9Q5nEhPLVIKe78XH5wFMN15fCiDkY7ylPDSWZuWcD93G3jpJajrKhO3CQld8SLKz0sHiOi24mxdvtsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
@@ -277,7 +261,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-7.1.2.tgz",
|
||||
"integrity": "sha512-52hKQLGpEQzwNufIXG1SPVU+wQ3RIe7Rp2+g+VdAmZojCAZ6yCdGEffzQEQ6RnBZGBEs40nl14rWBmfJpCoHsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "16.15.0",
|
||||
@@ -453,7 +436,6 @@
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.3.2.tgz",
|
||||
"integrity": "sha512-BVsYu84Gut8YTNR1v5/PnifmqXv4SVcfA/hVIM/mXADZfIocip/DfNppBtv+4am5hGH1XuH6FSoU8OHkBvZMeA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
@@ -467,7 +449,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-1.1.0.tgz",
|
||||
"integrity": "sha512-z9ZBWEG+8pIB5V1zYzlRPXx0oRJ5H7coPnMQK8EZOw03UTPI9Umn6viL36f5w+CuqkKsnCM50RVStpjZmR0Bng==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
@@ -549,7 +530,6 @@
|
||||
"version": "24.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz",
|
||||
"integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
@@ -585,7 +565,6 @@
|
||||
"version": "4.0.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.25.tgz",
|
||||
"integrity": "sha512-7GsxL8Wd5X3jh73/qgW7WCnbBvGSdD8U3E4Jiuus3/8I+PN20DtlkJtwFhr/sImXfDDQ2Z03i/9wNiIF3TVSsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -616,7 +595,6 @@
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz",
|
||||
"integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -631,7 +609,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
@@ -665,7 +642,6 @@
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -758,7 +734,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
@@ -768,7 +743,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -876,14 +850,12 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz",
|
||||
"integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
@@ -947,7 +919,6 @@
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
@@ -1005,7 +976,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1049,7 +1019,6 @@
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz",
|
||||
"integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
@@ -1066,7 +1035,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -1079,7 +1047,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -1137,7 +1104,6 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
@@ -1228,7 +1194,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -1238,7 +1203,6 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
@@ -1493,7 +1457,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
@@ -1609,7 +1572,6 @@
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
@@ -1623,7 +1585,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
@@ -1637,7 +1598,6 @@
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -1654,7 +1614,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1675,7 +1634,6 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
@@ -1699,7 +1657,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
@@ -1715,7 +1672,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
|
||||
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -1728,7 +1684,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^3.0.0"
|
||||
@@ -1754,14 +1709,12 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
|
||||
"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-inside-container": "^1.0.0"
|
||||
@@ -1777,7 +1730,6 @@
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
|
||||
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-ref-resolver": {
|
||||
@@ -1811,7 +1763,6 @@
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
@@ -1834,7 +1785,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
@@ -1846,7 +1796,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
@@ -1894,63 +1843,54 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz",
|
||||
"integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
@@ -2060,14 +2000,12 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mssql": {
|
||||
"version": "12.7.10",
|
||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.10.tgz",
|
||||
"integrity": "sha512-vTmKBsogfFhd8dMhFjwn2QiPZuUP2/zpqt19L8eEJpQFwRa19bR+n3pFBjRtGdz+YfuC5jgj4YixILSAAF3ovQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tediousjs/connection-string": "^1.0.0",
|
||||
@@ -2087,7 +2025,6 @@
|
||||
"version": "3.24.6",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.6.tgz",
|
||||
"integrity": "sha512-enu73KhbAua2L3dIxjc6qg6ZJ7H3pt1BImZASWNWY5xSsmJ1ZuUPULTDGXgmJbI61vb+nJAG19IZclIq19UidQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
@@ -2109,7 +2046,6 @@
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
@@ -2129,7 +2065,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
|
||||
"integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
@@ -2216,7 +2151,6 @@
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz",
|
||||
"integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.5.1",
|
||||
@@ -2258,7 +2192,6 @@
|
||||
"version": "8.23.1",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
|
||||
"integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.14.1",
|
||||
@@ -2286,7 +2219,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz",
|
||||
"integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@@ -2294,14 +2226,12 @@
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz",
|
||||
"integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
@@ -2311,7 +2241,6 @@
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
@@ -2321,14 +2250,12 @@
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz",
|
||||
"integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
@@ -2345,7 +2272,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
@@ -2395,7 +2321,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -2405,7 +2330,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -2415,7 +2339,6 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -2425,7 +2348,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
@@ -2438,7 +2360,6 @@
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
|
||||
"integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -2479,7 +2400,6 @@
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
@@ -2692,7 +2612,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -2705,7 +2624,6 @@
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2759,7 +2677,6 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
@@ -2783,7 +2700,6 @@
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -2990,7 +2906,6 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
@@ -3000,7 +2915,6 @@
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz",
|
||||
"integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
@@ -3026,7 +2940,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
@@ -3076,7 +2989,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.1.2.tgz",
|
||||
"integrity": "sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -3086,7 +2998,6 @@
|
||||
"version": "20.3.8",
|
||||
"resolved": "https://registry.npmjs.org/tedious/-/tedious-20.3.8.tgz",
|
||||
"integrity": "sha512-ZrJiwyGn340Sa0V0g37Xqj5bvmJ1riYwvEtg7zdm/lpw70f2ZNIQ2fvHmntU5fEOorWDa48Jo1oKYAuUP+rh7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/core-auth": "^1.10.1",
|
||||
@@ -3107,7 +3018,6 @@
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
@@ -3120,7 +3030,6 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3145,7 +3054,6 @@
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz",
|
||||
"integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
@@ -3195,7 +3103,6 @@
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
|
||||
"integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
@@ -3248,7 +3155,6 @@
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@@ -3289,7 +3195,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz",
|
||||
"integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-wsl": "^3.1.0",
|
||||
@@ -3306,7 +3211,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
@@ -3314,7 +3218,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@crudy/restloom",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
@@ -3326,10 +3230,10 @@
|
||||
},
|
||||
"packages/express": {
|
||||
"name": "@crudy/restloom-express",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
@@ -3346,10 +3250,10 @@
|
||||
},
|
||||
"packages/fastify": {
|
||||
"name": "@crudy/restloom-fastify",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
@@ -3365,84 +3269,60 @@
|
||||
},
|
||||
"packages/mssql": {
|
||||
"name": "@crudy/restloom-mssql",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0",
|
||||
"mssql": "^12.7.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mssql": "^12.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"mssql": "12.7.10",
|
||||
"typescript": "^5.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mssql": "^12.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mssql": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/mysql": {
|
||||
"name": "@crudy/restloom-mysql",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0",
|
||||
"mysql2": "^3.24.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"mysql2": "^3.24.5",
|
||||
"typescript": "^5.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mysql2": "^3.11.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mysql2": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/postgres": {
|
||||
"name": "@crudy/restloom-postgres",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0",
|
||||
"pg": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pg": "^8.11.0",
|
||||
"pg": "^8.16.0",
|
||||
"typescript": "^5.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg": "^8.11.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/sqlite": {
|
||||
"name": "@crudy/restloom-sqlite",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom",
|
||||
"version": "0.3.1",
|
||||
"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"],
|
||||
@@ -12,6 +12,7 @@
|
||||
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
|
||||
},
|
||||
"files": ["dist", "LICENSE", "README.md"],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": { "node": ">=20" },
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom-express",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"description": "Express adapter for restloom",
|
||||
"keywords": [
|
||||
@@ -28,6 +28,7 @@
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -36,7 +37,7 @@
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^5.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom-fastify",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"description": "Fastify adapter for restloom",
|
||||
"keywords": [
|
||||
@@ -28,6 +28,7 @@
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -36,7 +37,7 @@
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"fastify": "^5.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom-mssql",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"description": "SQL Server dialect for restloom",
|
||||
"keywords": [
|
||||
@@ -29,6 +29,7 @@
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -37,18 +38,12 @@
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mssql": "^12.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mssql": { "optional": true }
|
||||
"@crudy/restloom": "^0.5.0",
|
||||
"mssql": "^12.7.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/mssql": "^12.0.0",
|
||||
"mssql": "12.7.10",
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { RestError, type Dialect, type SqlRenderer } from '@crudy/restloom';
|
||||
import mssqlDriver, { type ConnectionPool } from 'mssql';
|
||||
import mssqlDriver, { type ConnectionPool, type config as MssqlConfig } from 'mssql';
|
||||
import { introspect } from './introspect.js';
|
||||
import { MssqlRenderer } from './renderer.js';
|
||||
|
||||
export * from '@crudy/restloom';
|
||||
|
||||
/** A connected mssql ConnectionPool, or what opens one: a connection string or config. */
|
||||
export type MssqlConnection = ConnectionPool | MssqlConfig | string;
|
||||
|
||||
function poolOf(c: MssqlConnection): Promise<ConnectionPool> {
|
||||
if (typeof c !== 'string' && typeof (c as ConnectionPool).request === 'function') return Promise.resolve(c as ConnectionPool);
|
||||
const ready = new mssqlDriver.ConnectionPool(c as MssqlConfig).connect();
|
||||
// A failed connection surfaces on the first request, not as an unhandled rejection at startup.
|
||||
ready.catch(() => undefined);
|
||||
return ready;
|
||||
}
|
||||
|
||||
export { MssqlRenderer } from './renderer.js';
|
||||
|
||||
const CODES: Record<number, [number, string]> = {
|
||||
@@ -37,20 +50,21 @@ export interface MssqlOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* The SQL Server 2022 dialect over an mssql ConnectionPool. Each request is one
|
||||
* The SQL Server 2022 dialect over an mssql ConnectionPool, given or opened. Each request is one
|
||||
* transaction; the statement timeout is the pool's `requestTimeout`, since the
|
||||
* driver sets it per pool. Row security is SQL Server's security policies,
|
||||
* which a `transaction` hook feeds through sp_set_session_context.
|
||||
*/
|
||||
export function mssql(pool: ConnectionPool, options: MssqlOptions = {}): Dialect {
|
||||
export function mssql(connection: MssqlConnection, options: MssqlOptions = {}): Dialect {
|
||||
const ready = poolOf(connection);
|
||||
return {
|
||||
name: 'mssql',
|
||||
capabilities: { returning: true, estimatedCount: false, rowSecurity: true, procedures: true },
|
||||
renderer: options.renderer ?? new MssqlRenderer(),
|
||||
decode: (value) => (typeof value === 'string' ? JSON.parse(value) : value),
|
||||
introspect: (scope) => introspect(pool, scope),
|
||||
introspect: async (scope) => introspect(await ready, scope),
|
||||
async transaction(fn) {
|
||||
const tx = new mssqlDriver.Transaction(pool);
|
||||
const tx = new mssqlDriver.Transaction(await ready);
|
||||
await tx.begin();
|
||||
try {
|
||||
const out = await fn({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom-mysql",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"description": "MySQL dialect for restloom",
|
||||
"keywords": [
|
||||
@@ -28,6 +28,7 @@
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -36,17 +37,11 @@
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mysql2": "^3.11.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mysql2": { "optional": true }
|
||||
"@crudy/restloom": "^0.5.0",
|
||||
"mysql2": "^3.24.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"mysql2": "^3.24.5",
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { RestError, type Dialect, type SqlRenderer } from '@crudy/restloom';
|
||||
import type { Pool } from 'mysql2/promise';
|
||||
import { createPool, type Pool, type PoolOptions } from 'mysql2/promise';
|
||||
import { introspect } from './introspect.js';
|
||||
import { MysqlRenderer } from './renderer.js';
|
||||
|
||||
export * from '@crudy/restloom';
|
||||
|
||||
/** A mysql2/promise Pool, or what opens one: a connection URL or pool options. */
|
||||
export type MysqlConnection = Pool | PoolOptions | string;
|
||||
|
||||
const poolOf = (c: MysqlConnection): Pool =>
|
||||
typeof c === 'string' ? createPool({ uri: c }) : typeof (c as Pool).getConnection === 'function' ? (c as Pool) : createPool(c as PoolOptions);
|
||||
|
||||
export { MysqlRenderer } from './renderer.js';
|
||||
|
||||
const CODES: Record<number, [number, string]> = {
|
||||
@@ -31,12 +39,13 @@ export interface MysqlOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* The MySQL 8 dialect over a mysql2/promise Pool. Each request is one
|
||||
* The MySQL 8 dialect over a mysql2/promise Pool, given or opened. Each request is one
|
||||
* transaction on one connection, in UTC, with MAX_EXECUTION_TIME for reads.
|
||||
* Text compares with each column's collation, so with a case-insensitive
|
||||
* collation `eq` ignores case.
|
||||
*/
|
||||
export function mysql(pool: Pool, options: MysqlOptions = {}): Dialect {
|
||||
export function mysql(connection: MysqlConnection, options: MysqlOptions = {}): Dialect {
|
||||
const pool = poolOf(connection);
|
||||
return {
|
||||
name: 'mysql',
|
||||
capabilities: { returning: false, estimatedCount: false, rowSecurity: false, procedures: true },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom-postgres",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"description": "Postgres dialect for restloom",
|
||||
"keywords": ["restloom", "postgres", "postgresql", "rest", "api"],
|
||||
@@ -12,24 +12,19 @@
|
||||
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
|
||||
},
|
||||
"files": ["dist", "LICENSE", "README.md"],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": { "node": ">=20" },
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg": "^8.11.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg": { "optional": true }
|
||||
"@crudy/restloom": "^0.5.0",
|
||||
"pg": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pg": "^8.11.0",
|
||||
"pg": "^8.16.0",
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { SqlRenderer, type Dialect, type Tx } from '@crudy/restloom';
|
||||
import type { Pool } from 'pg';
|
||||
import pg, { type Pool, type PoolConfig } from 'pg';
|
||||
import { mapError } from './errors.js';
|
||||
import { introspect } from './introspect.js';
|
||||
|
||||
export * from '@crudy/restloom';
|
||||
export { mapError } from './errors.js';
|
||||
|
||||
/** A pg Pool, or what opens one: a connection string or a pool config. */
|
||||
export type PostgresConnection = Pool | PoolConfig | string;
|
||||
|
||||
const poolOf = (c: PostgresConnection): Pool =>
|
||||
typeof c === 'string' ? new pg.Pool({ connectionString: c }) : typeof (c as Pool).connect === 'function' ? (c as Pool) : new pg.Pool(c as PoolConfig);
|
||||
|
||||
/** Postgres writes every primitive the core's defaults assume; subclass it to change one. */
|
||||
export class PostgresRenderer extends SqlRenderer {}
|
||||
|
||||
@@ -13,11 +20,13 @@ export interface PostgresOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* The Postgres dialect over a `pg` Pool. Every request runs in one transaction
|
||||
* The Postgres dialect over a `pg` Pool, given or opened from a connection
|
||||
* string or config. Every request runs in one transaction
|
||||
* with a local statement timeout, read-only for reads, so a `transaction` hook
|
||||
* can set the role and session settings that row security reads.
|
||||
*/
|
||||
export function postgres(pool: Pool, options: PostgresOptions = {}): Dialect {
|
||||
export function postgres(connection: PostgresConnection, options: PostgresOptions = {}): Dialect {
|
||||
const pool = poolOf(connection);
|
||||
return {
|
||||
name: 'postgres',
|
||||
capabilities: { returning: true, estimatedCount: true, rowSecurity: true, procedures: true },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@crudy/restloom-sqlite",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"description": "SQLite dialect for restloom",
|
||||
"keywords": [
|
||||
@@ -28,6 +28,7 @@
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": { "access": "public" },
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -36,7 +37,7 @@
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crudy/restloom": "^0.3.0"
|
||||
"@crudy/restloom": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"better-sqlite3": ">=12"
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { Database } from 'better-sqlite3';
|
||||
import { introspect } from './introspect.js';
|
||||
import { SqliteRenderer } from './renderer.js';
|
||||
|
||||
export * from '@crudy/restloom';
|
||||
|
||||
export { SqliteRenderer } from './renderer.js';
|
||||
|
||||
const CODES: Record<string, [number, string]> = {
|
||||
@@ -27,7 +29,8 @@ export interface SqliteOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* The SQLite dialect over a better-sqlite3 Database. It turns on foreign keys
|
||||
* The SQLite dialect over a better-sqlite3 Database, or any object with its
|
||||
* API, such as sql.js in a browser. It turns on foreign keys
|
||||
* and case-sensitive LIKE for the connection. One connection runs one
|
||||
* transaction at a time, so requests are queued. SQLite cannot stop a running
|
||||
* statement, so `limits.timeoutMs` does not apply, and it has no functions to
|
||||
|
||||
Reference in New Issue
Block a user