all repos — stealth-developers @ 2a969de7ae6d1926b61eea5c14ce8c9971bcd579

feat: bug schemas; use new common attachment schema
vi v@vt3e.cat
Thu, 26 Feb 2026 02:54:54 +0000
commit

2a969de7ae6d1926b61eea5c14ce8c9971bcd579

parent

960f8f74d3c05db34f143b02ffb9180777ed6380

A src/database/schema/attachments.ts

@@ -0,0 +1,33 @@

+import { sql } from "drizzle-orm"; +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const attachments = sqliteTable( + "attachments", + { + id: text("id").primaryKey(), + + ownerType: text("owner_type", { + enum: ["ticket", "ticket_message", "bug"], + }).notNull(), + ownerId: integer("owner_id").notNull(), + + authorId: text("author_id"), + fileName: text("file_name").notNull(), + contentType: text("content_type").notNull(), + size: integer("size").notNull(), + + bucket: text("bucket").notNull(), + key: text("key").notNull(), + url: text("url").notNull(), + + uploadedAt: integer("uploaded_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + }, + (t) => [ + index("attachments_owner_idx").on(t.ownerType, t.ownerId), + index("attachments_author_idx").on(t.authorId), + ], +); + +export type Attachment = typeof attachments.$inferSelect;
A src/database/schema/bugs.ts

@@ -0,0 +1,44 @@

+import { defineRelations, sql } from "drizzle-orm"; +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const bugs = sqliteTable( + "bugs", + { + id: integer("id").primaryKey({ autoIncrement: true }), + guildId: text("guild_id").notNull(), + bugNumber: integer("bug_number").notNull(), + + authorId: text("author_id").notNull(), + status: text("status", { enum: ["open", "closed"] }) + .default("open") + .notNull(), + + title: text("title").notNull(), + description: text("description").notNull(), + + projects: text("projects", { mode: "json" }) + .$type<string[]>() + .default([]) + .notNull(), + + sent: integer("sent", { mode: "boolean" }).default(false).notNull(), + messageId: text("message_id"), + threadId: text("thread_id"), + + createdAt: integer("created_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + closedAt: integer("closed_at", { mode: "timestamp" }), + }, + (t) => [ + index("bugs_guild_idx").on(t.guildId), + index("bugs_author_idx").on(t.authorId), + index("bugs_status_idx").on(t.status), + index("bugs_guild_number_idx").on(t.guildId, t.bugNumber), + ], +); + +export type Bug = typeof bugs.$inferSelect;
M src/database/schema/index.tssrc/database/schema/index.ts

@@ -1,2 +1,4 @@

export * from "./tickets"; export * from "./guild"; +export * from "./attachments"; +export * from "./bugs";
M src/database/schema/tickets.tssrc/database/schema/tickets.ts

@@ -59,63 +59,19 @@ },

(t) => [index("ticket_msgs_ticket_idx").on(t.ticketId)], ); -export const ticketAttachments = sqliteTable("ticket_attachments", { - id: text("id").primaryKey(), - ticketId: integer("ticket_id") - .references(() => tickets.id, { onDelete: "cascade" }) - .notNull(), - messageId: integer("message_id").references(() => ticketMessages.id, { - onDelete: "cascade", - }), - - fileName: text("file_name").notNull(), - contentType: text("content_type").notNull(), - size: integer("size").notNull(), - - bucket: text("bucket").notNull(), - key: text("key").notNull(), - url: text("url").notNull(), - - uploadedAt: integer("uploaded_at", { mode: "timestamp" }) - .default(sql`(unixepoch())`) - .notNull(), -}); - export const TicketRelations = defineRelations( - { - tickets, - ticketMessages, - ticketAttachments, - }, + { tickets, ticketMessages }, (r) => ({ tickets: { messages: r.many.ticketMessages({ from: r.tickets.id, to: r.ticketMessages.ticketId, }), - attachments: r.many.ticketAttachments({ - from: r.tickets.id, - to: r.ticketAttachments.ticketId, - }), }, ticketMessages: { ticket: r.one.tickets({ from: r.ticketMessages.ticketId, to: r.tickets.id, - }), - attachments: r.many.ticketAttachments({ - from: r.ticketMessages.id, - to: r.ticketAttachments.messageId, - }), - }, - ticketAttachments: { - ticket: r.one.tickets({ - from: r.ticketAttachments.ticketId, - to: r.tickets.id, - }), - message: r.one.ticketMessages({ - from: r.ticketAttachments.messageId, - to: r.ticketMessages.id, }), }, }),

@@ -123,4 +79,3 @@ );

export type Ticket = typeof tickets.$inferSelect; export type TicketMessage = typeof ticketMessages.$inferSelect; -export type ticketAttachment = typeof ticketAttachments.$inferSelect;
M src/events/messageCreate.tssrc/events/messageCreate.ts

@@ -1,4 +1,4 @@

-import { db, ticketAttachments, ticketMessages, tickets } from "@/database"; +import { attachments, db, ticketMessages, tickets } from "@/database"; import { getPublicUrl, s3 } from "@/utils/s3"; import { ActionRowBuilder,

@@ -126,16 +126,17 @@ const blob = await resp.blob();

await file.write(blob); const publicUrl = getPublicUrl(key); - await db.insert(ticketAttachments).values({ + + await db.insert(attachments).values({ id: attachment.id, - ticketId: ticket.id, - messageId: savedMessage.id, + ownerType: "ticket_message", + ownerId: savedMessage.id, fileName: attachment.name, contentType: attachment.contentType ?? "application/octet-stream", size: attachment.size, bucket: config.data.s3.bucket, key: key, - url: attachment.url, + url: publicUrl, }); statusList[i].status = "done";
M src/interactions/buttons/tickets/ticket.tssrc/interactions/buttons/tickets/ticket.ts

@@ -24,11 +24,11 @@ import { ModalBuilder } from "discord.js";

import { eq, sql } from "drizzle-orm"; import { + type Attachment, type Guild, type Ticket, db, guild, - type ticketAttachment, tickets, } from "@/database"; import { makeComponentId, parseComponentId } from "@/utils/components";

@@ -262,7 +262,7 @@ });

const attachmentsString = attachments .map( - (att: ticketAttachment, idx: number) => + (att: Attachment, idx: number) => `[Attachment #${idx + 1} (${att.fileName.split(".").slice(-1)[0].toUpperCase()})](<${getPublicUrl(att.key)}>)`, ) .join(", ");
M src/utils/transcripts.tssrc/utils/transcripts.ts

@@ -1,31 +1,37 @@

import { + type Attachment, type Ticket, + attachments, db, - type ticketAttachment, - ticketAttachments, ticketMessages, } from "@/database"; import { getPublicUrl } from "@/utils/s3"; -import { asc, eq } from "drizzle-orm"; +import { and, asc, eq } from "drizzle-orm"; export async function generateTranscript( ticket: Ticket, -): Promise<[string, ticketAttachment[]]> { +): Promise<[string, Attachment[]]> { const messages = await db .select() .from(ticketMessages) .where(eq(ticketMessages.ticketId, ticket.id)) .orderBy(asc(ticketMessages.createdAt)); - const attachments = await db + const messageIds = messages.map((m) => m.id); + + const ticketAttachments = await db .select() - .from(ticketAttachments) - .where(eq(ticketAttachments.ticketId, ticket.id)); + .from(attachments) + .where(and(eq(attachments.ownerType, "ticket_message"))); - const attachmentMap = new Map<number, typeof attachments>(); - for (const att of attachments) { - const existing = attachmentMap.get(att.messageId ?? 0) ?? []; - attachmentMap.set(att.messageId ?? 0, [...existing, att]); + const relevantAttachments = ticketAttachments.filter((att) => + messageIds.includes(att.ownerId), + ); + + const attachmentMap = new Map<number, Attachment[]>(); + for (const att of relevantAttachments) { + const existing = attachmentMap.get(att.ownerId) ?? []; + attachmentMap.set(att.ownerId, [...existing, att]); } const lines = messages.map((msg) => {

@@ -62,5 +68,5 @@ separator,

"\n", ].join("\n"); - return [header + lines.join("\n"), attachments]; + return [header + lines.join("\n"), relevantAttachments]; }