2023-08-01 17:50:00 +02:00
|
|
|
import {
|
|
|
|
createDocument,
|
|
|
|
getDocument,
|
|
|
|
getDocuments,
|
|
|
|
transformDocument,
|
|
|
|
} from "@lib/documents.ts";
|
|
|
|
import { Root } from "https://esm.sh/remark-frontmatter@4.0.1";
|
2023-08-29 13:48:52 +02:00
|
|
|
import { GenericResource } from "@lib/types.ts";
|
|
|
|
import { parseRating } from "@lib/helpers.ts";
|
2025-01-05 21:27:31 +01:00
|
|
|
import { isLocalImage } from "@lib/string.ts";
|
|
|
|
import { SILVERBULLET_SERVER } from "@lib/env.ts";
|
2025-01-06 16:14:29 +01:00
|
|
|
import { imageTable } from "@lib/db/schema.ts";
|
|
|
|
import { db } from "@lib/db/sqlite.ts";
|
2025-01-05 21:27:31 +01:00
|
|
|
import { eq } from "drizzle-orm/sql";
|
2025-01-25 18:57:06 +01:00
|
|
|
import { createCache } from "@lib/cache.ts";
|
2023-08-11 16:13:20 +02:00
|
|
|
|
2025-01-05 21:27:31 +01:00
|
|
|
export async function addThumbnailToResource<T extends GenericResource>(
|
2023-08-11 16:13:20 +02:00
|
|
|
res: T,
|
|
|
|
): Promise<T> {
|
2025-01-05 21:27:31 +01:00
|
|
|
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;
|
2023-08-11 16:13:20 +02:00
|
|
|
}
|
2023-08-01 17:50:00 +02:00
|
|
|
|
2023-08-29 13:48:52 +02:00
|
|
|
type SortType = "rating" | "date" | "name" | "author";
|
|
|
|
|
|
|
|
function sortFunction<T extends GenericResource>(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":
|
2025-01-06 16:14:29 +01:00
|
|
|
return a.meta?.author?.localeCompare(b.meta?.author || "") || 0;
|
|
|
|
default:
|
|
|
|
return 0;
|
2023-08-29 13:48:52 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createCrud<T extends GenericResource>(
|
2023-08-11 16:13:20 +02:00
|
|
|
{ prefix, parse, render, hasThumbnails = false }: {
|
2023-08-01 17:50:00 +02:00
|
|
|
prefix: string;
|
2023-08-11 16:13:20 +02:00
|
|
|
hasThumbnails?: boolean;
|
|
|
|
render?: (doc: T) => string;
|
2023-08-01 17:50:00 +02:00
|
|
|
parse: (doc: string, id: string) => T;
|
|
|
|
},
|
|
|
|
) {
|
2025-01-25 18:57:06 +01:00
|
|
|
const cache = createCache<T>(`crud/${prefix}`, { expires: 60 * 1000 });
|
|
|
|
|
2023-08-01 17:50:00 +02:00
|
|
|
function pathFromId(id: string) {
|
2023-08-04 22:35:25 +02:00
|
|
|
return `${prefix}${id.replaceAll(":", "")}.md`;
|
2023-08-01 17:50:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function read(id: string) {
|
|
|
|
const path = pathFromId(id);
|
2025-01-06 16:14:29 +01:00
|
|
|
|
2023-08-01 17:50:00 +02:00
|
|
|
const content = await getDocument(path);
|
2025-01-18 00:46:05 +01:00
|
|
|
if (!content) {
|
|
|
|
return;
|
|
|
|
}
|
2023-08-19 23:29:39 +02:00
|
|
|
|
2025-01-25 00:45:22 +01:00
|
|
|
let parsed = parse(content, id);
|
2023-08-11 16:13:20 +02:00
|
|
|
|
|
|
|
if (hasThumbnails) {
|
2025-01-25 00:45:22 +01:00
|
|
|
parsed = await addThumbnailToResource(parsed);
|
2023-08-11 16:13:20 +02:00
|
|
|
}
|
2025-01-06 16:14:29 +01:00
|
|
|
const doc = { ...parsed, content };
|
2023-08-11 16:13:20 +02:00
|
|
|
|
2025-01-06 16:14:29 +01:00
|
|
|
return doc;
|
2023-08-01 17:50:00 +02:00
|
|
|
}
|
2023-08-11 16:13:20 +02:00
|
|
|
function create(id: string, content: string | ArrayBuffer | T) {
|
2023-08-01 17:50:00 +02:00
|
|
|
const path = pathFromId(id);
|
2025-01-25 18:57:06 +01:00
|
|
|
cache.set("all", undefined);
|
2023-08-11 16:13:20 +02:00
|
|
|
if (
|
|
|
|
typeof content === "string" || content instanceof ArrayBuffer
|
|
|
|
) {
|
|
|
|
return createDocument(path, content);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (render) {
|
2025-01-18 00:46:05 +01:00
|
|
|
const rendered = render(content);
|
|
|
|
return createDocument(path, rendered);
|
2023-08-11 16:13:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error("No renderer defined for " + prefix + " CRUD");
|
2023-08-01 17:50:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function update(id: string, updater: (r: Root) => Root) {
|
|
|
|
const path = pathFromId(id);
|
|
|
|
const content = await getDocument(path);
|
2025-01-18 00:46:05 +01:00
|
|
|
if (!content) {
|
|
|
|
return;
|
|
|
|
}
|
2023-08-01 17:50:00 +02:00
|
|
|
const newDoc = transformDocument(content, updater);
|
|
|
|
await createDocument(path, newDoc);
|
|
|
|
}
|
|
|
|
|
2023-08-29 13:48:52 +02:00
|
|
|
async function readAll({ sort = "rating" }: { sort?: SortType } = {}) {
|
2025-01-25 18:57:06 +01:00
|
|
|
if (cache.has("all")) {
|
|
|
|
return cache.get("all") as unknown as T[];
|
|
|
|
}
|
2023-08-01 17:50:00 +02:00
|
|
|
const allDocuments = await getDocuments();
|
2025-01-06 16:14:29 +01:00
|
|
|
const parsed = (await Promise.all(
|
2023-08-01 17:50:00 +02:00
|
|
|
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);
|
|
|
|
}),
|
2025-01-18 00:46:05 +01:00
|
|
|
)).sort(sortFunction<T>(sort)).filter((v) => !!v);
|
|
|
|
|
2025-01-06 16:14:29 +01:00
|
|
|
return parsed;
|
2023-08-01 17:50:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
read,
|
|
|
|
readAll,
|
|
|
|
create,
|
|
|
|
update,
|
|
|
|
};
|
|
|
|
}
|