feat(api): implement /api/event endpoint
This commit is contained in:
parent
b10b374b84
commit
50d915854f
3 changed files with 407 additions and 0 deletions
178
src/app/api/event/route.ts
Normal file
178
src/app/api/event/route.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
});
|
62
src/app/api/event/swagger.ts
Normal file
62
src/app/api/event/swagger.ts
Normal 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'],
|
||||||
|
});
|
||||||
|
}
|
167
src/app/api/event/validation.ts
Normal file
167
src/app/api/event/validation.ts
Normal 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),
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue