all repos — stealth-developers @ 445c1b86b7f22d43c98d7b37912e42660b90dd13

db: moreee utils
vi did:web:vt3e.cat
Tue, 23 Jun 2026 00:29:04 +0100
commit

445c1b86b7f22d43c98d7b37912e42660b90dd13

parent

43a6ea6a77ad7a662d493b0f020e52f86b72e076

M pkgs/db/src/index.tspkgs/db/src/index.ts

@@ -2,12 +2,19 @@ import { createClient } from "@libsql/client";

import { drizzle } from "drizzle-orm/libsql"; import config from "@stealth-developers/config"; +import * as schema from "./schemas"; +import * as relations from "./schemas/relations"; + const client = createClient({ url: config.db.url, authToken: config.db.auth, }); -const db = drizzle({ client }); +const db = drizzle({ + client, + schema, + relations: relations.default, +}); export * from "./schemas"; export * from "./utils";
M pkgs/db/src/schemas/index.tspkgs/db/src/schemas/index.ts

@@ -3,5 +3,3 @@ export * from "./actors";

export * from "./guilds"; export * from "./tickets"; - -export * from "./relations";
M pkgs/db/src/schemas/relations.tspkgs/db/src/schemas/relations.ts

@@ -4,7 +4,7 @@ import { users, sessions } from "./users";

import { actors } from "./actors"; import { tickets, ticketParticipants, ticketMessages, ticketAttachments } from "./tickets"; -export const schemaRelations = defineRelations( +export default defineRelations( { guilds, users,
A pkgs/db/src/utils/guild.ts

@@ -0,0 +1,45 @@

+import { eq } from "drizzle-orm"; +import db, { guilds } from ".."; + +export async function upsertGuild(guild: { discordId: string; name: string; icon: string | null }) { + const [upsertedGuild] = await db + .insert(guilds) + .values({ + discordId: guild.discordId, + name: guild.name, + icon: guild.icon, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: guilds.discordId, + set: { + name: guild.name, + icon: guild.icon, + updatedAt: new Date(), + }, + }) + .returning(); + + return upsertedGuild; +} + +export async function getGuild(id: number) { + const [result] = await db.select().from(guilds).where(eq(guilds.id, id)); + return result; +} + +export async function getGuildByDiscordId(discordId: string) { + const [result] = await db.select().from(guilds).where(eq(guilds.discordId, discordId)); + return result; +} + +export async function updateGuild(id: number, guild: Partial<typeof guilds.$inferInsert>) { + const [updatedGuild] = await db + .update(guilds) + .set({ ...guild, updatedAt: new Date() }) + .where(eq(guilds.id, id)) + .returning(); + + return updatedGuild; +}
M pkgs/db/src/utils/index.tspkgs/db/src/utils/index.ts

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

-import { eq, and } from "drizzle-orm"; -import db, { actors, sessions, users, guilds, tickets, type Ticket } from ".."; - -export async function getUser(id: number) { - const [result] = await db.select().from(users).where(eq(users.id, id)); - return result; -} - -export async function getUserByDiscordId(discordId: string) { - const [result] = await db.select().from(users).where(eq(users.discordId, discordId)); - return result; -} - -export async function getActor(id: number) { - const [result] = await db.select().from(actors).where(eq(actors.id, id)); - return result; -} - -export async function getActorByDiscordId(discordId: string, guildId: number) { - const [result] = await db - .select() - .from(actors) - .where(and(eq(actors.discordId, discordId), eq(actors.guildId, guildId))); - return result; -} - -export async function getActorByDiscordIdAndGuildDiscordId( - discordId: string, - guildDiscordId: string, -) { - const [guild] = await db.select().from(guilds).where(eq(guilds.discordId, guildDiscordId)); - if (!guild) return null; - return getActorByDiscordId(discordId, guild.id); -} - -export async function getUserSessions(id: number) { - const result = await db.select().from(sessions).where(eq(sessions.userId, id)); - return result; -} - -export async function getSession(id: string) { - const [result] = await db.select().from(sessions).where(eq(sessions.id, id)); - return result; -} - -export async function upsertGuild(guild: { discordId: string; name: string; icon: string | null }) { - const [upsertedGuild] = await db - .insert(guilds) - .values({ - discordId: guild.discordId, - name: guild.name, - icon: guild.icon, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: guilds.discordId, - set: { - name: guild.name, - icon: guild.icon, - updatedAt: new Date(), - }, - }) - .returning(); - - return upsertedGuild; -} - -export async function upsertUser(user: { - username: string; - displayName: string | null; - id: string; - avatar: string | null; -}) { - const [upsertedUser] = await db - .insert(users) - .values({ - discordId: user.id, - username: user.username, - displayName: user.displayName, - avatar: user.avatar, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: users.discordId, - set: { - username: user.username, - displayName: user.displayName, - avatar: user.avatar, - updatedAt: new Date(), - }, - }) - .returning(); - - return upsertedUser; -} - -export async function upsertActor(data: { - guild: { - discordId: string; - name: string; - icon: string | null; - }; - user: { - discordId: string; - username: string; - displayName: string | null; - avatar: string | null; - }; - moderator: boolean; -}) { - const dbGuild = await upsertGuild(data.guild); - const dbUser = await upsertUser({ - id: data.user.discordId, - username: data.user.username, - displayName: data.user.displayName, - avatar: data.user.avatar, - }); - - if (!dbGuild || !dbUser) { - throw new Error("Failed to upsert guild or user"); - } - - const [actor] = await db - .insert(actors) - .values({ - guildId: dbGuild.id, - userId: dbUser.id, - discordId: data.user.discordId, - username: data.user.username, - displayName: data.user.displayName, - avatar: data.user.avatar, - moderator: data.moderator, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: [actors.guildId, actors.userId], - set: { - username: data.user.username, - displayName: data.user.displayName, - avatar: data.user.avatar, - moderator: data.moderator, - updatedAt: new Date(), - }, - }) - .returning(); - - return actor; -} - -export async function anonymiseUser(id: number) { - const result = await db - .update(actors) - .set({ - username: `Anonymised User`, - displayName: null, - avatar: null, - updatedAt: new Date(), - }) - .where(eq(actors.id, id)) - .returning(); - - return result; -} - -export async function getGuild(id: number) { - const [result] = await db.select().from(guilds).where(eq(guilds.id, id)); - return result; -} - -export async function getGuildByDiscordId(discordId: string) { - const [result] = await db.select().from(guilds).where(eq(guilds.discordId, discordId)); - return result; -} - -export async function getTicket(ticketId: string): Promise<Ticket | undefined>; -export async function getTicket(guildId: number, ticketId: number): Promise<Ticket | undefined>; - -export async function getTicket( - idOrGuildId: string | number, - ticketId?: number, -): Promise<Ticket | undefined> { - return typeof idOrGuildId === "string" - ? getTicketById(idOrGuildId) - : getTicketByGuild(idOrGuildId, ticketId); -} -async function getTicketById(id: string) { - const [ticket] = await db.select().from(tickets).where(eq(tickets.id, id)); - return ticket; -} -async function getTicketByGuild(guildId: number, localId?: number) { - if (localId === undefined) return undefined; - const [ticket] = await db - .select() - .from(tickets) - .where(and(eq(tickets.guildId, guildId), eq(tickets.localId, localId))); - return ticket; -} - -export async function editTicket(ticketId: string, data: Partial<Ticket>) { - const [result] = await db.update(tickets).set(data).where(eq(tickets.id, ticketId)).returning(); - return result; -} +export * from "./guild"; +export * from "./session"; +export * from "./ticket"; +export * from "./user";
A pkgs/db/src/utils/session.ts

@@ -0,0 +1,43 @@

+import db, { eq } from ".."; +import { sessions } from "../schemas"; + +export async function getUserSessions(id: number) { + const result = await db.select().from(sessions).where(eq(sessions.userId, id)); + return result; +} + +export async function getSession(id: string) { + const [result] = await db.select().from(sessions).where(eq(sessions.id, id)); + return result; +} + +export async function createSession(data: { + id: string; + userId: number; + accessToken: string; + refreshToken: string; + scope?: string | null; + tokenType?: string | null; + expiresAt: Date; +}) { + const [session] = await db + .insert(sessions) + .values({ + id: data.id, + userId: data.userId, + accessToken: data.accessToken, + refreshToken: data.refreshToken, + scope: data.scope, + tokenType: data.tokenType, + expiresAt: data.expiresAt, + createdAt: new Date(), + }) + .returning(); + + return session; +} + +export async function deleteSession(id: string) { + const [deleted] = await db.delete(sessions).where(eq(sessions.id, id)).returning(); + return deleted; +}
A pkgs/db/src/utils/ticket.ts

@@ -0,0 +1,263 @@

+import { randomUUID } from "node:crypto"; +import { and, desc, eq } from "drizzle-orm"; +import db, { + ticketMessages, + ticketAttachments, + ticketParticipants, + tickets, + type Ticket, +} from ".."; + +// fetch +export async function getTicket(ticketId: string): Promise<Ticket | undefined>; +export async function getTicket(guildId: number, ticketId: number): Promise<Ticket | undefined>; + +export async function getTicket( + idOrGuildId: string | number, + ticketId?: number, +): Promise<Ticket | undefined> { + return typeof idOrGuildId === "string" + ? getTicketById(idOrGuildId) + : getTicketByGuild(idOrGuildId, ticketId); +} +export async function getTicketById(id: string) { + const [ticket] = await db.select().from(tickets).where(eq(tickets.id, id)); + return ticket; +} +export async function getTicketByGuild(guildId: number, localId?: number) { + if (localId === undefined) return undefined; + const [ticket] = await db + .select() + .from(tickets) + .where(and(eq(tickets.guildId, guildId), eq(tickets.localId, localId))); + return ticket; +} +export async function getTicketByChannelId(channelId: string) { + const [ticket] = await db.select().from(tickets).where(eq(tickets.channelId, channelId)); + return ticket; +} + +export async function editTicket(ticketId: string, data: Partial<Ticket>) { + const [result] = await db.update(tickets).set(data).where(eq(tickets.id, ticketId)).returning(); + return result; +} + +// standard ops +export async function createTicket(data: { + guildId: number; + openedBy: number; + subject: number; + status: string; + channelId?: string | null; + openReason?: string | null; +}) { + const id = randomUUID(); + + return await db.transaction(async (tx) => { + const [lastTicket] = await tx + .select({ localId: tickets.localId }) + .from(tickets) + .where(eq(tickets.guildId, data.guildId)) + .orderBy(desc(tickets.localId)) + .limit(1); + + const nextLocalId = lastTicket ? lastTicket.localId + 1 : 1; + + const [newTicket] = await tx + .insert(tickets) + .values({ + id: id, + guildId: data.guildId, + localId: nextLocalId, + status: data.status, + channelId: data.channelId, + openedBy: data.openedBy, + subject: data.subject, + openReason: data.openReason, + openedAt: new Date(), + }) + .returning(); + + if (!newTicket) throw new Error("Failed to create ticket"); + + await tx.insert(ticketParticipants).values({ + ticketId: newTicket.id, + actorId: data.openedBy, + role: data.openedBy === data.subject ? "subject" : "participant", + createdAt: new Date(), + }); + + return newTicket; + }); +} + +export async function closeTicket(data: { + ticketId: string; + closedBy: number; + reason?: string | null; + status?: string; +}) { + const [closedTicket] = await db + .update(tickets) + .set({ + status: data.status ?? "closed", + closedAt: new Date(), + closedBy: data.closedBy, + closeReason: data.reason, + }) + .where(eq(tickets.id, data.ticketId)) + .returning(); + + return closedTicket; +} + +export async function claimTicket(ticketId: string, actorId: number) { + const [ticket] = await db + .update(tickets) + .set({ + claimedBy: actorId, + claimedAt: new Date(), + }) + .where(eq(tickets.id, ticketId)) + .returning(); + + return ticket; +} + +export async function unclaimTicket(ticketId: string) { + const [ticket] = await db + .update(tickets) + .set({ + claimedBy: null, + claimedAt: null, + }) + .where(eq(tickets.id, ticketId)) + .returning(); + + return ticket; +} + +// messages +export async function createTicketMessageWithAttachments(data: { + ticketId: string; + actorId: number; + publicId: string; + messageId?: string | null; + content: string; + actorRole: "participant" | "moderator" | "subject"; + attachments?: { + publicId: string; + fileName: string; + fileType: string; + fileSize: number; + }[]; +}) { + return await db.transaction(async (tx) => { + const [message] = await tx + .insert(ticketMessages) + .values({ + publicId: data.publicId, + messageId: data.messageId, + ticketId: data.ticketId, + actorId: data.actorId, + content: data.content, + actorRole: data.actorRole, + createdAt: new Date(), + }) + .returning(); + + if (data.attachments && data.attachments.length > 0) { + await tx.insert(ticketAttachments).values( + data.attachments.map((att) => ({ + publicId: att.publicId, + messageId: message?.id, + ticketId: data.ticketId, + actorId: data.actorId, + fileName: att.fileName, + fileType: att.fileType, + fileSize: att.fileSize, + })), + ); + } + + const ticketUpdate: Partial<Ticket> = { + lastMessageAt: new Date(), + }; + + if (data.actorRole === "moderator") { + ticketUpdate.hasModeratorMessage = true; + } else if (data.actorRole === "subject") { + ticketUpdate.hasAuthorMessage = true; + } + + await tx.update(tickets).set(ticketUpdate).where(eq(tickets.id, data.ticketId)); + + return message; + }); +} + +export async function getTicketTranscript(ticketId: string) { + return await db.query.tickets.findFirst({ + where: { + id: ticketId, + }, + with: { + opener: true, + subjectActor: true, + claimedByActor: true, + participants: { + with: { + actor: true, + }, + }, + messages: { + orderBy: (messages, { asc }) => [asc(messages.createdAt)], + with: { + actor: true, + attachments: true, + }, + }, + }, + }); +} + +// participants +export async function addTicketParticipant(data: { + ticketId: string; + actorId: number; + role: "participant" | "moderator" | "subject"; + reason?: string | null; +}) { + const [participant] = await db + .insert(ticketParticipants) + .values({ + ticketId: data.ticketId, + actorId: data.actorId, + role: data.role, + reason: data.reason, + createdAt: new Date(), + }) + .returning(); + + return participant; +} + +export async function removeTicketParticipant(ticketId: string, actorId: number) { + const [removed] = await db + .delete(ticketParticipants) + .where(and(eq(ticketParticipants.ticketId, ticketId), eq(ticketParticipants.actorId, actorId))) + .returning(); + + return removed; +} + +export async function getTicketParticipants(ticketId: string) { + return await db.query.ticketParticipants.findMany({ + where: { + ticketId: ticketId, + }, + with: { + actor: true, + }, + }); +}
A pkgs/db/src/utils/user.ts

@@ -0,0 +1,134 @@

+import db, { eq, and } from ".."; +import { actors, guilds, users } from "../schemas"; +import { upsertGuild } from "./guild"; + +export async function upsertUser(user: { + username: string; + displayName: string | null; + id: string; + avatar: string | null; +}) { + const [upsertedUser] = await db + .insert(users) + .values({ + discordId: user.id, + username: user.username, + displayName: user.displayName, + avatar: user.avatar, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: users.discordId, + set: { + username: user.username, + displayName: user.displayName, + avatar: user.avatar, + updatedAt: new Date(), + }, + }) + .returning(); + + return upsertedUser; +} + +export async function upsertActor(data: { + guild: { + discordId: string; + name: string; + icon: string | null; + }; + user: { + discordId: string; + username: string; + displayName: string | null; + avatar: string | null; + }; + moderator: boolean; +}) { + const dbGuild = await upsertGuild(data.guild); + const dbUser = await upsertUser({ + id: data.user.discordId, + username: data.user.username, + displayName: data.user.displayName, + avatar: data.user.avatar, + }); + + if (!dbGuild || !dbUser) { + throw new Error("Failed to upsert guild or user"); + } + + const [actor] = await db + .insert(actors) + .values({ + guildId: dbGuild.id, + userId: dbUser.id, + discordId: data.user.discordId, + username: data.user.username, + displayName: data.user.displayName, + avatar: data.user.avatar, + moderator: data.moderator, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: [actors.guildId, actors.userId], + set: { + username: data.user.username, + displayName: data.user.displayName, + avatar: data.user.avatar, + moderator: data.moderator, + updatedAt: new Date(), + }, + }) + .returning(); + + return actor; +} + +export async function anonymiseUser(id: number) { + const result = await db + .update(actors) + .set({ + username: `Anonymised User`, + displayName: null, + avatar: null, + updatedAt: new Date(), + }) + .where(eq(actors.id, id)) + .returning(); + + return result; +} + +export async function getUser(id: number) { + const [result] = await db.select().from(users).where(eq(users.id, id)); + return result; +} + +export async function getUserByDiscordId(discordId: string) { + const [result] = await db.select().from(users).where(eq(users.discordId, discordId)); + return result; +} + +export async function getActor(id: number) { + const [result] = await db.select().from(actors).where(eq(actors.id, id)); + return result; +} + +export async function getActorByDiscordId(discordId: string, guildId: number) { + const [result] = await db + .select() + .from(actors) + .where(and(eq(actors.discordId, discordId), eq(actors.guildId, guildId))); + return result; +} + +export async function getActorByDiscordIdAndGuildDiscordId( + discordId: string, + guildDiscordId: string, +) { + const [guild] = await db.select().from(guilds).where(eq(guilds.discordId, guildDiscordId)); + if (!guild) return null; + return getActorByDiscordId(discordId, guild.id); +}