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:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user