Add article history by SKU and SSE update signals for live clients.

Expose getHistoryBySku as GET /article/:sku/history, and document mutation-emitted company signals via GET /events.
This commit is contained in:
seb
2026-08-05 09:54:56 +02:00
parent 4a9403bfd1
commit 782c49a680
28 changed files with 757 additions and 2 deletions

View File

@@ -30,6 +30,7 @@ const options = {
tags: [
{ name: 'Auth', description: 'Google SSO and session' },
{ name: 'Health', description: 'Connectivity checks' },
{ name: 'Events', description: 'SSE update signals for live clients' },
{ name: 'Article', description: 'Articles under categories' },
{ name: 'Category', description: 'Category tree and membership' },
{ name: 'Company', description: 'Company CRUD' },
@@ -211,6 +212,16 @@ function renderOperation(apiPath, method, operation) {
if (bodyLines) lines.push(...bodyLines);
}
const signals = operation['x-signals'];
if (Array.isArray(signals) && signals.length) {
lines.push('## Signals');
lines.push('');
lines.push(
`On success, emits SSE signal${signals.length === 1 ? '' : 's'}: ${signals.map((s) => `\`${s}\``).join(', ')} (see [\`GET /events\`](../events.md)).`,
);
lines.push('');
}
const responses = operation.responses || {};
const successCodes = Object.keys(responses)
.filter((c) => c.startsWith('2'))
@@ -305,6 +316,35 @@ function renderReadme(operations) {
lines.push('');
lines.push('Machine-readable spec: [`openapi.json`](openapi.json).');
lines.push('');
lines.push('## Signals (SSE)');
lines.push('');
lines.push(
'Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). After a successful mutation, the API emits a named SSE event (signal) to subscribers of the same selected company.',
);
lines.push('');
lines.push('| Signal | Emitted by |');
lines.push('|--------|------------|');
/** @type {Map<string, string[]>} */
const bySignal = new Map();
for (const op of operations) {
const signals = spec.paths[op.path]?.[op.method]?.['x-signals'];
if (!Array.isArray(signals)) continue;
const label = `\`${op.method.toUpperCase()} ${op.path}\``;
for (const signal of signals) {
if (!bySignal.has(signal)) bySignal.set(signal, []);
bySignal.get(signal).push(label);
}
}
const signalNames = [...bySignal.keys()].sort();
if (signalNames.length === 0) {
lines.push('| — | _(none yet)_ |');
} else {
for (const signal of signalNames) {
lines.push(`| \`${signal}\` | ${bySignal.get(signal).join(', ')} |`);
}
}
lines.push('');
lines.push('## DESIGN');
lines.push('');
lines.push('- for every route we have a file in /src/api/routes');