feat(api): implement /api/event endpoint

This commit is contained in:
Dominik 2025-06-20 13:30:04 +02:00
parent b10b374b84
commit 50d915854f
Signed by: dominik
GPG key ID: 06A4003FC5049644
3 changed files with 407 additions and 0 deletions

178
src/app/api/event/route.ts Normal file
View file

@ -0,0 +1,178 @@
import { prisma } from '@/prisma';
import { auth } from '@/auth';
import {
returnZodTypeCheckedResponse,
userAuthenticated,
} from '@/lib/apiHelpers';
import { ErrorResponseSchema, ZodErrorResponseSchema } from '../validation';
import {
createEventSchema,
EventResponseSchema,
EventsResponseSchema,
} from './validation';
export const GET = auth(async (req) => {
const authCheck = userAuthenticated(req);
if (!authCheck.continue)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
authCheck.response,
authCheck.metadata,
);
const dbUser = await prisma.user.findUnique({
where: {
id: authCheck.user.id,
},
});
if (!dbUser)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{ success: false, message: 'User not found' },
{ status: 404 },
);
const userEvents = await prisma.meeting.findMany({
where: {
OR: [
{ organizer_id: dbUser.id },
{ participants: { some: { user_id: dbUser.id } } },
],
},
select: {
id: true,
title: true,
description: true,
start_time: true,
end_time: true,
status: true,
location: true,
created_at: true,
updated_at: true,
organizer: {
select: {
id: true,
name: true,
first_name: true,
last_name: true,
image: true,
timezone: true,
},
},
participants: {
select: {
user: {
select: {
id: true,
name: true,
first_name: true,
last_name: true,
image: true,
timezone: true,
},
},
status: true,
},
},
},
});
return returnZodTypeCheckedResponse(
EventsResponseSchema,
{
success: true,
events: userEvents,
},
{ status: 200 },
);
});
export const POST = auth(async (req) => {
const authCheck = userAuthenticated(req);
if (!authCheck.continue)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
authCheck.response,
authCheck.metadata,
);
const dataRaw = await req.json();
const data = await createEventSchema.safeParseAsync(dataRaw);
if (!data.success) {
return returnZodTypeCheckedResponse(
ZodErrorResponseSchema,
{
success: false,
message: 'Invalid request data',
errors: data.error.issues,
},
{ status: 400 },
);
}
const { title, description, start_time, end_time, location, participants } =
data.data;
const newEvent = await prisma.meeting.create({
data: {
title,
description,
start_time,
end_time,
location,
organizer_id: authCheck.user.id!,
participants: participants
? {
create: participants.map((userId) => ({
user: { connect: { id: userId } },
})),
}
: undefined,
},
select: {
id: true,
title: true,
description: true,
start_time: true,
end_time: true,
status: true,
location: true,
created_at: true,
updated_at: true,
organizer: {
select: {
id: true,
name: true,
first_name: true,
last_name: true,
image: true,
timezone: true,
},
},
participants: {
select: {
user: {
select: {
id: true,
name: true,
first_name: true,
last_name: true,
image: true,
timezone: true,
},
},
status: true,
},
},
},
});
return returnZodTypeCheckedResponse(
EventResponseSchema,
{
success: true,
event: newEvent,
},
{ status: 201 },
);
});

View file

@ -0,0 +1,62 @@
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import {
EventResponseSchema,
EventsResponseSchema,
createEventSchema,
} from './validation';
import {
invalidRequestDataResponse,
notAuthenticatedResponse,
serverReturnedDataValidationErrorResponse,
userNotFoundResponse,
} from '@/lib/defaultApiResponses';
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
registry.registerPath({
method: 'get',
path: '/api/event',
responses: {
200: {
description: 'List of events for the authenticated user',
content: {
'application/json': {
schema: EventsResponseSchema,
},
},
},
...notAuthenticatedResponse,
...userNotFoundResponse,
...serverReturnedDataValidationErrorResponse,
},
tags: ['Event'],
});
registry.registerPath({
method: 'post',
path: '/api/event',
request: {
body: {
content: {
'application/json': {
schema: createEventSchema,
},
},
},
},
responses: {
201: {
description: 'Event created successfully.',
content: {
'application/json': {
schema: EventResponseSchema,
},
},
},
...invalidRequestDataResponse,
...notAuthenticatedResponse,
...userNotFoundResponse,
...serverReturnedDataValidationErrorResponse,
},
tags: ['Event'],
});
}

View file

@ -0,0 +1,167 @@
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import zod from 'zod/v4';
import {
existingUserIdServerSchema,
PublicUserSchema,
} from '../user/validation';
import { ParticipantSchema } from './[eventID]/participant/validation';
extendZodWithOpenApi(zod);
// ----------------------------------------
//
// Event ID Validation
//
// ----------------------------------------
export const eventIdSchema = zod.string().min(1, 'Event ID is required');
// ----------------------------------------
//
// Event Title Validation
//
// ----------------------------------------
export const eventTitleSchema = zod
.string()
.min(1, 'Title is required')
.max(100, 'Title must be at most 100 characters long');
// ----------------------------------------
//
// Event Description Validation
//
// ----------------------------------------
export const eventDescriptionSchema = zod
.string()
.max(500, 'Description must be at most 500 characters long');
// ----------------------------------------
//
// Event start time Validation
//
// ----------------------------------------
export const eventStartTimeSchema = zod.iso
.datetime()
.or(zod.date().transform((date) => date.toISOString()))
.refine((date) => !isNaN(new Date(date).getTime()), {
message: 'Invalid start time',
});
// ----------------------------------------
//
// Event end time Validation
//
// ----------------------------------------
export const eventEndTimeSchema = zod.iso.datetime().or(
zod
.date()
.transform((date) => date.toISOString())
.refine((date) => !isNaN(new Date(date).getTime()), {
message: 'Invalid end time',
}),
);
// ----------------------------------------
//
// Event Location Validation
//
// ----------------------------------------
export const eventLocationSchema = zod
.string()
.max(200, 'Location must be at most 200 characters long');
// ----------------------------------------
//
// Event Participants Validation
//
// ----------------------------------------
export const eventParticipantsSchema = zod.array(existingUserIdServerSchema);
// ----------------------------------------
//
// Event Status Validation
//
// ----------------------------------------
export const eventStatusSchema = zod.enum([
'TENTATIVE',
'CONFIRMED',
'CANCELLED',
]);
// ----------------------------------------
//
// Create Event Schema
//
// ----------------------------------------
export const createEventSchema = zod
.object({
title: eventTitleSchema,
description: eventDescriptionSchema.optional(),
start_time: eventStartTimeSchema,
end_time: eventEndTimeSchema,
location: eventLocationSchema.optional().default(''),
participants: eventParticipantsSchema.optional(),
status: eventStatusSchema.optional().default('TENTATIVE'),
})
.refine((data) => new Date(data.start_time) < new Date(data.end_time), {
message: 'Start time must be before end time',
});
// ----------------------------------------
//
// Update Event Schema
//
// ----------------------------------------
export const updateEventSchema = zod
.object({
title: eventTitleSchema.optional(),
description: eventDescriptionSchema.optional(),
start_time: eventStartTimeSchema.optional(),
end_time: eventEndTimeSchema.optional(),
location: eventLocationSchema.optional().default(''),
participants: eventParticipantsSchema.optional(),
status: eventStatusSchema.optional(),
})
.refine(
(data) => {
if (data.start_time && data.end_time) {
return new Date(data.start_time) < new Date(data.end_time);
}
return true;
},
{
message: 'Start time must be before end time',
},
);
// ----------------------------------------
//
// Event Schema Validation (for API responses)
//
// ----------------------------------------
export const EventSchema = zod
.object({
id: eventIdSchema,
title: eventTitleSchema,
description: eventDescriptionSchema.nullish(),
start_time: eventStartTimeSchema,
end_time: eventEndTimeSchema,
location: eventLocationSchema.nullish(),
status: eventStatusSchema,
created_at: zod.date(),
updated_at: zod.date(),
organizer: PublicUserSchema,
participants: zod.array(ParticipantSchema).nullish(),
})
.openapi('Event', {
description: 'Event information including all fields',
});
export const EventResponseSchema = zod.object({
success: zod.boolean(),
event: EventSchema,
});
export const EventsResponseSchema = zod.object({
success: zod.boolean(),
events: zod.array(EventSchema),
});