AkurAI Build
Menu

popagent

public

Latest change 87f51a535b92b11ccaf217f96b05a14c907d8eac - Store project documentation in PostgreSQL by Ólafur Búi Ólafsson

import type { DocumentationFolder, DocumentationPage, DocumentationPageSummary } from "./api-types";
import { storage } from "./storage";
import { retryableInit } from "./retryable-init";

export const MAX_DOCUMENTATION_CHARACTERS = 1_000_000;

export class DocumentationConflictError extends Error {
  constructor(readonly current: DocumentationPage) {
    super("Documentation revision conflict");
  }
}

function documentationPath(input: string, folder = false): string {
  const path = input.trim().replaceAll("\\", "/").replace(/^\/+|\/+$/g, "");
  const parts = path.split("/");
  if (!path || parts.some((part) => !part || part === "." || part === ".." || part.startsWith("."))) {
    throw new RangeError(`Invalid documentation ${folder ? "folder" : "path"}`);
  }
  if (!folder && !path.endsWith(".md")) throw new RangeError("Invalid documentation path");
  return path;
}

function digest(content: string): string {
  return new Bun.CryptoHasher("sha256").update(content).digest("hex");
}

function pageTitle(path: string, content: string): string {
  const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
  if (heading) return heading.replace(/\s+#+$/, "");
  return path.split("/").at(-1)!.replace(/\.md$/i, "").replaceAll(/[-_]+/g, " ");
}

export class DocumentationFiles {
  private readonly initializeOnce = retryableInit(async () => {
    await storage.init();
    await storage.db.none(await Bun.file(new URL("./documentation.sql", import.meta.url)).text());
  });

  private init(): Promise<void> {
    return this.initializeOnce();
  }

  async list(workspaceId: string): Promise<{ folders: DocumentationFolder[]; pages: DocumentationPageSummary[] }> {
    await this.init();
    const [folders, pages] = await Promise.all([
      storage.db.any<DocumentationFolder>(`
        SELECT path, name, updated_at::text AS "updatedAt"
        FROM popagent_document_folders WHERE workspace_id = $1 ORDER BY path
      `, [workspaceId]),
      storage.db.any<DocumentationPageSummary>(`
        SELECT path, title, revision, length(content)::int AS size, updated_at::text AS "updatedAt"
        FROM popagent_document_pages WHERE workspace_id = $1 ORDER BY path
      `, [workspaceId]),
    ]);
    return { folders, pages };
  }

  async read(workspaceId: string, inputPath: string): Promise<DocumentationPage> {
    await this.init();
    const path = documentationPath(inputPath);
    const page = await storage.db.oneOrNone<DocumentationPage>(`
      SELECT path, title, revision, length(content)::int AS size,
             updated_at::text AS "updatedAt", content
      FROM popagent_document_pages WHERE workspace_id = $1 AND path = $2
    `, [workspaceId, path]);
    if (!page) throw new Error("Documentation page not found");
    return page;
  }

  async createFolder(workspaceId: string, inputPath: string): Promise<DocumentationFolder> {
    await this.init();
    const path = documentationPath(inputPath, true);
    const segments = path.split("/");
    let current = "";
    let folder!: DocumentationFolder;
    for (const name of segments) {
      current = current ? `${current}/${name}` : name;
      folder = await storage.db.one<DocumentationFolder>(`
        INSERT INTO popagent_document_folders (workspace_id, path, name)
        VALUES ($1, $2, $3)
        ON CONFLICT (workspace_id, path) DO UPDATE SET name = EXCLUDED.name
        RETURNING path, name, updated_at::text AS "updatedAt"
      `, [workspaceId, current, name]);
    }
    return folder;
  }

  async save(workspaceId: string, input: { path: string; content: string; revision?: string }): Promise<DocumentationPage> {
    const path = documentationPath(input.path);
    if (input.content.length > MAX_DOCUMENTATION_CHARACTERS) throw new RangeError("Documentation page is too large");
    await this.init();
    const current = await this.read(workspaceId, path).catch((error) => {
      if (error instanceof Error && error.message === "Documentation page not found") return undefined;
      throw error;
    });
    if (current && input.revision !== current.revision) throw new DocumentationConflictError(current);
    if (!current && input.revision) throw new Error("Documentation page not found");
    const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
    if (parent) await this.createFolder(workspaceId, parent);
    const revision = digest(input.content);
    const title = pageTitle(path, input.content);
    return storage.db.one<DocumentationPage>(`
      INSERT INTO popagent_document_pages (workspace_id, path, title, content, revision)
      VALUES ($1, $2, $3, $4, $5)
      ON CONFLICT (workspace_id, path) DO UPDATE SET
        title = EXCLUDED.title, content = EXCLUDED.content,
        revision = EXCLUDED.revision, updated_at = NOW()
      RETURNING path, title, revision, length(content)::int AS size,
                updated_at::text AS "updatedAt", content
    `, [workspaceId, path, title, input.content, revision]);
  }

  async move(workspaceId: string, inputPath: string, nextInputPath: string, revision: string): Promise<DocumentationPage> {
    const current = await this.read(workspaceId, inputPath);
    if (revision !== current.revision) throw new DocumentationConflictError(current);
    const nextPath = documentationPath(nextInputPath);
    if (nextPath === current.path) return current;
    const parent = nextPath.includes("/") ? nextPath.slice(0, nextPath.lastIndexOf("/")) : "";
    if (parent) await this.createFolder(workspaceId, parent);
    const page = await storage.db.oneOrNone<DocumentationPage>(`
      UPDATE popagent_document_pages SET path = $3, updated_at = NOW()
      WHERE workspace_id = $1 AND path = $2
        AND NOT EXISTS (
          SELECT 1 FROM popagent_document_pages WHERE workspace_id = $1 AND path = $3
        )
      RETURNING path, title, revision, length(content)::int AS size,
                updated_at::text AS "updatedAt", content
    `, [workspaceId, current.path, nextPath]);
    if (!page) throw new Error("Documentation page already exists");
    return page;
  }

  async remove(workspaceId: string, inputPath: string, revision: string): Promise<void> {
    const current = await this.read(workspaceId, inputPath);
    if (revision !== current.revision) throw new DocumentationConflictError(current);
    await storage.db.none(
      "DELETE FROM popagent_document_pages WHERE workspace_id = $1 AND path = $2",
      [workspaceId, current.path],
    );
  }

  async removeFolder(workspaceId: string, inputPath: string): Promise<string[]> {
    await this.init();
    const path = documentationPath(inputPath, true);
    return storage.db.tx(async (transaction) => {
      const pages = await transaction.any<{ path: string }>(`
        DELETE FROM popagent_document_pages
        WHERE workspace_id = $1 AND (path = $2 OR path LIKE $2 || '/%')
        RETURNING path
      `, [workspaceId, path]);
      const deleted = await transaction.oneOrNone<{ path: string }>(`
        DELETE FROM popagent_document_folders
        WHERE workspace_id = $1 AND path = $2
        RETURNING path
      `, [workspaceId, path]);
      if (!deleted) throw new Error("Documentation folder not found");
      await transaction.none(`
        DELETE FROM popagent_document_folders
        WHERE workspace_id = $1 AND path LIKE $2 || '/%'
      `, [workspaceId, path]);
      return pages.map((page) => page.path);
    });
  }
}