refactor(database): 🚚 Rename "Attachment" to "Media"

This commit is contained in:
Jesse Wierzbinski 2025-01-23 16:08:42 +01:00
parent bbd56b600d
commit 2f61cd8f0a
No known key found for this signature in database
21 changed files with 2429 additions and 101 deletions

View file

@ -97,7 +97,7 @@ export default apiRoute((app) =>
min_id ? gt(Notes.id, min_id) : undefined,
eq(Notes.authorId, otherUser.id),
only_media
? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})`
? sql`EXISTS (SELECT 1 FROM "Medias" WHERE "Medias"."noteId" = ${Notes.id})`
: undefined,
pinned
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = ${Notes.id} AND "UserToPinnedNotes"."userId" = ${otherUser.id})`

View file

@ -3,7 +3,7 @@ import { mimeLookup } from "@/content_types";
import { mergeAndDeduplicate } from "@/lib";
import { sanitizedHtmlStrip } from "@/sanitization";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji, User } from "@versia/kit/db";
import { Emoji, Media, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1";
@ -251,7 +251,7 @@ export default apiRoute((app) =>
await mediaManager.addFile(avatar);
const contentType = uploadedFile.type;
self.avatar = Attachment.getUrl(path);
self.avatar = Media.getUrl(path);
self.source.avatar = {
content_type: contentType,
};
@ -269,7 +269,7 @@ export default apiRoute((app) =>
await mediaManager.addFile(header);
const contentType = uploadedFile.type;
self.header = Attachment.getUrl(path);
self.header = Media.getUrl(path);
self.source.header = {
content_type: contentType,
};

View file

@ -1,7 +1,7 @@
import { apiRoute, auth, emojiValidator, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji, db } from "@versia/kit/db";
import { Emoji, Media, db } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { z } from "zod";
@ -270,7 +270,7 @@ export default apiRoute((app) => {
if (element instanceof File) {
const uploaded = await mediaManager.addFile(element);
url = Attachment.getUrl(uploaded.path);
url = Media.getUrl(uploaded.path);
contentType = uploaded.uploadedFile.type;
} else {
url = element;

View file

@ -1,7 +1,7 @@
import { apiRoute, auth, emojiValidator, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji } from "@versia/kit/db";
import { Emoji, Media } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
import { z } from "zod";
@ -149,7 +149,7 @@ export default apiRoute((app) =>
const uploaded = await mediaManager.addFile(element);
url = Attachment.getUrl(uploaded.path);
url = Media.getUrl(uploaded.path);
contentType = uploaded.uploadedFile.type;
} else {
url = element;

View file

@ -1,6 +1,6 @@
import { apiRoute, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db";
import { Media } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
@ -48,7 +48,7 @@ const routePut = createRoute({
description: "Media updated",
content: {
"application/json": {
schema: Attachment.schema,
schema: Media.schema,
},
},
},
@ -82,7 +82,7 @@ const routeGet = createRoute({
description: "Media",
content: {
"application/json": {
schema: Attachment.schema,
schema: Media.schema,
},
},
},
@ -101,7 +101,7 @@ export default apiRoute((app) => {
app.openapi(routePut, async (context) => {
const { id } = context.req.valid("param");
const attachment = await Attachment.fromId(id);
const attachment = await Media.fromId(id);
if (!attachment) {
throw new ApiError(404, "Media not found");
@ -115,7 +115,7 @@ export default apiRoute((app) => {
if (thumbnail) {
const { path } = await mediaManager.addFile(thumbnail);
thumbnailUrl = Attachment.getUrl(path);
thumbnailUrl = Media.getUrl(path);
}
const descriptionText = description || attachment.data.description;
@ -138,7 +138,7 @@ export default apiRoute((app) => {
app.openapi(routeGet, async (context) => {
const { id } = context.req.valid("param");
const attachment = await Attachment.fromId(id);
const attachment = await Media.fromId(id);
if (!attachment) {
throw new ApiError(404, "Media not found");

View file

@ -1,6 +1,6 @@
import { apiRoute, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db";
import { Media } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { config } from "~/packages/config-manager/index.ts";
@ -43,7 +43,7 @@ const route = createRoute({
description: "Attachment",
content: {
"application/json": {
schema: Attachment.schema,
schema: Media.schema,
},
},
},
@ -71,7 +71,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { file, thumbnail, description } = context.req.valid("form");
const attachment = await Attachment.fromFile(file, {
const attachment = await Media.fromFile(file, {
thumbnail,
description,
});

View file

@ -1,6 +1,6 @@
import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Note } from "@versia/kit/db";
import { Media, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1";
import { z } from "zod";
@ -246,7 +246,7 @@ export default apiRoute((app) => {
} = context.req.valid("json");
const foundAttachments =
media_ids.length > 0 ? await Attachment.fromIds(media_ids) : [];
media_ids.length > 0 ? await Media.fromIds(media_ids) : [];
if (foundAttachments.length !== media_ids.length) {
throw new ApiError(

View file

@ -1,6 +1,6 @@
import { apiRoute, auth, jsonOrForm } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment, Note } from "@versia/kit/db";
import { Media, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1";
import { z } from "zod";
@ -151,7 +151,7 @@ export default apiRoute((app) =>
// Check if media attachments are all valid
const foundAttachments =
media_ids.length > 0 ? await Attachment.fromIds(media_ids) : [];
media_ids.length > 0 ? await Media.fromIds(media_ids) : [];
if (foundAttachments.length !== media_ids.length) {
throw new ApiError(

View file

@ -74,7 +74,7 @@ export default apiRoute((app) =>
? sql`EXISTS (SELECT 1 FROM "Users" WHERE "Users"."id" = ${Notes.authorId} AND "Users"."instanceId" IS NULL)`
: undefined,
only_media
? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})`
? sql`EXISTS (SELECT 1 FROM "Medias" WHERE "Medias"."noteId" = ${Notes.id})`
: undefined,
user
? sql`NOT EXISTS (SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} AND "Filters"."filter_action" = 'hide' AND EXISTS (SELECT 1 FROM "FilterKeywords" WHERE "FilterKeywords"."filterId" = "Filters"."id" AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%') AND "Filters"."context" @> ARRAY['public'])`

View file

@ -1,6 +1,6 @@
import { apiRoute, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db";
import { Media } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { config } from "~/packages/config-manager/index.ts";
@ -43,7 +43,7 @@ const route = createRoute({
description: "Uploaded media",
content: {
"application/json": {
schema: Attachment.schema,
schema: Media.schema,
},
},
},
@ -70,7 +70,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { file, thumbnail, description } = context.req.valid("form");
const attachment = await Attachment.fromFile(file, {
const attachment = await Media.fromFile(file, {
thumbnail,
description,
});

View file

@ -21,7 +21,7 @@
"@tufjs/canonical-json": "^2.0.0",
"@versia/client": "^0.1.5",
"@versia/federation": "^0.1.4",
"@versia/kit": "packages/plugin-kit",
"@versia/kit": "workspace:*",
"altcha-lib": "^1.2.0",
"blurhash": "^2.0.5",
"bullmq": "^5.35.1",

View file

@ -2,7 +2,7 @@ import { proxyUrl } from "@/response";
import type { Attachment as ApiAttachment } from "@versia/client/types";
import type { ContentFormat } from "@versia/federation/types";
import { db } from "@versia/kit/db";
import { Attachments } from "@versia/kit/tables";
import { Medias } from "@versia/kit/tables";
import {
type InferInsertModel,
type InferSelectModel,
@ -20,9 +20,9 @@ import { MediaManager } from "../media/media-manager.ts";
import { MediaJobType, mediaQueue } from "../queues/media.ts";
import { BaseInterface } from "./base.ts";
type AttachmentType = InferSelectModel<typeof Attachments>;
type MediaType = InferSelectModel<typeof Medias>;
export class Attachment extends BaseInterface<typeof Attachments> {
export class Media extends BaseInterface<typeof Medias> {
public static schema: z.ZodType<ApiAttachment> = z.object({
id: z.string().uuid(),
type: z.enum(["unknown", "image", "gifv", "video", "audio"]),
@ -51,10 +51,10 @@ export class Attachment extends BaseInterface<typeof Attachments> {
blurhash: z.string().nullable(),
});
public static $type: AttachmentType;
public static $type: MediaType;
public async reload(): Promise<void> {
const reloaded = await Attachment.fromId(this.data.id);
const reloaded = await Media.fromId(this.data.id);
if (!reloaded) {
throw new Error("Failed to reload attachment");
@ -63,23 +63,23 @@ export class Attachment extends BaseInterface<typeof Attachments> {
this.data = reloaded.data;
}
public static async fromId(id: string | null): Promise<Attachment | null> {
public static async fromId(id: string | null): Promise<Media | null> {
if (!id) {
return null;
}
return await Attachment.fromSql(eq(Attachments.id, id));
return await Media.fromSql(eq(Medias.id, id));
}
public static async fromIds(ids: string[]): Promise<Attachment[]> {
return await Attachment.manyFromSql(inArray(Attachments.id, ids));
public static async fromIds(ids: string[]): Promise<Media[]> {
return await Media.manyFromSql(inArray(Medias.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({
orderBy: SQL<unknown> | undefined = desc(Medias.id),
): Promise<Media | null> {
const found = await db.query.Medias.findFirst({
where: sql,
orderBy,
});
@ -87,17 +87,17 @@ export class Attachment extends BaseInterface<typeof Attachments> {
if (!found) {
return null;
}
return new Attachment(found);
return new Media(found);
}
public static async manyFromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Attachments.id),
orderBy: SQL<unknown> | undefined = desc(Medias.id),
limit?: number,
offset?: number,
extra?: Parameters<typeof db.query.Attachments.findMany>[0],
): Promise<Attachment[]> {
const found = await db.query.Attachments.findMany({
extra?: Parameters<typeof db.query.Medias.findMany>[0],
): Promise<Media[]> {
const found = await db.query.Medias.findMany({
where: sql,
orderBy,
limit,
@ -105,18 +105,16 @@ export class Attachment extends BaseInterface<typeof Attachments> {
with: extra?.with,
});
return found.map((s) => new Attachment(s));
return found.map((s) => new Media(s));
}
public async update(
newAttachment: Partial<AttachmentType>,
): Promise<AttachmentType> {
public async update(newAttachment: Partial<MediaType>): Promise<MediaType> {
await db
.update(Attachments)
.update(Medias)
.set(newAttachment)
.where(eq(Attachments.id, this.id));
.where(eq(Medias.id, this.id));
const updated = await Attachment.fromId(this.data.id);
const updated = await Media.fromId(this.data.id);
if (!updated) {
throw new Error("Failed to update attachment");
@ -126,26 +124,24 @@ export class Attachment extends BaseInterface<typeof Attachments> {
return updated.data;
}
public save(): Promise<AttachmentType> {
public save(): Promise<MediaType> {
return this.update(this.data);
}
public async delete(ids?: string[]): Promise<void> {
if (Array.isArray(ids)) {
await db.delete(Attachments).where(inArray(Attachments.id, ids));
await db.delete(Medias).where(inArray(Medias.id, ids));
} else {
await db.delete(Attachments).where(eq(Attachments.id, this.id));
await db.delete(Medias).where(eq(Medias.id, this.id));
}
}
public static async insert(
data: InferInsertModel<typeof Attachments>,
): Promise<Attachment> {
const inserted = (
await db.insert(Attachments).values(data).returning()
)[0];
data: InferInsertModel<typeof Medias>,
): Promise<Media> {
const inserted = (await db.insert(Medias).values(data).returning())[0];
const attachment = await Attachment.fromId(inserted.id);
const attachment = await Media.fromId(inserted.id);
if (!attachment) {
throw new Error("Failed to insert attachment");
@ -160,7 +156,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
description?: string;
thumbnail?: File;
},
): Promise<Attachment> {
): Promise<Media> {
if (file.size > config.validation.max_media_size) {
throw new ApiError(
413,
@ -191,17 +187,17 @@ export class Attachment extends BaseInterface<typeof Attachments> {
const { path } = await mediaManager.addFile(file);
const url = Attachment.getUrl(path);
const url = Media.getUrl(path);
let thumbnailUrl = "";
if (options?.thumbnail) {
const { path } = await mediaManager.addFile(options.thumbnail);
thumbnailUrl = Attachment.getUrl(path);
thumbnailUrl = Media.getUrl(path);
}
const newAttachment = await Attachment.insert({
const newAttachment = await Media.insert({
url,
thumbnailUrl: thumbnailUrl || undefined,
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
@ -319,11 +315,11 @@ export class Attachment extends BaseInterface<typeof Attachments> {
public static fromVersia(
attachmentToConvert: ContentFormat,
): Promise<Attachment> {
): Promise<Media> {
const key = Object.keys(attachmentToConvert)[0];
const value = attachmentToConvert[key];
return Attachment.insert({
return Media.insert({
mimeType: key,
url: value.content,
description: value.description || undefined,

View file

@ -16,8 +16,8 @@ import type {
} from "@versia/federation/types";
import { Instance, db } from "@versia/kit/db";
import {
Attachments,
EmojiToNote,
Medias,
NoteToMentions,
Notes,
Users,
@ -44,7 +44,7 @@ import {
import { config } from "~/packages/config-manager";
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
import { Application } from "./application.ts";
import { Attachment } from "./attachment.ts";
import { Media } from "./attachment.ts";
import { BaseInterface } from "./base.ts";
import { Emoji } from "./emoji.ts";
import { User } from "./user.ts";
@ -56,7 +56,7 @@ type NoteTypeWithRelations = NoteType & {
mentions: (InferSelectModel<typeof Users> & {
instance: typeof Instance.$type | null;
})[];
attachments: (typeof Attachment.$type)[];
attachments: (typeof Media.$type)[];
reblog: NoteTypeWithoutRecursiveRelations | null;
emojis: (typeof Emoji.$type)[];
reply: NoteType | null;
@ -102,7 +102,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
sensitive: z.boolean(),
spoiler_text: z.string(),
visibility: z.enum(["public", "unlisted", "private", "direct"]),
media_attachments: z.array(Attachment.schema),
media_attachments: z.array(Media.schema),
mentions: z.array(
z.object({
id: z.string().uuid(),
@ -442,7 +442,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
uri?: string;
mentions?: User[];
/** List of IDs of database Attachment objects */
mediaAttachments?: Attachment[];
mediaAttachments?: Media[];
replyId?: string;
quoteId?: string;
application?: Application;
@ -515,7 +515,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
emojis?: Emoji[];
uri?: string;
mentions?: User[];
mediaAttachments?: Attachment[];
mediaAttachments?: Media[];
replyId?: string;
quoteId?: string;
application?: Application;
@ -623,28 +623,26 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
* Deletes all existing attachments associated with this note, then replaces them with the provided attachments.
* @param mediaAttachments - The IDs of the attachments to associate with this note
*/
public async updateAttachments(
mediaAttachments: Attachment[],
): Promise<void> {
public async updateAttachments(mediaAttachments: Media[]): Promise<void> {
if (mediaAttachments.length === 0) {
return;
}
// Remove old attachments
await db
.update(Attachments)
.update(Medias)
.set({
noteId: null,
})
.where(eq(Attachments.noteId, this.data.id));
.where(eq(Medias.noteId, this.data.id));
await db
.update(Attachments)
.update(Medias)
.set({
noteId: this.data.id,
})
.where(
inArray(
Attachments.id,
Medias.id,
mediaAttachments.map((i) => i.id),
),
);
@ -740,16 +738,16 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
}
}
const attachments: Attachment[] = [];
const attachments: Media[] = [];
for (const attachment of note.attachments ?? []) {
const resolvedAttachment = await Attachment.fromVersia(
attachment,
).catch((e) => {
const resolvedAttachment = await Media.fromVersia(attachment).catch(
(e) => {
logger.error`${e}`;
sentry?.captureException(e);
return null;
});
},
);
if (resolvedAttachment) {
attachments.push(resolvedAttachment);
@ -908,7 +906,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
favourited: data.liked,
favourites_count: data.likeCount,
media_attachments: (data.attachments ?? []).map(
(a) => new Attachment(a).toApi() as ApiAttachment,
(a) => new Media(a).toApi() as ApiAttachment,
),
mentions: data.mentions.map((mention) => ({
id: mention.id,
@ -1014,7 +1012,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
},
},
attachments: (status.attachments ?? []).map((attachment) =>
new Attachment(attachment).toVersia(),
new Media(attachment).toVersia(),
),
is_sensitive: status.sensitive,
mentions: status.mentions.map((mention) =>

View file

@ -1,4 +1,4 @@
import { Attachment } from "@versia/kit/db";
import { Media } from "@versia/kit/db";
import { Worker } from "bullmq";
import { config } from "~/packages/config-manager";
import { connection } from "~/utils/redis.ts";
@ -21,7 +21,7 @@ export const getMediaWorker = (): Worker<MediaJobData, void, MediaJobType> =>
await job.log(`Fetching attachment ID [${attachmentId}]`);
const attachment = await Attachment.fromId(attachmentId);
const attachment = await Media.fromId(attachmentId);
if (!attachment) {
throw new Error(
@ -68,7 +68,7 @@ export const getMediaWorker = (): Worker<MediaJobData, void, MediaJobType> =>
const { path, uploadedFile } =
await mediaManager.addFile(processedFile);
const url = Attachment.getUrl(path);
const url = Media.getUrl(path);
const sha256 = new Bun.SHA256();

View file

@ -1,5 +1,5 @@
import { Args } from "@oclif/core";
import { Attachment, Emoji } from "@versia/kit/db";
import { Emoji, Media } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import chalk from "chalk";
import { and, eq, isNull } from "drizzle-orm";
@ -115,7 +115,7 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
await Emoji.insert({
shortcode: args.shortcode,
url: Attachment.getUrl(uploaded.path),
url: Media.getUrl(uploaded.path),
visibleInPicker: true,
contentType: uploaded.uploadedFile.type,
});
@ -124,7 +124,7 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
`${chalk.green("✓")} Created emoji ${chalk.green(
args.shortcode,
)} with url ${chalk.blue(
chalk.underline(Attachment.getUrl(uploaded.path)),
chalk.underline(Media.getUrl(uploaded.path)),
)}`,
);

View file

@ -1,5 +1,5 @@
import { Args, Flags } from "@oclif/core";
import { Attachment, Emoji } from "@versia/kit/db";
import { Emoji, Media } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import chalk from "chalk";
import { and, inArray, isNull } from "drizzle-orm";
@ -214,7 +214,7 @@ export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
await Emoji.insert({
shortcode: emoji.emoji.name,
url: Attachment.getUrl(uploaded.path),
url: Media.getUrl(uploaded.path),
visibleInPicker: true,
contentType: uploaded.uploadedFile.type,
});

View file

@ -0,0 +1,4 @@
ALTER TABLE "Attachments" RENAME TO "Medias";--> statement-breakpoint
ALTER TABLE "Medias" DROP CONSTRAINT "Attachments_noteId_Notes_id_fk";
--> statement-breakpoint
ALTER TABLE "Medias" ADD CONSTRAINT "Medias_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "public"."Notes"("id") ON DELETE cascade ON UPDATE cascade;

File diff suppressed because it is too large Load diff

View file

@ -288,6 +288,13 @@
"when": 1735776034097,
"tag": "0040_good_nocturne",
"breakpoints": true
},
{
"idx": 41,
"version": "7",
"when": 1737644734501,
"tag": "0041_bright_doctor_spectrum",
"breakpoints": true
}
]
}

View file

@ -303,7 +303,7 @@ export const Tokens = pgTable("Tokens", {
}),
});
export const Attachments = pgTable("Attachments", {
export const Medias = pgTable("Medias", {
id: id(),
url: text("url").notNull(),
remoteUrl: text("remote_url"),
@ -799,9 +799,9 @@ export const UserToPinnedNotes = pgTable(
],
);
export const AttachmentsRelations = relations(Attachments, ({ one }) => ({
export const AttachmentsRelations = relations(Medias, ({ one }) => ({
notes: one(Notes, {
fields: [Attachments.noteId],
fields: [Medias.noteId],
references: [Notes.id],
}),
}));
@ -894,7 +894,7 @@ export const NotesRelations = relations(Notes, ({ many, one }) => ({
references: [Users.id],
relationName: "NoteToAuthor",
}),
attachments: many(Attachments),
attachments: many(Medias),
mentions: many(NoteToMentions),
reblog: one(Notes, {
fields: [Notes.reblogId],

View file

@ -1,7 +1,7 @@
// biome-ignore lint/performance/noBarrelFile: <explanation>
export { User } from "~/classes/database/user.ts";
export { Role } from "~/classes/database/role.ts";
export { Attachment } from "~/classes/database/attachment.ts";
export { Media } from "~/classes/database/attachment.ts";
export { Emoji } from "~/classes/database/emoji.ts";
export { Instance } from "~/classes/database/instance.ts";
export { Note } from "~/classes/database/note.ts";