feat: Phase 11 — CRUD (create/update/delete) + auth + web UI

- Core: createDocument/updateDocument/deleteDocument (file + DB + revision + chunks)
- Parser: stringifyFile (serialize markdown with frontmatter to disk)
- API: tRPC CRUD routers (auth-gated via requireWriteAuth middleware)
- API: createContext now passes expectedSecret from deps (request-scoped auth)
- MCP: 3 new write tools (create_document, update_document, delete_document)
- Web: /create page + ?edit=1 form, /api/auth/login (cookie-based), /api/docs REST CRUD
- Web: Edit buttons on homepage + doc pages (auth-gated)
- Tests: 8 new tests (5 tRPC CRUD + 3 MCP CRUD auth), 40 total all green
This commit is contained in:
asepharyana
2026-08-20 13:08:27 +07:00
parent 2d974b8952
commit 57f90018da
19 changed files with 1215 additions and 23 deletions
+48 -1
View File
@@ -1,5 +1,6 @@
import matter from "gray-matter";
import { readFileSync } from "node:fs";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import type {
DocSection,
DocStatus,
@@ -63,3 +64,49 @@ export function parseFile(absPath: string, relPath: string): ParsedFile {
return { meta, body: content };
}
/**
* Serialize document metadata + body back to a markdown file with YAML
* frontmatter. The file is written to `absPath`, creating parent dirs as needed.
* The `relPath` is stored in frontmatter as `path` so the file is round-trip
* stable (parseFile → stringifyFile → parseFile yields the same meta+body).
*
* @param absPath absolute path on disk
* @param relPath path relative to content root (e.g. "docs/my/doc.md")
* @param meta document metadata
* @param body markdown body (frontmatter stripped — same as parseFile.body)
*/
export function stringifyFile(
absPath: string,
relPath: string,
meta: DocumentMeta,
body: string,
): void {
const data: Record<string, unknown> = {
title: meta.title,
type: meta.type,
section: meta.section,
status: meta.status,
author: meta.author,
tags: meta.tags,
path: relPath,
created_at: meta.createdAt,
updated_at: meta.updatedAt,
};
const yaml = "---\n" +
Object.entries(data)
.map(([k, v]) => {
if (Array.isArray(v)) {
return `${k}: [${v.map((x) => `"${x}"`).join(", ")}]`;
}
if (typeof v === "string") {
return `${k}: ${JSON.stringify(v)}`;
}
return `${k}: ${v}`;
})
.join("\n") +
"\n---\n";
const content = yaml + body;
mkdirSync(dirname(absPath), { recursive: true });
writeFileSync(absPath, content, "utf8");
}