server/packages/database-interface/attachment.ts
2024-06-12 18:52:01 -10:00

226 lines
7 KiB
TypeScript

import { proxyUrl } from "@/response";
import type { EntityValidator } from "@lysand-org/federation";
import {
type InferInsertModel,
type InferSelectModel,
type SQL,
desc,
eq,
inArray,
} from "drizzle-orm";
import { db } from "~/drizzle/db";
import { Attachments } from "~/drizzle/schema";
import type { AsyncAttachment as APIAsyncAttachment } from "~/types/mastodon/async_attachment";
import type { Attachment as APIAttachment } from "~/types/mastodon/attachment";
import { BaseInterface } from "./base";
export type AttachmentType = InferSelectModel<typeof Attachments>;
export class Attachment extends BaseInterface<typeof Attachments> {
async reload(): Promise<void> {
const reloaded = await Attachment.fromId(this.data.id);
if (!reloaded) {
throw new Error("Failed to reload attachment");
}
this.data = reloaded.data;
}
public static async fromId(id: string | null): Promise<Attachment | null> {
if (!id) {
return null;
}
return await Attachment.fromSql(eq(Attachments.id, id));
}
public static async fromIds(ids: string[]): Promise<Attachment[]> {
return await Attachment.manyFromSql(inArray(Attachments.id, ids));
}
public static async fromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Attachments.id),
): Promise<Attachment | null> {
const found = await db.query.Attachments.findFirst({
where: sql,
orderBy,
});
if (!found) {
return null;
}
return new Attachment(found);
}
public static async manyFromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Attachments.id),
limit?: number,
offset?: number,
extra?: Parameters<typeof db.query.Attachments.findMany>[0],
): Promise<Attachment[]> {
const found = await db.query.Attachments.findMany({
where: sql,
orderBy,
limit,
offset,
with: extra?.with,
});
return found.map((s) => new Attachment(s));
}
async update(
newAttachment: Partial<AttachmentType>,
): Promise<AttachmentType> {
await db
.update(Attachments)
.set(newAttachment)
.where(eq(Attachments.id, this.id));
const updated = await Attachment.fromId(this.data.id);
if (!updated) {
throw new Error("Failed to update attachment");
}
this.data = updated.data;
return updated.data;
}
save(): Promise<AttachmentType> {
return this.update(this.data);
}
async delete(ids: string[]): Promise<void>;
async delete(): Promise<void>;
async delete(ids?: unknown): Promise<void> {
if (Array.isArray(ids)) {
await db.delete(Attachments).where(inArray(Attachments.id, ids));
} else {
await db.delete(Attachments).where(eq(Attachments.id, this.id));
}
}
public static async insert(
data: InferInsertModel<typeof Attachments>,
): Promise<Attachment> {
const inserted = (
await db.insert(Attachments).values(data).returning()
)[0];
const attachment = await Attachment.fromId(inserted.id);
if (!attachment) {
throw new Error("Failed to insert attachment");
}
return attachment;
}
get id() {
return this.data.id;
}
public getMastodonType(): APIAttachment["type"] {
if (this.data.mimeType.startsWith("image/")) {
return "image";
}
if (this.data.mimeType.startsWith("video/")) {
return "video";
}
if (this.data.mimeType.startsWith("audio/")) {
return "audio";
}
return "unknown";
}
public toApiMeta(): APIAttachment["meta"] {
return {
width: this.data.width || undefined,
height: this.data.height || undefined,
fps: this.data.fps || undefined,
size:
this.data.width && this.data.height
? `${this.data.width}x${this.data.height}`
: undefined,
duration: this.data.duration || undefined,
length: undefined,
aspect:
this.data.width && this.data.height
? this.data.width / this.data.height
: undefined,
original: {
width: this.data.width || undefined,
height: this.data.height || undefined,
size:
this.data.width && this.data.height
? `${this.data.width}x${this.data.height}`
: undefined,
aspect:
this.data.width && this.data.height
? this.data.width / this.data.height
: undefined,
},
// Idk whether size or length is the right value
};
}
public toApi(): APIAttachment | APIAsyncAttachment {
return {
id: this.data.id,
type: this.getMastodonType(),
url: proxyUrl(this.data.url) ?? "",
remote_url: proxyUrl(this.data.remoteUrl),
preview_url: proxyUrl(this.data.thumbnailUrl || this.data.url),
text_url: null,
meta: this.toApiMeta(),
description: this.data.description,
blurhash: this.data.blurhash,
};
}
public toLysand(): typeof EntityValidator.$ContentFormat {
return {
[this.data.mimeType]: {
content: this.data.url,
blurhash: this.data.blurhash ?? undefined,
description: this.data.description ?? undefined,
duration: this.data.duration ?? undefined,
fps: this.data.fps ?? undefined,
height: this.data.height ?? undefined,
size: this.data.size ?? undefined,
hash: this.data.sha256
? {
sha256: this.data.sha256,
}
: undefined,
width: this.data.width ?? undefined,
},
};
}
public static fromLysand(
attachmentToConvert: typeof EntityValidator.$ContentFormat,
): Promise<Attachment> {
const key = Object.keys(attachmentToConvert)[0];
const value = attachmentToConvert[key];
return Attachment.insert({
mimeType: key,
url: value.content,
description: value.description || undefined,
duration: value.duration || undefined,
fps: value.fps || undefined,
height: value.height || undefined,
size: value.size || undefined,
width: value.width || undefined,
sha256: value.hash?.sha256 || undefined,
blurhash: value.blurhash || undefined,
});
}
}