import { createDocument, getDocument, getDocuments, transformDocument, } from "@lib/documents.ts"; import { Root } from "https://esm.sh/remark-frontmatter@4.0.1"; import { GenericResource } from "@lib/types.ts"; import { parseRating } from "@lib/helpers.ts"; import { isLocalImage } from "@lib/string.ts"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; import { imageTable } from "@lib/db/schema.ts"; import { db } from "@lib/db/sqlite.ts"; import { eq } from "drizzle-orm/sql"; import { createCache } from "@lib/cache.ts"; export async function addThumbnailToResource( res: T, ): Promise { if (!res?.meta?.image) return res; const imageUrl = isLocalImage(res.meta.image) ? `${SILVERBULLET_SERVER}/${res.meta.image}` : res.meta.image; const image = await db.select().from(imageTable) .where(eq(imageTable.url, imageUrl)) .limit(1) .then((images) => images[0]); if (image) { return { ...res, meta: { ...res.meta, average: image.average, thumbnail: image.blurhash, }, }; } return res; } type SortType = "rating" | "date" | "name" | "author"; function sortFunction(sortType: SortType) { return (a: T, b: T) => { switch (sortType) { case "rating": return parseRating(a.meta?.rating || 0) > parseRating(b.meta?.rating || 0) ? -1 : 1; case "date": return (a.meta?.date || 0) > (b.meta?.date || 0) ? -1 : 1; case "name": return a.name.localeCompare(b.name); case "author": return a.meta?.author?.localeCompare(b.meta?.author || "") || 0; default: return 0; } }; } export function createCrud( { prefix, parse, render, hasThumbnails = false }: { prefix: string; hasThumbnails?: boolean; render?: (doc: T) => string; parse: (doc: string, id: string) => T; }, ) { const cache = createCache(`crud/${prefix}`, { expires: 60 * 1000 }); function pathFromId(id: string) { return `${prefix}${id.replaceAll(":", "")}.md`; } async function read(id: string) { const path = pathFromId(id); const content = await getDocument(path); if (!content) { return; } let parsed = parse(content, id); if (hasThumbnails) { parsed = await addThumbnailToResource(parsed); } const doc = { ...parsed, content }; return doc; } function create(id: string, content: string | ArrayBuffer | T) { const path = pathFromId(id); cache.set("all", undefined); if ( typeof content === "string" || content instanceof ArrayBuffer ) { return createDocument(path, content); } if (render) { const rendered = render(content); return createDocument(path, rendered); } throw new Error("No renderer defined for " + prefix + " CRUD"); } async function update(id: string, updater: (r: Root) => Root) { const path = pathFromId(id); const content = await getDocument(path); if (!content) { return; } const newDoc = transformDocument(content, updater); await createDocument(path, newDoc); } async function readAll({ sort = "rating" }: { sort?: SortType } = {}) { if (cache.has("all")) { return cache.get("all") as unknown as T[]; } const allDocuments = await getDocuments(); const parsed = (await Promise.all( allDocuments.filter((d) => { return d.name.startsWith(prefix) && d.contentType === "text/markdown" && !d.name.endsWith("index.md"); }).map((doc) => { const id = doc.name.replace(prefix, "").replace(/\.md$/, ""); return read(id); }), )).sort(sortFunction(sort)).filter((v) => !!v); return parsed; } return { read, readAll, create, update, }; }