refactor(services): validate UUID parameters with Zod and add error handling to prevent DB errors

This commit is contained in:
Chneemann 2026-08-29 17:36:39 +02:00
parent 4dc68e92a7
commit b8ed4a5b66
No known key found for this signature in database
4 changed files with 101 additions and 46 deletions

View file

@ -3,7 +3,7 @@
* @description Page component for viewing a specific channel and its messages.
*/
import { notFound } from "next/navigation";
import { redirect } from "next/navigation";
import { AppHeader } from "@/components/layout/AppHeader";
import { ChatInput } from "@/components/chat/ChatInput";
import { ChatMessages } from "@/components/chat/ChatMessages";
@ -24,13 +24,13 @@ export default async function ChannelPage({
}) {
const { channelId } = await params;
// Paralleles Laden von Kanal-Daten und Nachrichten
// Parallel loading of channel data and messages
const [channel, channelMessages] = await Promise.all([
getChannelById(channelId),
getChannelMessages(channelId),
]);
if (!channel) return notFound();
if (!channel) redirect("/");
return (
<div className="flex p-4 flex-col h-full bg-background">

View file

@ -1,20 +1,34 @@
/**
* @file lib/services/channel.service.ts
* @description Service module providing database access methods for channel management.
* @description Service module providing database access methods for channel management and validation.
*/
import { db } from "@/db";
import { channels } from "@/db/schema";
import { eq } from "drizzle-orm";
import { z } from "zod";
const uuidSchema = z.uuid();
/**
* Retrieves a single channel record from the database by its unique identifier.
* Validates the UUID format before executing the database query.
*
* @param {string} channelId - The unique identifier of the channel to fetch.
* @returns {Promise<Object | undefined>} The channel object if found, or undefined if no matching channel exists.
* @returns {Promise<Object | null>} The channel object if found, or null if no matching channel exists or an invalid ID was provided.
*/
export async function getChannelById(channelId: string) {
return await db.query.channels.findFirst({
where: eq(channels.id, channelId),
});
if (!uuidSchema.safeParse(channelId).success) {
return null;
}
try {
const channel = await db.query.channels.findFirst({
where: eq(channels.id, channelId),
});
return channel ?? null;
} catch (error) {
console.error(`Error fetching channel ${channelId}:`, error);
return null;
}
}

View file

@ -1,29 +1,43 @@
/**
* @file lib/services/message.service.ts
* @description Data access service for retrieving and managing message entities from the database.
* @description Service module providing database access methods for fetching and managing channel messages and associated member profiles.
*/
import { db } from "@/db";
import { messages } from "@/db/schema";
import { eq } from "drizzle-orm";
import { eq, asc } from "drizzle-orm";
import { z } from "zod";
const uuidSchema = z.uuid();
/**
* Retrieves all messages belonging to a specific channel, ordered chronologically ascending,
* including associated member and user details.
* Retrieves all messages for a specific channel sorted chronologically, including member and user details.
* Validates the UUID format before querying the database and handles potential runtime errors gracefully.
*
* @param {string} channelId - The unique identifier of the target channel.
* @returns {Promise<Array<Object>>} A promise resolving to a list of message objects populated with nested member and user data.
* @param {string} channelId - The unique identifier of the channel whose messages are to be fetched.
* @returns {Promise<Array<Object>>} An array of message objects with nested member and user data, or an empty array if invalid or failed.
*/
export async function getChannelMessages(channelId: string) {
return await db.query.messages.findMany({
where: eq(messages.channelId, channelId),
with: {
member: {
with: {
user: true,
if (!uuidSchema.safeParse(channelId).success) {
return [];
}
try {
const channelMessages = await db.query.messages.findMany({
where: eq(messages.channelId, channelId),
orderBy: [asc(messages.createdAt)],
with: {
member: {
with: {
user: true,
},
},
},
},
orderBy: (messages, { asc }) => [asc(messages.createdAt)],
});
});
return channelMessages;
} catch (error) {
console.error(`Error fetching messages for channel ${channelId}:`, error);
return [];
}
}

View file

@ -6,31 +6,47 @@
import { db } from "@/db";
import { members, servers } from "@/db/schema";
import { eq, and } from "drizzle-orm";
import { z } from "zod";
const uuidSchema = z.uuid();
/**
* Retrieves a server along with its channels sorted chronologically if the specified user is a verified member.
* Validates UUID formats prior to database execution to prevent database errors.
*
* @param {string} serverId - The unique identifier of the server to retrieve.
* @param {string} userId - The unique identifier of the requesting user.
* @returns {Promise<Object | null>} The server record with nested channels array, or null if the user is not a member or the server is not found.
* @returns {Promise<Object | null>} The server record with nested channels array, or null if the user is not a member, the server does not exist, or an invalid ID was provided.
*/
export async function getServerWithChannels(serverId: string, userId: string) {
const isMember = await db.query.members.findFirst({
where: and(eq(members.serverId, serverId), eq(members.userId, userId)),
});
if (
!uuidSchema.safeParse(serverId).success ||
!uuidSchema.safeParse(userId).success
) {
return null;
}
if (!isMember) return null;
try {
const isMember = await db.query.members.findFirst({
where: and(eq(members.serverId, serverId), eq(members.userId, userId)),
});
const server = await db.query.servers.findFirst({
where: eq(servers.id, serverId),
with: {
channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)],
if (!isMember) return null;
const server = await db.query.servers.findFirst({
where: eq(servers.id, serverId),
with: {
channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)],
},
},
},
});
});
return server;
return server ?? null;
} catch (error) {
console.error(`Error fetching server ${serverId}:`, error);
return null;
}
}
/**
@ -40,18 +56,29 @@ export async function getServerWithChannels(serverId: string, userId: string) {
* @returns {Promise<Array<Object>>} An array of server objects associated with the user.
*/
export async function getUserServers(userId: string) {
const userMemberships = await db.query.members.findMany({
where: eq(members.userId, userId),
with: {
server: {
with: {
channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)],
if (!uuidSchema.safeParse(userId).success) {
return [];
}
try {
const userMemberships = await db.query.members.findMany({
where: eq(members.userId, userId),
with: {
server: {
with: {
channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)],
},
},
},
},
},
});
});
return userMemberships.map((membership) => membership.server).filter(Boolean);
return userMemberships
.map((membership) => membership.server)
.filter(Boolean);
} catch (error) {
console.error(`Error fetching servers for user ${userId}:`, error);
return [];
}
}