Files
Nexumi/apps/webui/src/lib/module-configs/suggestions.ts
TheOnlyMace 6a223892dd Refactor queue management to use lazy initialization and improve Redis connection handling
- Updated queue and QueueEvents initialization to be lazy, preventing unnecessary Redis connections during the build process.
- Refactored queue access methods to ensure consistent connection handling across the application.
- Enhanced error handling for Redis connections during production builds to reduce log noise.
2026-07-22 21:38:56 +02:00

130 lines
4.2 KiB
TypeScript

import type {
SuggestionAction,
SuggestionConfigDashboard,
SuggestionConfigDashboardPatch,
SuggestionDashboard,
SuggestionListQuery
} from '@nexumi/shared';
import type { Suggestion } from '@prisma/client';
import { prisma } from '../prisma';
import { addJobAndAwait, getSuggestionsQueue, getSuggestionsQueueEvents } from '../queues';
export class SuggestionActionTimeoutError extends Error {
constructor() {
super('The bot did not confirm the suggestion update in time. Reload the list shortly to check the result.');
}
}
async function ensureSuggestionConfig(guildId: string) {
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
return prisma.suggestionConfig.upsert({ where: { guildId }, update: {}, create: { guildId } });
}
function toConfigDashboard(
config: Awaited<ReturnType<typeof ensureSuggestionConfig>>
): SuggestionConfigDashboard {
return {
enabled: config.enabled,
openChannelId: config.openChannelId ?? '',
approvedChannelId: config.approvedChannelId ?? '',
deniedChannelId: config.deniedChannelId ?? '',
createThread: config.createThread
};
}
export async function getSuggestionConfigDashboard(guildId: string): Promise<SuggestionConfigDashboard> {
const config = await ensureSuggestionConfig(guildId);
return toConfigDashboard(config);
}
export async function updateSuggestionConfigDashboard(
guildId: string,
patch: SuggestionConfigDashboardPatch
): Promise<SuggestionConfigDashboard> {
await ensureSuggestionConfig(guildId);
const { openChannelId, approvedChannelId, deniedChannelId, ...rest } = patch;
const updated = await prisma.suggestionConfig.update({
where: { guildId },
data: {
...rest,
...(openChannelId !== undefined ? { openChannelId: openChannelId === '' ? null : openChannelId } : {}),
...(approvedChannelId !== undefined
? { approvedChannelId: approvedChannelId === '' ? null : approvedChannelId }
: {}),
...(deniedChannelId !== undefined
? { deniedChannelId: deniedChannelId === '' ? null : deniedChannelId }
: {})
}
});
return toConfigDashboard(updated);
}
function toDashboard(suggestion: Suggestion): SuggestionDashboard {
return {
id: suggestion.id,
authorId: suggestion.authorId,
content: suggestion.content,
status: suggestion.status as SuggestionDashboard['status'],
upvotes: suggestion.upvotes,
downvotes: suggestion.downvotes,
staffReason: suggestion.staffReason,
createdAt: suggestion.createdAt.toISOString()
};
}
export async function listSuggestions(
guildId: string,
query: SuggestionListQuery
): Promise<SuggestionDashboard[]> {
const suggestions = await prisma.suggestion.findMany({
where: { guildId, ...(query.status ? { status: query.status } : {}) },
orderBy: { createdAt: 'desc' },
take: query.limit
});
return suggestions.map(toDashboard);
}
/**
* Staff actions (approve/deny/consider/implement) need to move or edit the
* suggestion's Discord embed and DM the author, which requires the bot's
* discord.js Client. The WebUI enqueues a `suggestionStatusUpdate` job on
* the same `suggestions` BullMQ queue the bot worker consumes (see
* `apps/bot/src/modules/suggestions/service.ts#runSuggestionStatusUpdateJob`)
* and waits (bounded) for it to finish.
*/
export async function applySuggestionAction(
guildId: string,
suggestionId: string,
action: SuggestionAction,
_actorUserId: string
): Promise<SuggestionDashboard> {
const existing = await prisma.suggestion.findFirst({ where: { id: suggestionId, guildId } });
if (!existing) {
throw new SuggestionActionTimeoutError();
}
const { confirmed } = await addJobAndAwait<{ id: string }>(
getSuggestionsQueue(),
getSuggestionsQueueEvents(),
'suggestionStatusUpdate',
{
suggestionId,
guildId,
status: action.status,
reason: action.staffReason ?? ''
},
{ removeOnComplete: true, removeOnFail: 25 }
);
if (!confirmed) {
throw new SuggestionActionTimeoutError();
}
const updated = await prisma.suggestion.findUnique({ where: { id: suggestionId } });
if (!updated) {
throw new SuggestionActionTimeoutError();
}
return toDashboard(updated);
}