app/lib/carrel/dictionary-handler.server.ts
109 lines and 14 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| withDraft | 24 to 24 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| dictionaryHandler | 26 to 124 | 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 | 28 to 42 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| existing | 45 to 49 | 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 | 54 to 72 | 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:56> | 56 to 68 | 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:70> | 70 to 70 | 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:71> | 71 to 71 | 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 | 74 to 93 | 7 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| saveDraft | 97 to 103 | 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 | 105 to 109 | 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 | 111 to 114 | 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 | 116 to 118 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| revisionSource | 120 to 122 | 1 | 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/dictionary.ts, app/lib/carrel/content-kinds.server.ts, app/lib/carrel/errors.server.ts, app/lib/carrel/front-matter-lines.ts, app/lib/dictionary/parse.mjs, app/lib/dictionary/save.server.ts, app/lib/editor/github.server.ts. Imported by app/lib/carrel/site-adapter.server.ts.
Source
// The "dictionary" kind of Carrel's content group (docs/DICTIONARY.md): the entries that open the named
// Software pages. The id is `dictionary.<key>`, the key being the file's name (`capsid` for
// content/dictionary/capsid.md). Every write is saveDictionaryEntry, the one dictionary save, so an entry saved
// through Carrel is validated by the code CI runs, committed, written to D1 and live on the next request with no
// deploy. An entry's page is a registered path, so what an entry may open is structure and lives in
// CONTENT_PAGE_PATHS in code; the validator refuses a path that is not one.
import { VersionConflictError } from "@dustinedwards/site-api";
import type { ContentStatus, ContentSummary, WriteResult } from "@dustinedwards/site-api";
import { listDictionaryRows } from "~/db/dictionary";
import type { ContentKindHandler } from "~/lib/carrel/content-kinds.server";
import { asSiteApiError } from "~/lib/carrel/errors.server";
import { withPublication } from "~/lib/carrel/front-matter-lines";
import { dictionaryPath, parseDictionaryEntry } from "~/lib/dictionary/parse.mjs";
import { fileIsDraft, saveDictionaryEntry } from "~/lib/dictionary/save.server";
import { fileRevisions, fileRevisionSource, readFile } from "~/lib/editor/github.server";
type DictionaryEnv = 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 source with its draft flag set to `draft`, touching no line when it already says so. */
const withDraft = (raw: string, draft: boolean) => (fileIsDraft(raw) === draft ? raw : withPublication(raw, { draft }));
export function dictionaryHandler(env: DictionaryEnv): ContentKindHandler {
/** One write through saveDictionaryEntry as the Carrel actor, answered in the package's terms. */
async function write(key: string, changeId: string, raw: string, expectedVersion: string | null): Promise<WriteResult> {
try {
const saved = await saveDictionaryEntry(env, {
key,
raw,
expectedBlobSha: expectedVersion ?? undefined,
isNew: expectedVersion === null,
actor: { kind: "carrel", changeId },
});
return { id: key, version: saved.sourceBlobSha, status: saved.draft ? "draft" : "published", changeId };
} catch (error) {
// A DictionaryInvalid carries every message the validator gave; asSiteApiError passes them on.
return asSiteApiError(env, error, dictionaryPath(key));
}
}
/** The stored file for a write that names a version; a version for a missing id is a conflict. */
async function existing(key: string): Promise<string> {
const file = await readFile(env, dictionaryPath(key));
if (!file) throw new VersionConflictError(null);
return file.content;
}
return {
kind: "dictionary",
async list(query) {
const needle = query.q?.toLowerCase();
const summaries: ContentSummary[] = (await listDictionaryRows(env)).map((row) => {
const status: ContentStatus = row.draft ? "draft" : "published";
return {
id: row.key,
kind: "dictionary",
title: row.term,
status,
path: row.draft ? null : row.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(key) {
const file = await readFile(env, dictionaryPath(key));
if (!file) return null;
const row = await env.DB.prepare(`SELECT synced_at FROM dictionary_entries WHERE key = ?1`).bind(key).first<{ synced_at: number }>();
const { data } = parseDictionaryEntry({ file: dictionaryPath(key), raw: file.content });
const draft = data.draft === true;
return {
id: key,
kind: "dictionary",
title: typeof data.term === "string" ? data.term : key,
status: draft ? "draft" : "published",
path: draft || typeof data.path !== "string" ? null : data.path,
publishAt: null,
publishedAt: null,
updatedAt: row ? new Date(row.synced_at * 1000).toISOString() : null,
format: "markdown",
source: file.content,
version: file.sha,
};
},
// Keeps the stored status, as a post does: a save to a live entry edits it live, and a new entry starts as
// a draft. First publication is a separate call, which Carrel's key may make.
async saveDraft(key, input) {
const file = await readFile(env, dictionaryPath(key));
if (input.expectedVersion === null && file) throw new VersionConflictError(file.sha);
if (input.expectedVersion !== null && !file) throw new VersionConflictError(null);
const raw = withDraft(input.source, file ? fileIsDraft(file.content) : true);
return write(key, input.changeId, raw, input.expectedVersion);
},
async publish(key, input) {
// Read even when source is sent: a version for an id that does not exist is a conflict.
const stored = await existing(key);
return write(key, input.changeId, withDraft(input.source ?? stored, false), input.expectedVersion);
},
async unpublish(key, input) {
const source = await existing(key);
return write(key, input.changeId, withDraft(source, true), input.expectedVersion);
},
revisions(key) {
return fileRevisions(env, dictionaryPath(key));
},
revisionSource(key, version) {
return fileRevisionSource(env, dictionaryPath(key), version);
},
};
}