diff --git a/apps/rpc/src/index.ts b/apps/rpc/src/index.ts index c9ff5106be..10ea713eff 100644 --- a/apps/rpc/src/index.ts +++ b/apps/rpc/src/index.ts @@ -2,6 +2,7 @@ export type { AppRouter } from "./app-router" export type { Pageable } from "./query" export type * from "./modules/article/article-router" +export type * from "./modules/event/event-router" export type * from "./modules/audit-log/audit-log-router" export type * from "./modules/company/company-router" export type * from "./modules/feedback-form/feedback-router" diff --git a/apps/rpc/src/modules/event/event-router.ts b/apps/rpc/src/modules/event/event-router.ts index f67b179820..b735f4606a 100644 --- a/apps/rpc/src/modules/event/event-router.ts +++ b/apps/rpc/src/modules/event/event-router.ts @@ -9,302 +9,316 @@ import { GroupSchema, UserSchema, } from "@dotkomonline/types" +import type { inferProcedureInput, inferProcedureOutput } from "@trpc/server" import { z } from "zod" +import { isEditor } from "../../authorization" +import { withAuditLogEntry, withAuthentication, withAuthorization, withDatabaseTransaction } from "../../middlewares" import { BasePaginateInputSchema, PaginateInputSchema } from "../../query" -import { authenticatedProcedure, procedure, staffProcedure, t } from "../../trpc" +import { procedure, t } from "../../trpc" import { feedbackRouter } from "../feedback-form/feedback-router" import { attendanceRouter } from "./attendance-router" -export const eventRouter = t.router({ - attendance: attendanceRouter, - feedback: feedbackRouter, - get: procedure - .input(EventSchema.shape.id) - .output(EventWithAttendanceSchema) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const event = await ctx.eventService.getEventById(handle, input) - const attendance = event.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, event.attendanceId) - : null - return { event, attendance } - }) - ), - - find: procedure - .input(EventSchema.shape.id) - .output(EventWithAttendanceSchema.nullable()) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const event = await ctx.eventService.findEventById(handle, input) - if (!event) { - return null - } - const attendance = event.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, event.attendanceId) - : null - return { event, attendance } - }) - ), +export type GetEventInput = inferProcedureInput +export type GetEventOutput = inferProcedureOutput +const getEventProcedure = procedure + .input(EventSchema.shape.id) + .output(EventWithAttendanceSchema) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const event = await ctx.eventService.getEventById(ctx.handle, input) + const attendance = event.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, event.attendanceId) + : null + return { event, attendance } + }) - create: staffProcedure - .input( - z.object({ - event: EventWriteSchema, - groupIds: z.array(GroupSchema.shape.slug), - companyIds: z.array(CompanySchema.shape.id), - parentId: EventSchema.shape.parentId.optional(), - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const eventWithoutOrganizers = await ctx.eventService.createEvent(handle, input.event) - const event = await ctx.eventService.updateEventOrganizers( - handle, - eventWithoutOrganizers.id, - new Set(input.groupIds), - new Set(input.companyIds) - ) - await ctx.eventService.updateEventParent(handle, event.id, input.parentId ?? null) - return { event, attendance: null } - }) - }), - - edit: staffProcedure - .input( - z.object({ - id: EventSchema.shape.id, - event: EventWriteSchema, - groupIds: z.array(GroupSchema.shape.slug), - companyIds: z.array(CompanySchema.shape.id), - parentId: EventSchema.shape.parentId.optional(), - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const updatedEventWithoutOrganizers = await ctx.eventService.updateEvent(handle, input.id, input.event) - const updatedEvent = await ctx.eventService.updateEventOrganizers( - handle, - updatedEventWithoutOrganizers.id, - new Set(input.groupIds), - new Set(input.companyIds) - ) - await ctx.eventService.updateEventParent(handle, updatedEvent.id, input.parentId ?? null) +export type FindEventInput = inferProcedureInput +export type FindEventOutput = inferProcedureOutput +const findEventProcedure = procedure + .input(EventSchema.shape.id) + .output(EventWithAttendanceSchema.nullable()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const event = await ctx.eventService.findEventById(ctx.handle, input) + if (!event) return null + const attendance = event.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, event.attendanceId) + : null + return { event, attendance } + }) - const attendance = updatedEventWithoutOrganizers.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, updatedEventWithoutOrganizers.attendanceId) - : null - return { event: updatedEvent, attendance } - }) - }), - - delete: staffProcedure - .input( - z.object({ - id: EventSchema.shape.id, - }) +export type CreateEventInput = inferProcedureInput +export type CreateEventOutput = inferProcedureOutput +const createEventProcedure = procedure + .input( + z.object({ + event: EventWriteSchema, + groupIds: z.array(GroupSchema.shape.slug), + companyIds: z.array(CompanySchema.shape.id), + parentId: EventSchema.shape.parentId.optional(), + }) + ) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const eventWithoutOrganizers = await ctx.eventService.createEvent(ctx.handle, input.event) + const event = await ctx.eventService.updateEventOrganizers( + ctx.handle, + eventWithoutOrganizers.id, + new Set(input.groupIds), + new Set(input.companyIds) ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - return await ctx.eventService.deleteEvent(handle, input.id) - }) - }), + await ctx.eventService.updateEventParent(ctx.handle, event.id, input.parentId ?? null) + return { event, attendance: null } + }) - all: procedure - .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional() }).default({})) - .output( - z.object({ - items: EventWithAttendanceSchema.array(), - nextCursor: EventSchema.shape.id.optional(), - }) +export type EditEventInput = inferProcedureInput +export type EditEventOutput = inferProcedureOutput +const editEventProcedure = procedure + .input( + z.object({ + id: EventSchema.shape.id, + event: EventWriteSchema, + groupIds: z.array(GroupSchema.shape.slug), + companyIds: z.array(CompanySchema.shape.id), + parentId: EventSchema.shape.parentId.optional(), + }) + ) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const updatedEventWithoutOrganizers = await ctx.eventService.updateEvent(ctx.handle, input.id, input.event) + const updatedEvent = await ctx.eventService.updateEventOrganizers( + ctx.handle, + updatedEventWithoutOrganizers.id, + new Set(input.groupIds), + new Set(input.companyIds) ) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const { filter, ...page } = input - const events = await ctx.eventService.findEvents(handle, { ...filter }, page) - const attendances = await ctx.attendanceService.getAttendancesByIds( - handle, - events.map((item) => item.attendanceId).filter((id) => id !== null) - ) + await ctx.eventService.updateEventParent(ctx.handle, updatedEvent.id, input.parentId ?? null) - const eventsWithAttendance = events.map((event) => ({ - event, - attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, - })) + const attendance = updatedEventWithoutOrganizers.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, updatedEventWithoutOrganizers.attendanceId) + : null + return { event: updatedEvent, attendance } + }) - return { - items: eventsWithAttendance, - nextCursor: events.at(-1)?.id, - } - }) - ), +export type DeleteEventInput = inferProcedureInput +export type DeleteEventOutput = inferProcedureOutput +const deleteEventProcedure = procedure + .input(z.object({ id: EventSchema.shape.id })) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + return await ctx.eventService.deleteEvent(ctx.handle, input.id) + }) - allByAttendingUserId: authenticatedProcedure - .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional(), id: UserSchema.shape.id })) - .output( - z.object({ - items: EventWithAttendanceSchema.array(), - nextCursor: EventSchema.shape.id.optional(), - }) +export type AllEventsInput = inferProcedureInput +export type AllEventsOutput = inferProcedureOutput +const allEventsProcedure = procedure + .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional() }).default({})) + .output( + z.object({ + items: EventWithAttendanceSchema.array(), + nextCursor: EventSchema.shape.id.optional(), + }) + ) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const { filter, ...page } = input + const events = await ctx.eventService.findEvents(ctx.handle, { ...filter }, page) + const attendances = await ctx.attendanceService.getAttendancesByIds( + ctx.handle, + events.map((item) => item.attendanceId).filter((id) => id !== null) ) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const { id, filter, ...page } = input - const events = await ctx.eventService.findEventsByAttendingUserId(handle, id, { ...filter }, page) - const attendances = await ctx.attendanceService.getAttendancesByIds( - handle, - events.map((item) => item.attendanceId).filter((id) => id !== null) - ) - - const eventsWithAttendance = events.map((event) => ({ - event, - attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, - })) - - return { - items: eventsWithAttendance, - nextCursor: events.at(-1)?.id, - } - }) - ), - addAttendance: staffProcedure - .input( - z.object({ - values: AttendanceWriteSchema, - eventId: EventSchema.shape.id, - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const attendance = await ctx.attendanceService.createAttendance(handle, input.values) - const event = await ctx.eventService.updateEventAttendance(handle, input.eventId, attendance.id) - return { event, attendance } - }) - }), + const eventsWithAttendance = events.map((event) => ({ + event, + attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, + })) - updateParentEvent: staffProcedure - .input( - z.object({ - eventId: EventSchema.shape.id, - parentEventId: EventSchema.shape.id.nullable(), - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const updatedEvent = await ctx.eventService.updateEventParent(handle, input.eventId, input.parentEventId) - const attendance = updatedEvent.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, updatedEvent.attendanceId) - : null - return { event: updatedEvent, attendance } - }) - }), + return { + items: eventsWithAttendance, + nextCursor: events.at(-1)?.id, + } + }) - findParentEvent: procedure - .input( - z.object({ - eventId: EventSchema.shape.id, - }) +export type AllByAttendingUserIdInput = inferProcedureInput +export type AllByAttendingUserIdOutput = inferProcedureOutput +const allByAttendingUserIdProcedure = procedure + .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional(), id: UserSchema.shape.id })) + .output( + z.object({ + items: EventWithAttendanceSchema.array(), + nextCursor: EventSchema.shape.id.optional(), + }) + ) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const { id, filter, ...page } = input + const events = await ctx.eventService.findEventsByAttendingUserId(ctx.handle, id, { ...filter }, page) + const attendances = await ctx.attendanceService.getAttendancesByIds( + ctx.handle, + events.map((item) => item.attendanceId).filter((id) => id !== null) ) - .output(EventWithAttendanceSchema.nullable()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const childEvent = await ctx.eventService.findEventById(handle, input.eventId) - if (!childEvent?.parentId) { - return null - } + const eventsWithAttendance = events.map((event) => ({ + event, + attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, + })) - const event = await ctx.eventService.findEventById(handle, childEvent.parentId) + return { + items: eventsWithAttendance, + nextCursor: events.at(-1)?.id, + } + }) - if (!event) { - return null - } +export type AddAttendanceInput = inferProcedureInput +export type AddAttendanceOutput = inferProcedureOutput +const addAttendanceProcedure = procedure + .input(z.object({ values: AttendanceWriteSchema, eventId: EventSchema.shape.id })) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.createAttendance(ctx.handle, input.values) + const event = await ctx.eventService.updateEventAttendance(ctx.handle, input.eventId, attendance.id) + return { event, attendance } + }) - const attendance = event?.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, event.attendanceId) - : null +export type UpdateParentEventInput = inferProcedureInput +export type UpdateParentEventOutput = inferProcedureOutput +const updateParentEventProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id, parentEventId: EventSchema.shape.id.nullable() })) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const updatedEvent = await ctx.eventService.updateEventParent(ctx.handle, input.eventId, input.parentEventId) + const attendance = updatedEvent.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, updatedEvent.attendanceId) + : null + return { event: updatedEvent, attendance } + }) - return { event, attendance } - }) - }), +export type FindParentEventInput = inferProcedureInput +export type FindParentEventOutput = inferProcedureOutput +const findParentEventProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id })) + .output(EventWithAttendanceSchema.nullable()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const childEvent = await ctx.eventService.findEventById(ctx.handle, input.eventId) + if (!childEvent?.parentId) return null + const event = await ctx.eventService.findEventById(ctx.handle, childEvent.parentId) + if (!event) return null + const attendance = event.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, event.attendanceId) + : null + return { event, attendance } + }) - findChildEvents: procedure - .input( - z.object({ - eventId: EventSchema.shape.id, - }) +export type FindChildEventsInput = inferProcedureInput +export type FindChildEventsOutput = inferProcedureOutput +const findChildEventsProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id })) + .output(EventWithAttendanceSchema.array()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const events = await ctx.eventService.findByParentEventId(ctx.handle, input.eventId) + const attendances = await ctx.attendanceService.getAttendancesByIds( + ctx.handle, + events.map((item) => item.attendanceId).filter((id) => id !== null) ) - .output(EventWithAttendanceSchema.array()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const events = await ctx.eventService.findByParentEventId(handle, input.eventId) - - const attendances = await ctx.attendanceService.getAttendancesByIds( - handle, - events.map((item) => item.attendanceId).filter((id) => id !== null) - ) + return events.map((event) => ({ + event, + attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, + })) + }) - const eventsWithAttendance = events.map((event) => ({ - event, - attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, - })) +export type FindUnansweredByUserInput = inferProcedureInput +export type FindUnansweredByUserOutput = inferProcedureOutput +const findUnansweredByUserProcedure = procedure + .input(UserSchema.shape.id) + .output(EventSchema.array()) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => ctx.eventService.findEventsWithUnansweredFeedbackFormByUserId(ctx.handle, input)) - return eventsWithAttendance - }) - }), - - findUnansweredByUser: authenticatedProcedure - .input(UserSchema.shape.id) - .output(EventSchema.array()) - .query(async ({ input, ctx }) => - ctx.executeTransaction( - async (handle) => await ctx.eventService.findEventsWithUnansweredFeedbackFormByUserId(handle, input) - ) - ), - - isOrganizer: authenticatedProcedure - .input( - z.object({ - eventId: EventSchema.shape.id, - }) - ) - .output(z.boolean()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const event = await ctx.eventService.getEventById(handle, input.eventId) - const groups = await ctx.groupService.findManyByMemberUserId(handle, ctx.principal.subject) +export type IsOrganizerInput = inferProcedureInput +export type IsOrganizerOutput = inferProcedureOutput +const isOrganizerProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id })) + .output(z.boolean()) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const event = await ctx.eventService.getEventById(ctx.handle, input.eventId) + const groups = await ctx.groupService.findManyByMemberUserId(ctx.handle, ctx.principal.subject) + return groups.some((group) => event.hostingGroups.some((organizer) => organizer.slug === group.slug)) + }) - return groups.some((group) => event.hostingGroups.some((organizer) => organizer.slug === group.slug)) - }) - }), +export type FindManyDeregisterReasonsWithEventInput = inferProcedureInput< + typeof findManyDeregisterReasonsWithEventProcedure +> +export type FindManyDeregisterReasonsWithEventOutput = inferProcedureOutput< + typeof findManyDeregisterReasonsWithEventProcedure +> +const findManyDeregisterReasonsWithEventProcedure = procedure + .input(PaginateInputSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .query(async ({ input, ctx }) => { + const rows = await ctx.eventService.findManyDeregisterReasonsWithEvent(ctx.handle, input) + return { + items: rows, + nextCursor: rows.at(-1)?.id, + } + }) - findManyDeregisterReasonsWithEvent: staffProcedure.input(PaginateInputSchema).query(async ({ ctx, input }) => { - return ctx.executeTransaction(async (handle) => { - const rows = await ctx.eventService.findManyDeregisterReasonsWithEvent(handle, input) +export type CreateFileUploadInput = inferProcedureInput +export type CreateFileUploadOutput = inferProcedureOutput +const createFileUploadProcedure = procedure + .input(z.object({ filename: z.string(), contentType: z.string() })) + .output(z.custom()) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ ctx, input }) => { + return ctx.eventService.createFileUpload(ctx.handle, input.filename, input.contentType, ctx.principal.subject) + }) - return { - items: rows, - nextCursor: rows.at(-1)?.id, - } - }) - }), - - createFileUpload: staffProcedure - .input( - z.object({ - filename: z.string(), - contentType: z.string(), - }) - ) - .output(z.custom()) - .mutation(async ({ ctx, input }) => { - return ctx.executeTransaction(async (handle) => { - return ctx.eventService.createFileUpload(handle, input.filename, input.contentType, ctx.principal.subject) - }) - }), +export const eventRouter = t.router({ + attendance: attendanceRouter, + feedback: feedbackRouter, + get: getEventProcedure, + find: findEventProcedure, + create: createEventProcedure, + edit: editEventProcedure, + delete: deleteEventProcedure, + all: allEventsProcedure, + allByAttendingUserId: allByAttendingUserIdProcedure, + addAttendance: addAttendanceProcedure, + updateParentEvent: updateParentEventProcedure, + findParentEvent: findParentEventProcedure, + findChildEvents: findChildEventsProcedure, + findUnansweredByUser: findUnansweredByUserProcedure, + isOrganizer: isOrganizerProcedure, + findManyDeregisterReasonsWithEvent: findManyDeregisterReasonsWithEventProcedure, + createFileUpload: createFileUploadProcedure, })