Express API server wrapping existing store/graph core with REST endpoints for nodes, edges, graph, and search. React + Vite portal with React Flow for interactive graph visualization, Tailwind CSS styling, and full CRUD UI (sidebar, node panel, add/link modals, search bar, toast notifications).
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { addNode, getNode, listNodes, updateNode, removeNode, addEdge, removeEdge, query } from '../core/store';
|
|
import { getConnections, buildTree } from '../core/graph';
|
|
import { getDb } from '../core/db';
|
|
|
|
const router = Router();
|
|
|
|
function param(req: Request, name: string): string {
|
|
const v = req.params[name];
|
|
return Array.isArray(v) ? v[0] : v;
|
|
}
|
|
|
|
// List nodes
|
|
router.get('/nodes', (req: Request, res: Response) => {
|
|
try {
|
|
const options: any = {};
|
|
if (req.query.kind) options.kind = req.query.kind as string;
|
|
if (req.query.status) options.status = req.query.status as string;
|
|
if (req.query.tags) options.tags = (req.query.tags as string).split(',');
|
|
if (req.query.limit) options.limit = parseInt(req.query.limit as string);
|
|
if (req.query.includeStale === 'true') options.includeStale = true;
|
|
const nodes = listNodes(options);
|
|
res.json(nodes.map(n => ({ ...n, embedding: undefined })));
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Get single node + connections
|
|
router.get('/nodes/:id', (req: Request, res: Response) => {
|
|
try {
|
|
const node = getNode(param(req, 'id'));
|
|
if (!node) return res.status(404).json({ error: 'Node not found' });
|
|
const connections = getConnections(param(req, 'id'));
|
|
res.json({ ...node, embedding: undefined, connections });
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Add node
|
|
router.post('/nodes', async (req: Request, res: Response) => {
|
|
try {
|
|
const node = await addNode(req.body);
|
|
res.status(201).json({ ...node, embedding: undefined });
|
|
} catch (err: any) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Update node
|
|
router.patch('/nodes/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const node = await updateNode(param(req, 'id'), req.body);
|
|
if (!node) return res.status(404).json({ error: 'Node not found' });
|
|
res.json({ ...node, embedding: undefined });
|
|
} catch (err: any) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Delete node
|
|
router.delete('/nodes/:id', (req: Request, res: Response) => {
|
|
try {
|
|
const hard = req.query.hard === 'true';
|
|
const ok = removeNode(param(req, 'id'), hard);
|
|
if (!ok) return res.status(404).json({ error: 'Node not found' });
|
|
res.json({ ok: true });
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Add edge
|
|
router.post('/edges', (req: Request, res: Response) => {
|
|
try {
|
|
const { fromId, toId, type, metadata } = req.body;
|
|
const edge = addEdge(fromId, toId, type, metadata);
|
|
res.status(201).json(edge);
|
|
} catch (err: any) {
|
|
res.status(400).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Delete edge
|
|
router.delete('/edges/:id', (req: Request, res: Response) => {
|
|
try {
|
|
const ok = removeEdge(param(req, 'id'));
|
|
if (!ok) return res.status(404).json({ error: 'Edge not found' });
|
|
res.json({ ok: true });
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Graph — returns nodes + edges for React Flow
|
|
router.get('/graph', (_req: Request, res: Response) => {
|
|
try {
|
|
const db = getDb();
|
|
const nodes = (db.prepare('SELECT * FROM nodes WHERE is_stale = 0').all() as any[]).map(row => ({
|
|
id: row.id,
|
|
kind: row.kind,
|
|
title: row.title,
|
|
content: row.content,
|
|
status: row.status,
|
|
tags: JSON.parse(row.tags || '[]'),
|
|
metadata: JSON.parse(row.metadata || '{}'),
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
}));
|
|
const edges = (db.prepare('SELECT * FROM edges').all() as any[]).map(row => ({
|
|
id: row.id,
|
|
fromId: row.from_id,
|
|
toId: row.to_id,
|
|
type: row.type,
|
|
metadata: JSON.parse(row.metadata || '{}'),
|
|
createdAt: row.created_at,
|
|
}));
|
|
res.json({ nodes, edges });
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Search
|
|
router.post('/search', async (req: Request, res: Response) => {
|
|
try {
|
|
const { text, options } = req.body;
|
|
const results = await query(text, options || {});
|
|
res.json(results.map(r => ({ ...r, node: { ...r.node, embedding: undefined } })));
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|