feat(export): add single PDF export for sections and events with chapter ordering
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-08-22 11:41:49 +07:00
parent 2a70a111a2
commit 5cd9ea92a7
14 changed files with 1891 additions and 6 deletions
+115
View File
@@ -0,0 +1,115 @@
import { and, eq, like, or, sql } from "drizzle-orm";
import { db } from "@mcpedia/db";
import { documents, type DocumentRow } from "@mcpedia/db/schema";
import { CONTENT_ROOT } from "@mcpedia/config";
import { parseFile } from "@mcpedia/parser";
import { existsSync } from "node:fs";
import { join } from "node:path";
import type {
Document,
ExportData,
ExportQueryOptions,
} from "@mcpedia/types";
import { toMeta } from "./row-map";
import {
buildExportSummary,
sortExportDocuments,
} from "./export.helpers";
export * from "./export.helpers";
/**
* Fetch all documents for export with full bodies directly from PostgreSQL
* (with fallback to disk if empty).
*/
export async function getExportDocuments(
opts: ExportQueryOptions = {},
): Promise<ExportData> {
const status = opts.status ?? "published";
const sortBy = opts.sortBy ?? "category_points";
let rows: DocumentRow[] = [];
if (opts.slugs && opts.slugs.length > 0) {
// Explicit list of slugs
rows = await db
.select()
.from(documents)
.where(
and(
eq(documents.status, status),
sql`${documents.slug} IN ${opts.slugs}`,
),
);
} else if (opts.path) {
const cleanPath = opts.path.replace(/^\/+|\/+$/g, "");
// 1. Try exact match for single document
const exactMatch = await db
.select()
.from(documents)
.where(and(eq(documents.status, status), eq(documents.slug, cleanPath)));
if (exactMatch.length === 1 && exactMatch[0]) {
rows = exactMatch;
} else {
// 2. Try prefix match for folder/event or section
rows = await db
.select()
.from(documents)
.where(
and(
eq(documents.status, status),
or(
eq(documents.slug, cleanPath),
like(documents.slug, `${cleanPath}/%`),
eq(documents.section, cleanPath),
),
),
);
}
} else if (opts.section) {
// Section-wide match
rows = await db
.select()
.from(documents)
.where(
and(
eq(documents.status, status),
eq(documents.section, opts.section),
),
);
} else {
// All published documents
rows = await db
.select()
.from(documents)
.where(eq(documents.status, status));
}
// Map rows to Document with bodies
const loadedDocs: Document[] = rows.map((row) => {
let body = row.body;
if (!body) {
const abs = join(CONTENT_ROOT, row.path);
if (existsSync(abs)) {
try {
const parsed = parseFile(abs, row.path);
body = parsed.body;
} catch {
body = "";
}
}
}
return { ...toMeta(row), body: body || "" };
});
// Sort neatly
const sortedDocs = sortExportDocuments(loadedDocs, sortBy);
const summary = buildExportSummary(sortedDocs, opts.path, opts.section);
return {
summary,
documents: sortedDocs,
};
}