app/lib/carrel/cv-handler.server.ts
126 lines and 19 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| titleOf | 25 to 29 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:27> | 27 to 27 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| cvHandler | 31 to 143 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| registered | 33 to 35 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| target | 38 to 47 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:43> | 43 to 43 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| write | 50 to 66 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| background | 59 to 59 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| existing | 69 to 73 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| list | 78 to 93 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:80> | 80 to 89 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:91> | 91 to 91 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:92> | 92 to 92 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| get | 95 to 114 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| saveDraft | 117 to 124 | 6 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| publish | 126 to 129 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| unpublish | 131 to 133 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| revisions | 135 to 137 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| revisionSource | 139 to 141 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/db/cv.ts, app/lib/carrel/content-kinds.server.ts, app/lib/carrel/errors.server.ts, app/lib/cv/entries.mjs, app/lib/cv/parse.mjs, app/lib/cv/save.server.ts, app/lib/cv/view.mjs, app/lib/editor/github.server.ts. Imported by app/lib/carrel/site-adapter.server.ts.
Source
// The "cv" kind of Carrel's content group (docs/CV.md): the files in content/cv/. The id is `cv.<slug>`, the slug
// being the file's name (`cv.grants` for content/cv/grants.md, `cv.profile` for the edition and the person).
// Every write is saveCvFile, the one CV save, so a file saved through Carrel is validated by the code CI runs,
// committed, written to D1 and live on the next request with no deploy. A file cannot be created here: the set
// of files is structure and lives in CV_FILES, so an id no file names is refused. The CV has no draft state, so
// it cannot be unpublished.
import { RefusedError, VersionConflictError } from "@dustinedwards/site-api";
import type { ContentSummary, WriteResult } from "@dustinedwards/site-api";
import { listCvRows } from "~/db/cv";
import type { ContentKindHandler } from "~/lib/carrel/content-kinds.server";
import { asSiteApiError } from "~/lib/carrel/errors.server";
import { CV_PAGE } from "~/lib/cv/entries.mjs";
import { CV_DIR, CV_FILES, cvFileFor, cvSourcePath } from "~/lib/cv/parse.mjs";
import { saveCvFile } from "~/lib/cv/save.server";
import { TYPES } from "~/lib/cv/view.mjs";
import { fileRevisions, fileRevisionSource, readFile } from "~/lib/editor/github.server";
type CvEnv = Env & { GITHUB_TOKEN?: string };
/** A revision's version is a commit sha; an item's own version is the blob sha of its file. */
/** The name a list shows for a file: the section's plural as the CV prints it. */
function titleOf(slug: string) {
const type = cvFileFor(slug)?.type;
const plural = TYPES.find(([id]) => id === type)?.[2];
return `${CV_PAGE.title}: ${type === "profile" ? "Profile" : (plural ?? slug)}`;
}
export function cvHandler(env: CvEnv, ctx: Pick<ExecutionContext, "waitUntil">): ContentKindHandler {
/** The file of a registered slug, or null: an id no file names owns nothing. */
function registered(slug: string) {
return cvFileFor(slug) ? cvSourcePath(slug) : null;
}
/** The same, for a write: a missing registration is a refusal that says why. */
function target(slug: string) {
const found = registered(slug);
if (!found) {
throw new RefusedError(
`"${slug}" names no CV file. A save cannot create one: the set of files is CV_FILES ` +
`(app/lib/cv/parse.mjs). The files are ${CV_FILES.map((f) => f.slug).join(", ")}, in ${CV_DIR}/.`,
);
}
return found;
}
/** One write through saveCvFile as the Carrel actor, answered in the package's terms. */
async function write(slug: string, changeId: string, raw: string, expectedVersion: string | null): Promise<WriteResult> {
target(slug);
try {
const saved = await saveCvFile(env, {
slug,
raw,
expectedBlobSha: expectedVersion ?? undefined,
actor: { kind: "carrel", changeId },
// The PDF follows the save after the response: Browser Run is seconds, a save should not wait for it.
background: (work) => ctx.waitUntil(work),
});
return { id: slug, version: saved.sourceBlobSha, status: "published", changeId };
} catch (error) {
// A CvInvalid carries every message the validator gave; asSiteApiError passes them on.
return asSiteApiError(env, error, target(slug));
}
}
/** The stored file for a write that names a version; a version for a missing file is a conflict. */
async function existing(slug: string): Promise<string> {
const file = await readFile(env, target(slug));
if (!file) throw new VersionConflictError(null);
return file.content;
}
return {
kind: "cv",
async list(query) {
const needle = query.q?.toLowerCase();
const summaries: ContentSummary[] = (await listCvRows(env)).map((row) => ({
id: row.slug,
kind: "cv",
title: titleOf(row.slug),
status: "published",
path: CV_PAGE.path,
publishAt: null,
publishedAt: null,
updatedAt: row.syncedAt.toISOString(),
}));
return summaries
.filter((s) => !query.status || s.status === query.status)
.filter((s) => !needle || s.title.toLowerCase().includes(needle) || s.id.includes(needle));
},
async get(slug) {
const found = registered(slug);
if (!found) return null;
const file = await readFile(env, found);
if (!file) return null;
const row = await env.DB.prepare(`SELECT synced_at FROM cv WHERE slug = ?1`).bind(slug).first<{ synced_at: number }>();
return {
id: slug,
kind: "cv",
title: titleOf(slug),
status: "published",
path: CV_PAGE.path,
publishAt: null,
publishedAt: null,
updatedAt: row ? new Date(row.synced_at * 1000).toISOString() : null,
format: "markdown",
source: file.content,
version: file.sha,
};
},
// A file always exists, so a save that names no version is a conflict, as a page's is.
async saveDraft(slug, input) {
const found = target(slug);
const file = await readFile(env, found);
if (input.expectedVersion === null && file) throw new VersionConflictError(file.sha);
if (input.expectedVersion !== null && !file) throw new VersionConflictError(null);
if (!file) throw new RefusedError(`The file ${found} is missing from the repository, so it cannot be saved.`);
return write(slug, input.changeId, input.source, input.expectedVersion);
},
async publish(slug, input) {
const stored = await existing(slug);
return write(slug, input.changeId, input.source ?? stored, input.expectedVersion);
},
async unpublish() {
throw new RefusedError("The CV has no draft state, so it cannot be unpublished. Edit the file, or remove an entry.");
},
revisions(slug) {
return fileRevisions(env, registered(slug) ?? null);
},
revisionSource(slug, version) {
return fileRevisionSource(env, registered(slug) ?? null, version);
},
};
}