moving to postgres, part 1

This commit is contained in:
2025-09-17 17:51:22 +02:00
parent 8f87908505
commit 223add151c
19 changed files with 425 additions and 290 deletions

View File

@@ -1,75 +1,18 @@
import type { AccessToken } from "@twurple/auth";
import PocketBase, { RecordService } from "pocketbase";
import type { inventory } from "items";
import logger from "lib/logger";
import * as schema from "db/schema";
const pocketbaseurl = process.env.POCKETBASE_URL ?? "localhost:8090";
if (pocketbaseurl === "") { logger.enverr("POCKETBASE_URL"); process.exit(1); };
// const host = process.env.POSTGRES_HOST ?? "";
// if (!host) { logger.enverr("POSTGRES_HOST"); process.exit(1); };
//
// const user = process.env.POSTGRES_USER ?? "";
// if (!user) { logger.enverr("POSTGRES_USER"); process.exit(1); };
// const password = process.env.POSTGRES_PASSWORD ?? "";
// if (!password) { logger.enverr("POSTGRES_USER"); process.exit(1); };
// const database = process.env.POSTGRES_DB ?? "twitchbot";
//
// const connection = { host, user, password, database };
const url = `postgresql://admin:abcdefgh@localhost:5432/twitchbot`;
export type authRecord = {
id: string;
accesstoken: AccessToken;
};
export type userRecord = {
id: string;
username: string; // Don't use this, Use User.username or User.displayName. This is just to make the pocketbase data easier to read.
balance: number;
inventory: inventory;
lastlootbox: string;
};
export type usedItemRecord = {
id?: string;
user: string;
item: string;
created: string;
};
export type timeoutRecord = {
id?: string;
user: string;
target: string;
item: string;
created: string;
};
export type cheerEventRecord = {
id?: string;
user: string;
cheer: string;
created: string;
};
export type cheerRecord = {
id?: string;
user: string;
amount: number;
};
export type anivTimeoutRecord = {
id?: string;
message: string;
user: string;
duration: number;
};
export type getLootRecord = {
id?: string;
user: string;
qbucks: number;
items: inventory;
};
interface TypedPocketBase extends PocketBase {
collection(idOrName: 'auth'): RecordService<authRecord>;
collection(idOrName: 'users'): RecordService<userRecord>;
collection(idOrName: 'usedItems'): RecordService<usedItemRecord>;
collection(idOrName: 'timeouts'): RecordService<timeoutRecord>;
collection(idOrName: 'cheerEvents'): RecordService<cheerEventRecord>;
collection(idOrName: 'cheers'): RecordService<cheerRecord>;
collection(idOrName: 'anivTimeouts'): RecordService<anivTimeoutRecord>;
collection(idOrName: 'getLoots'): RecordService<getLootRecord>;
};
export default new PocketBase(pocketbaseurl).autoCancellation(false) as TypedPocketBase;
import { drizzle } from 'drizzle-orm/bun-sql';
export default drizzle(url, {
schema
});

View File

@@ -1,14 +1,13 @@
import pocketbase from "db/connection";
import db from "db/connection";
import User from "user";
import logger from "lib/logger";
import { anivTimeouts } from "db/schema";
import { type anivBots } from "lib/handleAnivMessage";
const pb = pocketbase.collection('anivTimeouts');
export async function createAnivTimeoutRecord(message: string, user: User, duration: number) {
try {
await pb.create({ message, user: user.id, duration });
} catch (e) {
logger.err(`Failed to create anivTimeoutRecord: user: ${user.displayName} message: "${message}" duration: ${duration}`);
logger.err(e as string);
};
export async function createAnivTimeoutRecord(message: string, anivbot: anivBots, user: User, duration: number) {
await db.insert(anivTimeouts).values({
message,
anivBot: anivbot,
user: parseInt(user.id),
duration
});
};

View File

@@ -1,38 +1,28 @@
import type { AccessToken } from "@twurple/auth";
import pocketbase, { type authRecord } from "db/connection";
const pb = pocketbase.collection('auth');
import db from "db/connection";
import { auth } from "db/schema";
import { eq } from "drizzle-orm";
export async function createAuthRecord(token: AccessToken, userId: string) {
try {
const data: authRecord = {
accesstoken: token,
id: userId
};
await pb.create(data);
} catch (err) { };
await db.insert(auth).values({
id: parseInt(userId),
accesstoken: token
});
};
export async function getAuthRecord(userId: string, requiredIntents: string[]) {
try {
const data = await pb.getOne(userId);
if (!requiredIntents.every(intent => data.accesstoken.scope.includes(intent))) return undefined;
return { accesstoken: data.accesstoken };
} catch (err) {
return undefined;
};
const data = await db.query.auth.findFirst({
where: eq(auth.id, parseInt(userId))
});
if (!data) return undefined;
if (!requiredIntents.every(intent => data.accesstoken.scope.includes(intent))) return undefined;
return { accesstoken: data.accesstoken };
};
export async function updateAuthRecord(userId: string, newtoken: AccessToken) {
try {
const newrecord = {
accesstoken: newtoken,
};
await pb.update(userId, newrecord);
} catch (err) { };
await db.update(auth).set({ accesstoken: newtoken }).where(eq(auth.id, parseInt(userId)));
};
export async function deleteAuthRecord(userId: string): Promise<void> {
try {
await pb.delete(userId);
} catch (err) { };
await db.delete(auth).where(eq(auth.id, parseInt(userId)));
};

View File

@@ -1,26 +1,20 @@
import pocketbase from "db/connection";
import db from "db/connection";
import { cheerEvents } from "db/schema";
import { and, between, eq, SQL } from "drizzle-orm";
import type { items } from "items";
import User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('cheerEvents');
export async function createCheerEventRecord(user: User, cheer: string): Promise<void> {
try {
await pb.create({ user: user.id, cheer });
} catch (e) {
logger.err(`Failed to create cheerEvent record in database: user: ${user.id}, cheer: ${cheer}`);
logger.err(e as string);
};
export async function createCheerEventRecord(user: User, cheer: items): Promise<void> {
await db.insert(cheerEvents).values({ user: parseInt(user.id), event: cheer });
};
export async function getCheerEvents(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get cheerEvents for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(cheerEvents.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(cheerEvents.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(cheerEvents).where(condition);
return data;
};

View File

@@ -1,26 +1,19 @@
import pocketbase from "db/connection";
import db from "db/connection";
import { cheers } from "db/schema";
import User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('cheers');
import { and, between, eq, SQL } from "drizzle-orm";
export async function createCheerRecord(user: User, amount: number): Promise<void> {
try {
await pb.create({ user: user.id, amount })
} catch (e) {
logger.err(`Failed to create cheer record in database: user: ${user.id}, amount: ${amount}`);
logger.err(e as string);
};
await db.insert(cheers).values({ user: parseInt(user.id), amount });
};
export async function getCheers(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get cheers for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(cheers.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(cheers.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(cheers).where(condition);
return data;
};

View File

@@ -1,19 +1,13 @@
import pocketbase from "db/connection";
import db from "db/connection";
import { getLoots } from "db/schema";
import type { inventory } from "items";
import logger from "lib/logger";
import type User from "user";
const pb = pocketbase.collection('getLoots');
export async function createGetLootRecord(user: User, qbucks: number, inventory: inventory) {
try {
await pb.create({
user: user.id,
qbucks,
items: inventory
});
} catch (e) {
logger.err(`Failed to create getLoot record for ${user.displayName}: ${inventory}`);
logger.err(e as string);
};
await db.insert(getLoots).values({
user: parseInt(user.id),
qbucks: qbucks,
items: inventory
});
};

View File

@@ -1,39 +1,35 @@
import pocketbase from "db/connection";
import db from "db/connection";
import { timeouts } from "db/schema";
import User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('timeouts');
import type { items } from "items";
import { and, between, eq, type SQL } from "drizzle-orm";
export async function createTimeoutRecord(user: User, target: User, item: string): Promise<void> {
try {
await pb.create({ user: user.id, target: target.id, item });
} catch (err) {
logger.err(`Failed to create timeout record in database: user: ${user.id}, target: ${target.id}, item: ${item}`);
logger.err(err as string);
};
export async function createTimeoutRecord(user: User, target: User, item: items): Promise<void> {
await db.insert(timeouts).values({
user: parseInt(user.id),
target: parseInt(target.id),
item
});
};
export async function getTimeoutsAsUser(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get timeouts as user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(timeouts.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(timeouts).where(condition);
return data;
};
export async function getTimeoutsAsTarget(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `target="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get timeouts as target: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(timeouts.target, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(timeouts).where(condition);
return data;
};

View File

@@ -1,27 +1,20 @@
import pocketbase from "db/connection";
import db from "db/connection";
import { usedItems } from "db/schema";
import User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('usedItems');
import type { items } from "items";
import { and, between, eq, type SQL } from "drizzle-orm";
export async function createUsedItemRecord(user: User, item: string): Promise<void> {
try {
await pb.create({ user: user.id, item });
} catch (err) {
logger.err(`Failed to create usedItem record in database: user: ${user.id}, item: ${item}`);
logger.err(err as string);
};
export async function createUsedItemRecord(user: User, item: items): Promise<void> {
await db.insert(usedItems).values({ user: parseInt(user.id), item });
};
export async function getItemsUsed(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get items used for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(usedItems.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(usedItems.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(usedItems).where(condition);
return data;
};

View File

@@ -1,58 +1,47 @@
import pocketbase, { type userRecord } from "db/connection";
import { emptyInventory, itemarray } from "items";
import db from "db/connection";
import { users } from "db/schema";
import { itemarray, type inventory } from "items";
import type User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('users');
import { desc, eq } from "drizzle-orm";
/** Use this function to both ensure existance and to retreive data */
export async function getUserRecord(user: User): Promise<userRecord> {
try {
const data = await pb.getOne(user.id);
export async function getUserRecord(user: User) {
const data = await db.query.users.findFirst({ where: eq(users.id, parseInt(user.id)) });
if (!data) return createUserRecord(user);
if (Object.keys(data.inventory).sort().toString() !== itemarray.sort().toString()) { // If the items in the user inventory are missing an item.
itemarray.forEach(key => {
if (!(key in data.inventory)) data.inventory[key] = 0;
});
};
return data;
} catch (err) {
// This gets triggered if the user doesn't exist in the database
return await createUserRecord(user);
if (Object.keys(data.inventory).sort().toString() !== itemarray.sort().toString()) { // If the items in the user inventory are missing an item.
itemarray.forEach(key => {
if (!(key in data.inventory)) data.inventory[key] = 0;
});
};
};
export async function getAllUserRecords(): Promise<userRecord[]> {
return await pb.getFullList();
};
async function createUserRecord(user: User): Promise<userRecord> {
const data = await pb.create({
id: user.id,
username: user.username,
balance: 0,
inventory: emptyInventory,
lastlootbox: new Date(0).toISOString()
});
return data;
};
export async function updateUserRecord(user: User, newData: userRecord): Promise<boolean> {
try {
await pb.update(user.id, newData);
return true;
} catch (err) {
logger.err(err as string);
return false;
};
export async function getAllUserRecords() {
return await db.select().from(users);
};
async function createUserRecord(user: User) {
return await db.insert(users).values({
id: parseInt(user.id),
username: user.username
}).returning().then(a => {
if (!a[0]) throw Error('Something went horribly wrong');
return a[0]
});
};
export type balanceUpdate = { balance: number; };
export type inventoryUpdate = { inventory: inventory; };
type updateUser = balanceUpdate | inventoryUpdate;
export async function updateUserRecord(user: User, newData: updateUser) {
await db.update(users).set(newData).where(eq(users.id, parseInt(user.id)));
return true;
};
export async function getBalanceLeaderboard() {
try {
return await pb.getList(1, 10, { sort: '-balance,id' }).then(a => a.items);
} catch (err) {
logger.err(err as string);
};
return await db.select().from(users).orderBy(desc(users.balance)).limit(10);
};

64
src/db/schema.ts Normal file
View File

@@ -0,0 +1,64 @@
import type { AccessToken } from "@twurple/auth";
import type { inventory, items } from "items";
import { integer, jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
import type { anivBots } from "lib/handleAnivMessage";
export const auth = pgTable('auth', {
id: integer().primaryKey(),
accesstoken: jsonb().$type<AccessToken>().notNull()
});
export const users = pgTable('users', {
id: integer().primaryKey().notNull(),
username: varchar().notNull(),
balance: integer().default(0).notNull(),
inventory: jsonb().$type<inventory>().default({}).notNull(),
lastlootbox: timestamp().default(new Date(0)).notNull()
});
export const timeouts = pgTable('timeouts', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
target: integer().notNull().references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const usedItems = pgTable('usedItems', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const cheerEvents = pgTable('cheerEvents', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
event: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const cheers = pgTable('cheers', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
amount: integer().notNull(),
created: timestamp().defaultNow().notNull()
});
export const anivTimeouts = pgTable('anivTimeouts', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
message: varchar().notNull(),
anivBot: varchar().$type<anivBots>().notNull(),
duration: integer().notNull(),
created: timestamp().defaultNow().notNull()
});
export const getLoots = pgTable('getLoots', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
qbucks: integer().notNull(),
items: jsonb().$type<inventory>().notNull(),
created: timestamp().defaultNow().notNull()
});