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