rhizome/src/http-api.ts

222 lines
6.0 KiB
TypeScript
Raw Normal View History

import Debug from "debug";
2024-12-23 17:29:38 -06:00
import express from "express";
import {readdirSync, readFileSync, watch} from "fs";
import path, {join} from "path";
import {Converter} from "showdown";
import {Collection} from "./collection";
2024-12-23 17:29:38 -06:00
import {HTTP_API_ADDR, HTTP_API_PORT} from "./config";
import {deltasAccepted} from "./deltas";
import {peers} from "./peers";
import {Delta} from "./types";
const debug = Debug('http-api');
type CollectionsToServe = {
[key: string]: Collection;
};
const docConverter = new Converter({
simpleLineBreaks: true,
completeHTMLDocument: true
});
const htmlDocFromMarkdown = (md: string): string => docConverter.makeHtml(md);
type mdFileInfo = {
name: string,
md: string,
html: string
};
class MDFiles {
files = new Map<string, mdFileInfo>();
readme?: mdFileInfo;
readFile(name: string) {
const md = readFileSync(join('./markdown', `${name}.md`)).toString();
const html = htmlDocFromMarkdown(md);
this.files.set(name, {name, md, html});
}
readReadme() {
const md = readFileSync('./README.md').toString();
const html = htmlDocFromMarkdown(md);
this.readme = {name: 'README', md, html};
}
getReadmeHTML() {
return this.readme?.html;
}
getHtml(name: string): string | undefined {
return this.files.get(name)?.html;
}
list(): string[] {
return Array.from(this.files.keys());
}
readDir() {
// Read list of markdown files from directory and
// render each markdown file as html
readdirSync('./markdown/')
.filter((f) => f.endsWith('.md'))
.map((name) => path.parse(name).name)
.forEach((name) => this.readFile(name));
}
watchDir() {
watch('./markdown', null, (eventType, filename) => {
if (!filename) return;
if (!filename.endsWith(".md")) return;
const name = path.parse(filename).name;
switch (eventType) {
case 'rename': {
debug(`file ${name} renamed`);
// Remove it from memory and re-scan everything
this.files.delete(name);
this.readDir();
break;
}
case 'change': {
debug(`file ${name} changed`);
// Re-read this file
this.readFile(name)
break;
}
}
});
}
watchReadme() {
watch('./README.md', null, (eventType, filename) => {
if (!filename) return;
switch (eventType) {
case 'change': {
debug(`README file changed`);
// Re-read this file
this.readReadme()
break;
}
}
});
}
}
2024-12-23 17:29:38 -06:00
export function runHttpApi(collections?: CollectionsToServe) {
const app = express();
app.use(express.json());
// Get list of markdown files
const mdFiles = new MDFiles();
mdFiles.readDir();
mdFiles.readReadme();
mdFiles.watchDir();
mdFiles.watchReadme();
// Serve README
app.get('/html/README', (_req: express.Request, res: express.Response) => {
const html = mdFiles.getReadmeHTML();
res.setHeader('content-type', 'text/html').send(html);
});
2024-12-23 17:29:38 -06:00
// Serve markdown files as html
app.get('/html/:name', (req: express.Request, res: express.Response) => {
let html = mdFiles.getHtml(req.params.name);
if (!html) {
res.status(404);
html = htmlDocFromMarkdown('# 404\n\n## [Index](/html)');
}
res.setHeader('content-type', 'text/html');
res.send(html);
});
2024-12-23 17:29:38 -06:00
// Serve index
{
2024-12-23 17:29:38 -06:00
let md = `# Files\n\n`;
md += `[README](/html/README)\n\n`;
for (const name of mdFiles.list()) {
2024-12-23 17:29:38 -06:00
md += `- [${name}](./${name})\n`;
}
const html = htmlDocFromMarkdown(md);
app.get('/html', (_req: express.Request, res: express.Response) => {
2024-12-23 17:29:38 -06:00
res.setHeader('content-type', 'text/html').send(html);
});
}
// Set up API routes
if (collections) {
for (const [name, collection] of Object.entries(collections)) {
debug(`collection: ${name}`);
// Get the ID of all domain entities
2024-12-23 17:29:38 -06:00
app.get(`/${name}/ids`, (_req: express.Request, res: express.Response) => {
res.json({ids: collection.getIds()});
});
// Add a new domain entity
// TODO: schema validation
2024-12-23 17:29:38 -06:00
app.put(`/${name}`, (req: express.Request, res: express.Response) => {
const {body: properties} = req;
const ent = collection.put(undefined, properties);
res.json(ent);
});
// Update a domain entity
2024-12-23 17:29:38 -06:00
app.put(`/${name}/:id`, (req: express.Request, res: express.Response) => {
const {body: properties, params: {id}} = req;
if (properties.id && properties.id !== id) {
res.status(400).json({error: "ID Mismatch", param: id, property: properties.id});
return;
}
const ent = collection.put(id, properties);
res.json(ent);
});
}
}
app.get("/deltas", (_req: express.Request, res: express.Response) => {
// TODO: streaming
res.json(deltasAccepted);
});
// Get the number of deltas ingested by this node
2024-12-23 17:29:38 -06:00
app.get("/deltas/count", (_req: express.Request, res: express.Response) => {
res.json(deltasAccepted.length);
});
// Get the list of peers seen by this node (including itself)
2024-12-23 17:29:38 -06:00
app.get("/peers", (_req: express.Request, res: express.Response) => {
res.json(peers.map(({reqAddr, publishAddr, isSelf, isSeedPeer}) => {
const deltasAcceptedCount = deltasAccepted
.filter((delta: Delta) => {
return delta.receivedFrom?.addr == reqAddr.addr &&
delta.receivedFrom?.port == reqAddr.port;
})
.length;
const peerInfo = {
reqAddr: reqAddr.toAddrString(),
publishAddr: publishAddr?.toAddrString(),
isSelf,
isSeedPeer,
deltaCount: {
accepted: deltasAcceptedCount
}
};
return peerInfo;
}));
});
// Get the number of peers seen by this node (including itself)
2024-12-23 17:29:38 -06:00
app.get("/peers/count", (_req: express.Request, res: express.Response) => {
res.json(peers.length);
});
app.listen(HTTP_API_PORT, HTTP_API_ADDR, () => {
debug(`HTTP API bound to http://${HTTP_API_ADDR}:${HTTP_API_PORT}`);
});
}