feat: core grammar, discovery and renderer; Postgres dialect; conformance suite
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
/**
|
||||
* Every ```http block followed by a ```json block in a docs page is one
|
||||
* conformance case: the request, then the exact body it must answer. A body
|
||||
* with an `error` key must come with a 4xx or 5xx status.
|
||||
*/
|
||||
export function examples(file) {
|
||||
const text = readFileSync(file, 'utf8');
|
||||
const out = [];
|
||||
const re = /```http\n([\s\S]*?)```\s*\n+```json\n([\s\S]*?)```/g;
|
||||
for (let m; (m = re.exec(text));) {
|
||||
const line = text.slice(0, m.index).split('\n').length;
|
||||
const request = m[1].split('\n').map((l) => l.trim()).filter(Boolean).join('');
|
||||
const space = request.indexOf(' ');
|
||||
const target = new URL(request.slice(space + 1), 'http://example');
|
||||
out.push({
|
||||
name: `${file.split('/').pop()}:${line} ${request}`,
|
||||
method: request.slice(0, space),
|
||||
path: target.pathname,
|
||||
query: target.searchParams,
|
||||
expected: JSON.parse(m[2]),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function run(rest, example, assert) {
|
||||
const res = await rest.handle({ method: example.method, path: example.path, query: example.query, headers: {}, context: undefined });
|
||||
assert.deepEqual(res.body, example.expected, `status ${res.status}`);
|
||||
if (example.expected && typeof example.expected === 'object' && 'error' in example.expected) assert.ok(res.status >= 400, `status ${res.status}`);
|
||||
else assert.ok(res.status < 300, `status ${res.status}`);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
DROP SCHEMA IF EXISTS demo CASCADE;
|
||||
CREATE SCHEMA demo;
|
||||
|
||||
CREATE TABLE demo.users (
|
||||
id integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
email text,
|
||||
password_hash text,
|
||||
"limit" integer,
|
||||
manager_id integer REFERENCES demo.users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE demo.assets (
|
||||
id integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
status text NOT NULL,
|
||||
owner_id integer REFERENCES demo.users (id),
|
||||
cost numeric(10, 2),
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE demo.tags (
|
||||
id integer PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE demo.asset_tags (
|
||||
asset_id integer NOT NULL REFERENCES demo.assets (id),
|
||||
tag_id integer NOT NULL REFERENCES demo.tags (id),
|
||||
PRIMARY KEY (asset_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE demo.incidents (
|
||||
id integer PRIMARY KEY,
|
||||
title text NOT NULL,
|
||||
asset_id integer NOT NULL REFERENCES demo.assets (id),
|
||||
severity integer NOT NULL,
|
||||
opened_on date NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO demo.users (id, name, email, password_hash, "limit", manager_id) VALUES
|
||||
(1, 'Ada', 'ada@example.com', 'x', 5, NULL),
|
||||
(2, 'Grace', 'grace@example.com', 'x', NULL, 1),
|
||||
(3, 'Linus', NULL, 'x', 2, 1);
|
||||
|
||||
INSERT INTO demo.assets (id, name, status, owner_id, cost, active, created_at) VALUES
|
||||
(1, 'vpn gateway', 'open', 1, 1200.50, true, '2026-01-02T10:00:00Z'),
|
||||
(2, 'mail relay', 'open', 2, 300.00, true, '2026-01-05T09:30:00Z'),
|
||||
(3, 'old wiki', 'retired', NULL, NULL, false, '2025-06-01T00:00:00Z'),
|
||||
(4, 'payroll db', 'open', 3, 4000.00, true, '2026-02-11T16:45:00Z');
|
||||
|
||||
INSERT INTO demo.tags (id, name) VALUES (1, 'pci'), (2, 'gdpr'), (3, 'internal');
|
||||
|
||||
INSERT INTO demo.asset_tags (asset_id, tag_id) VALUES (1, 1), (1, 3), (2, 2), (4, 1), (4, 2);
|
||||
|
||||
INSERT INTO demo.incidents (id, title, asset_id, severity, opened_on) VALUES
|
||||
(1, 'VPN outage', 1, 3, '2026-01-10'),
|
||||
(2, 'Spoofed mail', 2, 2, '2026-02-01'),
|
||||
(3, 'Brute force', 1, 4, '2026-03-05');
|
||||
@@ -0,0 +1,29 @@
|
||||
import { test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { createREST } from '../packages/core/dist/index.js';
|
||||
import { postgres } from '../packages/postgres/dist/index.js';
|
||||
import { examples, run } from './examples.mjs';
|
||||
|
||||
const require = createRequire(new URL('../packages/postgres/package.json', import.meta.url));
|
||||
const { Pool } = require('pg');
|
||||
const url = process.env.RESTLOOM_PG_URL;
|
||||
const docs = new URL('../docs/', import.meta.url).pathname;
|
||||
const pages = ['query-grammar.md'];
|
||||
|
||||
let pool;
|
||||
let rest;
|
||||
before(async () => {
|
||||
if (!url) return;
|
||||
pool = new Pool({ connectionString: url });
|
||||
await pool.query(readFileSync(new URL('./fixtures/postgres.sql', import.meta.url), 'utf8'));
|
||||
rest = createREST({ db: postgres(pool), expose: { schemas: ['demo'] }, resources: { asset_tags: { expose: false } } });
|
||||
});
|
||||
after(async () => { await pool?.end(); });
|
||||
|
||||
for (const page of pages) {
|
||||
for (const example of examples(docs + page)) {
|
||||
test(`postgres ${example.name}`, { skip: !url && 'RESTLOOM_PG_URL is not set' }, () => run(rest, example, assert));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user