Added Basic Redis Caching for DB Queries

This commit is contained in:
Ahmad 2025-02-26 21:35:01 -05:00
parent e1003ee214
commit 0d04adf4fd
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
8 changed files with 386 additions and 58 deletions

View file

@ -1,8 +1,10 @@
import pkg from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema.js';
import { eq } from 'drizzle-orm';
import * as schema from './schema.js';
import { loadConfig } from '../util/configLoader.js';
import { del, exists, getJson, setJson } from './redis.js';
const { Pool } = pkg;
const config = loadConfig();
@ -13,41 +15,121 @@ const dbPool = new Pool({
});
export const db = drizzle({ client: dbPool, schema });
class DatabaseError extends Error {
constructor(
message: string,
public originalError?: Error,
) {
super(message);
this.name = 'DatabaseError';
}
}
export async function getAllMembers() {
return await db
.select()
.from(schema.memberTable)
.where(eq(schema.memberTable.currentlyInServer, true));
try {
if (await exists('nonBotMembers')) {
const memberData =
await getJson<(typeof schema.memberTable.$inferSelect)[]>(
'nonBotMembers',
);
if (memberData && memberData.length > 0) {
return memberData;
} else {
await del('nonBotMembers');
return await getAllMembers();
}
} else {
const nonBotMembers = await db
.select()
.from(schema.memberTable)
.where(eq(schema.memberTable.currentlyInServer, true));
await setJson<(typeof schema.memberTable.$inferSelect)[]>(
'nonBotMembers',
nonBotMembers,
);
return nonBotMembers;
}
} catch (error) {
console.error('Error getting all members: ', error);
throw new DatabaseError('Failed to get all members: ', error as Error);
}
}
export async function setMembers(nonBotMembers: any) {
nonBotMembers.forEach(async (member: any) => {
const memberExists = await db
.select()
.from(schema.memberTable)
.where(eq(schema.memberTable.discordId, member.user.id));
if (memberExists.length > 0) {
await db
.update(schema.memberTable)
.set({ discordUsername: member.user.username })
try {
nonBotMembers.forEach(async (member: any) => {
const memberInfo = await db
.select()
.from(schema.memberTable)
.where(eq(schema.memberTable.discordId, member.user.id));
} else {
const members: typeof schema.memberTable.$inferInsert = {
discordId: member.user.id,
discordUsername: member.user.username,
};
await db.insert(schema.memberTable).values(members);
}
});
if (memberInfo.length > 0) {
await updateMember({
discordId: member.user.id,
discordUsername: member.user.username,
currentlyInServer: true,
});
} else {
const members: typeof schema.memberTable.$inferInsert = {
discordId: member.user.id,
discordUsername: member.user.username,
};
await db.insert(schema.memberTable).values(members);
}
});
} catch (error) {
console.error('Error setting members: ', error);
throw new DatabaseError('Failed to set members: ', error as Error);
}
}
export async function getMember(discordId: string) {
return await db.query.memberTable.findFirst({
where: eq(schema.memberTable.discordId, discordId),
with: {
moderations: true,
},
});
try {
if (await exists(`${discordId}-memberInfo`)) {
const cachedMember = await getJson<
typeof schema.memberTable.$inferSelect
>(`${discordId}-memberInfo`);
const cachedModerationHistory = await getJson<
(typeof schema.moderationTable.$inferSelect)[]
>(`${discordId}-moderationHistory`);
if (
cachedMember &&
'discordId' in cachedMember &&
cachedModerationHistory &&
cachedModerationHistory.length > 0
) {
return {
...cachedMember,
moderations: cachedModerationHistory,
};
} else {
await del(`${discordId}-memberInfo`);
await del(`${discordId}-moderationHistory`);
return await getMember(discordId);
}
} else {
const member = await db.query.memberTable.findFirst({
where: eq(schema.memberTable.discordId, discordId),
with: {
moderations: true,
},
});
await setJson<typeof schema.memberTable.$inferSelect>(
`${discordId}-memberInfo`,
member!,
);
await setJson<(typeof schema.moderationTable.$inferSelect)[]>(
`${discordId}-moderationHistory`,
member!.moderations,
);
return member;
}
} catch (error) {
console.error('Error getting member: ', error);
throw new DatabaseError('Failed to get member: ', error as Error);
}
}
export async function updateMember({
@ -56,14 +138,28 @@ export async function updateMember({
currentlyInServer,
currentlyBanned,
}: schema.memberTableTypes) {
return await db
.update(schema.memberTable)
.set({
discordUsername,
currentlyInServer,
currentlyBanned,
})
.where(eq(schema.memberTable.discordId, discordId));
try {
const result = await db
.update(schema.memberTable)
.set({
discordUsername,
currentlyInServer,
currentlyBanned,
})
.where(eq(schema.memberTable.discordId, discordId));
if (await exists(`${discordId}-memberInfo`)) {
await del(`${discordId}-memberInfo`);
}
if (await exists('nonBotMembers')) {
await del('nonBotMembers');
}
return result;
} catch (error) {
console.error('Error updating member: ', error);
throw new DatabaseError('Failed to update member: ', error as Error);
}
}
export async function updateMemberModerationHistory({
@ -76,22 +172,61 @@ export async function updateMemberModerationHistory({
expiresAt,
active,
}: schema.moderationTableTypes) {
const moderationEntry = {
discordId,
moderatorDiscordId,
action,
reason,
duration,
createdAt,
expiresAt,
active,
};
return await db.insert(schema.moderationTable).values(moderationEntry);
try {
const moderationEntry = {
discordId,
moderatorDiscordId,
action,
reason,
duration,
createdAt,
expiresAt,
active,
};
const result = await db
.insert(schema.moderationTable)
.values(moderationEntry);
if (await exists(`${discordId}-moderationHistory`)) {
await del(`${discordId}-moderationHistory`);
}
if (await exists(`${discordId}-memberInfo`)) {
await del(`${discordId}-memberInfo`);
}
return result;
} catch (error) {
console.error('Error updating moderation history: ', error);
throw new DatabaseError(
'Failed to update moderation history: ',
error as Error,
);
}
}
export async function getMemberModerationHistory(discordId: string) {
return await db
.select()
.from(schema.moderationTable)
.where(eq(schema.moderationTable.discordId, discordId));
try {
if (await exists(`${discordId}-moderationHistory`)) {
return await getJson<(typeof schema.moderationTable.$inferSelect)[]>(
`${discordId}-moderationHistory`,
);
} else {
const moderationHistory = await db
.select()
.from(schema.moderationTable)
.where(eq(schema.moderationTable.discordId, discordId));
await setJson<(typeof schema.moderationTable.$inferSelect)[]>(
`${discordId}-moderationHistory`,
moderationHistory,
);
return moderationHistory;
}
} catch (error) {
console.error('Error getting moderation history: ', error);
throw new DatabaseError(
'Failed to get moderation history: ',
error as Error,
);
}
}

105
src/db/redis.ts Normal file
View file

@ -0,0 +1,105 @@
import Redis from 'ioredis';
import { loadConfig } from '../util/configLoader.js';
const config = loadConfig();
const redis = new Redis(config.redisConnectionString);
class RedisError extends Error {
constructor(
message: string,
public originalError?: Error,
) {
super(message);
this.name = 'RedisError';
}
}
redis.on('error', (error) => {
console.error('Redis connection error:', error);
throw new RedisError('Failed to connect to Redis instance: ', error);
});
redis.on('connect', () => {
console.log('Successfully connected to Redis');
});
export async function set(
key: string,
value: string,
ttl?: number,
): Promise<'OK'> {
try {
await redis.set(key, value);
if (ttl) await redis.expire(key, ttl);
} catch (error) {
console.error('Redis set error: ', error);
throw new RedisError(`Failed to set key: ${key}, `, error as Error);
}
return Promise.resolve('OK');
}
export async function setJson<T>(
key: string,
value: T,
ttl?: number,
): Promise<'OK'> {
return await set(key, JSON.stringify(value), ttl);
}
export async function incr(key: string): Promise<number> {
try {
return await redis.incr(key);
} catch (error) {
console.error('Redis increment error: ', error);
throw new RedisError(`Failed to increment key: ${key}, `, error as Error);
}
}
export async function exists(key: string): Promise<boolean> {
try {
return (await redis.exists(key)) === 1;
} catch (error) {
console.error('Redis exists error: ', error);
throw new RedisError(
`Failed to check if key exists: ${key}, `,
error as Error,
);
}
}
export async function get(key: string): Promise<string | null> {
try {
return await redis.get(key);
} catch (error) {
console.error('Redis get error: ', error);
throw new RedisError(`Failed to get key: ${key}, `, error as Error);
}
}
export async function mget(...keys: string[]): Promise<(string | null)[]> {
try {
return await redis.mget(keys);
} catch (error) {
console.error('Redis mget error: ', error);
throw new RedisError(`Failed to get keys: ${keys}, `, error as Error);
}
}
export async function getJson<T>(key: string): Promise<T | null> {
const value = await get(key);
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return null;
}
}
export async function del(key: string): Promise<number> {
try {
return await redis.del(key);
} catch (error) {
console.error('Redis del error: ', error);
throw new RedisError(`Failed to delete key: ${key}, `, error as Error);
}
}

View file

@ -17,9 +17,12 @@ export const memberJoin = {
}
try {
const members = await guild.members.fetch();
const nonBotMembers = members.filter((m) => !m.user.bot);
await setMembers(nonBotMembers);
await setMembers([
{
discordId: member.user.id,
discordUsername: member.user.username,
},
]);
if (!member.user.bot) {
const attachment = await generateMemberBanner({
@ -37,10 +40,6 @@ export const memberJoin = {
content: `Welcome to ${guild.name}, we hope you enjoy your stay!`,
files: [attachment],
}),
updateMember({
discordId: member.user.id,
currentlyInServer: true,
}),
member.roles.add(config.roles.joinRoles),
logAction({
guild,

View file

@ -1,9 +1,19 @@
import { Client, Events } from 'discord.js';
import { setMembers } from '../db/db.js';
import { loadConfig } from '../util/configLoader.js';
export default {
name: Events.ClientReady,
once: true,
execute: async (client: Client) => {
const config = loadConfig();
const members = await client.guilds.cache
.find((guild) => guild.id === config.guildId)
?.members.fetch();
const nonBotMembers = members!.filter((m) => !m.user.bot);
await setMembers(nonBotMembers);
console.log(`Ready! Logged in as ${client.user?.tag}`);
},
};

View file

@ -3,6 +3,7 @@ export interface Config {
clientId: string;
guildId: string;
dbConnectionString: string;
redisConnectionString: string;
channels: {
welcome: string;
logs: string;