diff --git a/apps/rpc/src/modules/event/event-repository.ts b/apps/rpc/src/modules/event/event-repository.ts index d48d2ccaee..0aef078fed 100644 --- a/apps/rpc/src/modules/event/event-repository.ts +++ b/apps/rpc/src/modules/event/event-repository.ts @@ -1,4 +1,4 @@ -import type { DBHandle, Prisma } from "@dotkomonline/db" +import { type DBHandle, type Prisma, sql } from "@dotkomonline/db" import { type AttendanceId, type BaseEvent, @@ -433,76 +433,7 @@ export function getEventRepository(): EventRepository { Past events are not featured. We would rather have no featured events than "stale" events. */ - const events = await handle.$queryRaw` - WITH - capacities AS ( - SELECT - attendance_id, - SUM("capacity") AS sum - FROM attendance_pool - GROUP BY attendance_id - ), - attendees AS ( - SELECT - attendance_id, - COUNT(*) AS count - FROM attendee - GROUP BY attendance_id - ) - SELECT - event.*, - COALESCE(capacities.sum, 0) AS total_capacity, - COALESCE(attendees.count, 0) AS attendee_count, - -- 1,2,3: event type buckets - CASE event.type - WHEN 'GENERAL_ASSEMBLY' THEN 1 - WHEN 'COMPANY' THEN 2 - WHEN 'ACADEMIC' THEN 2 - ELSE 3 - END AS type_rank, - -- 1-4: registration buckets - CASE - -- 1. Future, registration open and not full AND capacities limited (> 0) - WHEN event.attendance_id IS NOT NULL - AND NOW() BETWEEN attendance.register_start AND attendance.register_end - AND COALESCE(capacities.sum, 0) > 0 - AND COALESCE(attendees.count, 0) < COALESCE(capacities.sum, 0) - THEN 1 - -- 2. Future, registration not started yet (capacities doesn't matter) - WHEN event.attendance_id IS NOT NULL - AND NOW() < attendance.register_start - THEN 2 - -- 3. Future, no registration OR unlimited capacities (total capacities = 0) - WHEN event.attendance_id IS NULL - OR COALESCE(capacities.sum, 0) = 0 - THEN 3 - -- 4. Future, registration full (status doesn't matter) - WHEN event.attendance_id IS NOT NULL - AND COALESCE(capacities.sum, 0) > 0 - AND COALESCE(attendees.count, 0) >= COALESCE(capacities.sum, 0) - THEN 4 - -- Fallback: treat as bucket 4 - ELSE 4 - END AS registration_bucket - FROM event - LEFT JOIN attendance - ON attendance.id = event.attendance_id - LEFT JOIN capacities - ON capacities.attendance_id = event.attendance_id - LEFT JOIN attendees - ON attendees.attendance_id = event.attendance_id - WHERE - event.status = 'PUBLIC' - -- Past events are not featured - AND event.start > NOW() - ORDER BY - type_rank ASC, - registration_bucket ASC, - -- Tiebreaker with earlier events first - event.start ASC - OFFSET ${offset} - LIMIT ${limit}; - ` + const events = await handle.$queryRawTyped(sql.findFeaturedEvents(offset, limit)) return parseOrReport( z.preprocess((data) => snakeCaseToCamelCase(data), BaseEventSchema.array()), diff --git a/packages/db/generated/prisma/internal/class.ts b/packages/db/generated/prisma/internal/class.ts index cb791c4a24..cf5aad3bd2 100644 --- a/packages/db/generated/prisma/internal/class.ts +++ b/packages/db/generated/prisma/internal/class.ts @@ -17,12 +17,13 @@ import type * as Prisma from "./prismaNamespace" const config: runtime.GetPrismaClientConfig = { "previewFeatures": [ - "relationJoins" + "relationJoins", + "typedSql" ], "clientVersion": "7.1.0", "engineVersion": "ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba", "activeProvider": "postgresql", - "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n\n previewFeatures = [\"relationJoins\"]\n}\n\ngenerator zod {\n provider = \"prisma-zod-generator\"\n output = \"../generated/schema\"\n config = \"../zod-generator.config.json\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nenum MembershipType {\n BACHELOR_STUDENT @map(\"BACHELOR_STUDENT\")\n MASTER_STUDENT @map(\"MASTER_STUDENT\")\n PHD_STUDENT @map(\"PHD_STUDENT\")\n KNIGHT @map(\"KNIGHT\")\n SOCIAL_MEMBER @map(\"SOCIAL_MEMBER\")\n OTHER @map(\"OTHER\")\n\n @@map(\"membership_type\")\n}\n\n/// Taken from the Feide API. The values were found by digging around in our Auth0 user profiles.\n///\n/// We have an additional value `UNKNOWN` to represent users that do not have a specialization or if some new value is\n/// suddenly added to the Feide API that we do not yet know about.\nenum MembershipSpecialization {\n ARTIFICIAL_INTELLIGENCE @map(\"ARTIFICIAL_INTELLIGENCE\")\n DATABASE_AND_SEARCH @map(\"DATABASE_AND_SEARCH\")\n INTERACTION_DESIGN @map(\"INTERACTION_DESIGN\")\n SOFTWARE_ENGINEERING @map(\"SOFTWARE_ENGINEERING\")\n UNKNOWN @map(\"UNKNOWN\")\n\n @@map(\"membership_specialization\")\n}\n\nmodel Membership {\n id String @id @default(uuid())\n userId String @map(\"user_id\")\n user User @relation(fields: [userId], references: [id])\n type MembershipType\n specialization MembershipSpecialization? @default(UNKNOWN)\n start DateTime @db.Timestamptz(3)\n end DateTime @db.Timestamptz(3)\n\n @@map(\"membership\")\n}\n\nmodel User {\n /// OpenID Connect Subject claim - for this reason there is no @default(uuid()) here.\n id String @id\n profileSlug String @unique @map(\"username\")\n name String?\n email String?\n imageUrl String? @map(\"image_url\")\n biography String?\n phone String?\n gender String?\n dietaryRestrictions String? @map(\"dietary_restrictions\")\n ntnuUsername String? @map(\"ntnu_username\")\n flags String[]\n /// Used for identifying the user in Google Workspace (my.name@online.ntnu.no)\n workspaceUserId String? @unique @map(\"workspace_user_id\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n privacyPermissionsId String? @unique @map(\"privacy_permissions_id\")\n privacyPermissions PrivacyPermissions?\n notificationPermissionsId String? @unique @map(\"notification_permissions_id\")\n notificationPermissions NotificationPermissions?\n\n attendee Attendee[]\n personalMark PersonalMark[]\n groupMemberships GroupMembership[]\n memberships Membership[]\n givenMarks PersonalMark[] @relation(\"GivenBy\")\n attendeesRefunded Attendee[] @relation(name: \"RefundedBy\")\n auditLogs AuditLog[]\n deregisterReasons DeregisterReason[]\n\n @@map(\"ow_user\")\n}\n\nmodel Company {\n id String @id @default(uuid())\n name String\n slug String @unique\n description String?\n phone String?\n email String?\n website String\n location String?\n imageUrl String? @map(\"image_url\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n events EventCompany[]\n JobListing JobListing[]\n\n @@map(\"company\")\n}\n\nenum GroupType {\n COMMITTEE @map(\"COMMITTEE\")\n NODE_COMMITTEE @map(\"NODE_COMMITTEE\")\n ASSOCIATED @map(\"ASSOCIATED\")\n INTEREST_GROUP @map(\"INTEREST_GROUP\")\n\n @@map(\"group_type\")\n}\n\nenum GroupMemberVisibility {\n ALL_MEMBERS @map(\"ALL_MEMBERS\")\n WITH_ROLES @map(\"WITH_ROLES\")\n LEADER @map(\"LEADER\")\n NONE @map(\"NONE\")\n\n @@map(\"group_member_visibility\")\n}\n\nenum GroupRecruitmentMethod {\n NONE @map(\"NONE\")\n SPRING_APPLICATION @map(\"SPRING_APPLICATION\")\n AUTUMN_APPLICATION @map(\"AUTUMN_APPLICATION\")\n GENERAL_ASSEMBLY @map(\"GENERAL_ASSEMBLY\")\n NOMINATION @map(\"NOMINATION\")\n OTHER @map(\"OTHER\")\n}\n\nmodel Group {\n slug String @id @unique\n abbreviation String\n name String?\n shortDescription String? @map(\"short_description\")\n description String\n imageUrl String? @map(\"image_url\")\n email String?\n contactUrl String? @map(\"contact_url\")\n showLeaderAsContact Boolean @default(false) @map(\"show_leader_as_contact\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n deactivatedAt DateTime? @map(\"deactivated_at\")\n workspaceGroupId String? @unique @map(\"workspace_group_id\")\n memberVisibility GroupMemberVisibility @default(ALL_MEMBERS) @map(\"member_visibility\")\n recruitmentMethod GroupRecruitmentMethod @default(NONE) @map(\"recruitment_method\")\n\n events EventHostingGroup[]\n type GroupType\n memberships GroupMembership[]\n marks MarkGroup[]\n roles GroupRole[]\n\n @@map(\"group\")\n}\n\nmodel GroupMembership {\n id String @id @default(uuid())\n start DateTime @db.Timestamptz(3)\n end DateTime? @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n groupId String @map(\"group_id\")\n userId String @map(\"user_id\")\n group Group @relation(fields: [groupId], references: [slug], onDelete: Cascade)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n roles GroupMembershipRole[]\n\n @@map(\"group_membership\")\n}\n\nmodel GroupMembershipRole {\n membershipId String @map(\"membership_id\")\n roleId String @map(\"role_id\")\n membership GroupMembership @relation(fields: [membershipId], references: [id], onDelete: Cascade)\n role GroupRole @relation(fields: [roleId], references: [id], onDelete: Cascade)\n\n @@id([membershipId, roleId])\n @@map(\"group_membership_role\")\n}\n\nenum GroupRoleType {\n LEADER @map(\"LEADER\")\n PUNISHER @map(\"PUNISHER\")\n TREASURER @map(\"TREASURER\")\n COSMETIC @map(\"COSMETIC\")\n DEPUTY_LEADER @map(\"DEPUTY_LEADER\")\n TRUSTEE @map(\"TRUSTEE\")\n EMAIL_ONLY @map(\"EMAIL_ONLY\")\n\n @@map(\"group_role_type\")\n}\n\nmodel GroupRole {\n id String @id @default(uuid())\n name String\n type GroupRoleType @default(COSMETIC)\n\n groupId String @map(\"group_id\")\n group Group @relation(fields: [groupId], references: [slug], onDelete: Cascade)\n\n groupMembershipRoles GroupMembershipRole[]\n\n @@unique([groupId, name])\n @@map(\"group_role\")\n}\n\nenum EventStatus {\n DRAFT @map(\"DRAFT\")\n PUBLIC @map(\"PUBLIC\")\n DELETED @map(\"DELETED\")\n\n @@map(\"event_status\")\n}\n\nenum EventType {\n /// Generalforsamling\n GENERAL_ASSEMBLY @map(\"GENERAL_ASSEMBLY\")\n /// Bedriftspresentasjon\n COMPANY @map(\"COMPANY\")\n /// Kurs\n ACADEMIC @map(\"ACADEMIC\")\n /// Sosialt\n SOCIAL @map(\"SOCIAL\")\n // This type is for the rare occation we have an event that is only open to committee members.\n /// Komitéarrangement\n INTERNAL @map(\"INTERNAL\")\n OTHER @map(\"OTHER\")\n // This type is for a committe called \"velkom\" and are special social events for new students.\n // These have a separate type because we have historically hid these from event lists to not\n // spam students that are not new with these events. In older versions of OnlineWeb these\n // were even treated as a completely separate event entity.\n /// Velkom/Fadderukene\n WELCOME @map(\"WELCOME\")\n\n @@map(\"event_type\")\n}\n\nmodel Attendance {\n id String @id @default(uuid())\n registerStart DateTime @map(\"register_start\") @db.Timestamptz(3)\n registerEnd DateTime @map(\"register_end\") @db.Timestamptz(3)\n deregisterDeadline DateTime @map(\"deregister_deadline\") @db.Timestamptz(3)\n selections Json @default(\"[]\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n /// The price as a whole integer in NOK (value 100 means NOK100.00)\n attendancePrice Int? @map(\"attendance_price\")\n\n pools AttendancePool[]\n attendees Attendee[]\n events Event[]\n\n @@map(\"attendance\")\n}\n\nmodel AttendancePool {\n id String @id @default(uuid())\n title String\n mergeDelayHours Int? @map(\"merge_delay_hours\")\n yearCriteria Json @map(\"year_criteria\")\n capacity Int\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n attendanceId String @map(\"attendance_id\")\n taskId String? @map(\"task_id\")\n attendance Attendance @relation(fields: [attendanceId], references: [id])\n task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade)\n\n attendees Attendee[]\n\n @@map(\"attendance_pool\")\n}\n\nmodel Attendee {\n id String @id @default(uuid())\n /// To preserve the user's grade at the time of registration\n userGrade Int? @map(\"user_grade\")\n feedbackFormAnswer FeedbackFormAnswer?\n /// Which options the user has selected from the Attendance selections\n selections Json @default(\"[]\")\n reserved Boolean\n earliestReservationAt DateTime @map(\"earliest_reservation_at\") @db.Timestamptz(3)\n attendedAt DateTime? @map(\"attended_at\") @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n paymentDeadline DateTime? @map(\"payment_deadline\")\n paymentLink String? @map(\"payment_link\")\n paymentId String? @map(\"payment_id\")\n paymentReservedAt DateTime? @map(\"payment_reserved_at\")\n paymentChargeDeadline DateTime? @map(\"payment_charge_deadline\")\n paymentChargedAt DateTime? @map(\"payment_charged_at\")\n paymentRefundedAt DateTime? @map(\"payment_refunded_at\")\n paymentCheckoutUrl String? @map(\"payment_checkout_url\")\n\n attendanceId String @map(\"attendance_id\")\n userId String @map(\"user_id\")\n attendancePoolId String @map(\"attendance_pool_id\")\n paymentRefundedById String? @map(\"payment_refunded_by_id\")\n attendance Attendance @relation(fields: [attendanceId], references: [id])\n user User @relation(fields: [userId], references: [id])\n attendancePool AttendancePool @relation(fields: [attendancePoolId], references: [id])\n paymentRefundedBy User? @relation(fields: [paymentRefundedById], references: [id], name: \"RefundedBy\")\n\n @@unique([attendanceId, userId], name: \"attendee_unique\")\n @@map(\"attendee\")\n}\n\nmodel Event {\n id String @id @default(uuid())\n title String\n start DateTime @db.Timestamptz(3)\n end DateTime @db.Timestamptz(3)\n status EventStatus\n description String\n shortDescription String? @map(\"short_description\")\n imageUrl String? @map(\"image_url\")\n locationTitle String? @map(\"location_title\")\n locationAddress String? @map(\"location_address\")\n locationLink String? @map(\"location_link\")\n type EventType\n feedbackForm FeedbackForm?\n markForMissedAttendance Boolean @default(true) @map(\"mark_for_missed_attendance\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n attendanceId String? @map(\"attendance_id\")\n parentId String? @map(\"parent_id\")\n attendance Attendance? @relation(fields: [attendanceId], references: [id])\n parent Event? @relation(\"children\", fields: [parentId], references: [id], map: \"event_parent_fkey\")\n children Event[] @relation(\"children\")\n\n companies EventCompany[]\n hostingGroups EventHostingGroup[]\n deregisterReasons DeregisterReason[]\n\n /// Historical metadata -- This is the id of the event in the previous version of OnlineWeb, if it was imported from\n /// the previous version\n metadataImportId Int? @map(\"metadata_import_id\")\n\n @@map(\"event\")\n}\n\nmodel EventCompany {\n eventId String @map(\"event_id\")\n companyId String @map(\"company_id\")\n event Event @relation(fields: [eventId], references: [id])\n company Company @relation(fields: [companyId], references: [id])\n\n @@id([eventId, companyId])\n @@map(\"event_company\")\n}\n\nenum MarkType {\n MANUAL\n LATE_ATTENDANCE\n MISSED_ATTENDANCE\n MISSING_FEEDBACK\n MISSING_PAYMENT\n}\n\nmodel Mark {\n id String @id @default(uuid())\n title String\n details String?\n /// Duration in days\n duration Int\n weight Int\n type MarkType @default(MANUAL)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n users PersonalMark[]\n groups MarkGroup[]\n\n @@map(\"mark\")\n}\n\nmodel MarkGroup {\n markId String @map(\"mark_id\")\n groupId String @map(\"group_id\")\n mark Mark @relation(fields: [markId], references: [id])\n group Group @relation(fields: [groupId], references: [slug])\n\n @@id([markId, groupId])\n @@map(\"mark_group\")\n}\n\nmodel PersonalMark {\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n\n markId String @map(\"mark_id\")\n userId String @map(\"user_id\")\n givenById String? @map(\"given_by_id\")\n mark Mark @relation(fields: [markId], references: [id])\n user User @relation(fields: [userId], references: [id])\n givenBy User? @relation(\"GivenBy\", fields: [givenById], references: [id])\n\n @@id([markId, userId])\n @@map(\"personal_mark\")\n}\n\nmodel PrivacyPermissions {\n id String @id @default(uuid())\n user User @relation(fields: [userId], references: [id])\n userId String @unique @map(\"user_id\")\n // TODO: rename to ~\"privateProfile\" and require authentication to view profile if true\n profileVisible Boolean @default(true) @map(\"profile_visible\")\n usernameVisible Boolean @default(true) @map(\"username_visible\")\n emailVisible Boolean @default(false) @map(\"email_visible\")\n phoneVisible Boolean @default(false) @map(\"phone_visible\")\n // TODO: delete this prop -- we do not have an address field on User\n addressVisible Boolean @default(false) @map(\"address_visible\")\n // TODO: default to true\n attendanceVisible Boolean @default(false) @map(\"attendance_visible\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@map(\"privacy_permissions\")\n}\n\nmodel NotificationPermissions {\n id String @id @default(uuid())\n user User @relation(fields: [userId], references: [id])\n userId String @unique @map(\"user_id\")\n applications Boolean @default(true)\n newArticles Boolean @default(true) @map(\"new_articles\")\n standardNotifications Boolean @default(true) @map(\"standard_notifications\")\n groupMessages Boolean @default(true) @map(\"group_messages\")\n markRulesUpdates Boolean @default(true) @map(\"mark_rules_updates\")\n receipts Boolean @default(true)\n registrationByAdministrator Boolean @default(true) @map(\"registration_by_administrator\")\n registrationStart Boolean @default(true) @map(\"registration_start\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@map(\"notification_permissions\")\n}\n\nmodel EventHostingGroup {\n groupId String @map(\"group_id\")\n eventId String @map(\"event_id\")\n group Group @relation(fields: [groupId], references: [slug])\n event Event @relation(fields: [eventId], references: [id])\n\n @@id([groupId, eventId])\n @@map(\"event_hosting_group\")\n}\n\nenum EmploymentType {\n PARTTIME @map(\"PARTTIME\")\n FULLTIME @map(\"FULLTIME\")\n SUMMER_INTERNSHIP @map(\"SUMMER_INTERNSHIP\")\n OTHER @map(\"OTHER\")\n\n @@map(\"employment_type\")\n}\n\nmodel JobListing {\n id String @id @default(uuid())\n title String\n description String\n shortDescription String? @map(\"short_description\")\n start DateTime @db.Timestamptz(3)\n end DateTime @db.Timestamptz(3)\n featured Boolean\n hidden Boolean\n deadline DateTime? @db.Timestamptz(3)\n employment EmploymentType\n applicationLink String? @map(\"application_link\")\n applicationEmail String? @map(\"application_email\")\n ///Applications are reviewed as soon as they are submitted\n rollingAdmission Boolean @map(\"rolling_admission\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n companyId String @map(\"company_id\")\n company Company @relation(fields: [companyId], references: [id])\n\n locations JobListingLocation[]\n\n @@map(\"job_listing\")\n}\n\nmodel JobListingLocation {\n name String\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n\n jobListingId String @map(\"job_listing_id\")\n jobListing JobListing @relation(fields: [jobListingId], references: [id])\n\n @@id([name, jobListingId])\n @@map(\"job_listing_location\")\n}\n\nmodel Offline {\n id String @id @default(uuid())\n title String\n fileUrl String? @map(\"file_url\")\n imageUrl String? @map(\"image_url\")\n publishedAt DateTime @map(\"published_at\") @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@map(\"offline\")\n}\n\nmodel Article {\n id String @id @default(uuid())\n slug String @unique\n title String\n author String\n photographer String\n imageUrl String @map(\"image_url\")\n excerpt String\n content String\n isFeatured Boolean @default(false) @map(\"is_featured\")\n vimeoId String? @map(\"vimeo_id\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n tags ArticleTagLink[]\n\n @@map(\"article\")\n}\n\nmodel ArticleTag {\n name String @id\n\n articles ArticleTagLink[]\n\n @@map(\"article_tag\")\n}\n\nmodel ArticleTagLink {\n articleId String @map(\"article_id\")\n tagName String @map(\"tag_name\")\n article Article @relation(fields: [articleId], references: [id])\n tag ArticleTag @relation(fields: [tagName], references: [name])\n\n @@id([articleId, tagName])\n @@map(\"article_tag_link\")\n}\n\nenum TaskType {\n RESERVE_ATTENDEE @map(\"RESERVE_ATTENDEE\")\n CHARGE_ATTENDEE @map(\"CHARGE_ATTENDEE\")\n MERGE_ATTENDANCE_POOLS @map(\"MERGE_ATTENDANCE_POOLS\")\n VERIFY_PAYMENT @map(\"VERIFY_PAYMENT\")\n VERIFY_FEEDBACK_ANSWERED @map(\"VERIFY_FEEDBACK_ANSWERED\")\n SEND_FEEDBACK_FORM_EMAILS @map(\"SEND_FEEDBACK_FORM_EMAILS\")\n VERIFY_ATTENDEE_ATTENDED @map(\"VERIFY_ATTENDEE_ATTENDED\")\n\n @@map(\"task_type\")\n}\n\nenum TaskStatus {\n PENDING @map(\"PENDING\")\n RUNNING @map(\"RUNNING\")\n COMPLETED @map(\"COMPLETED\")\n FAILED @map(\"FAILED\")\n CANCELED @map(\"CANCELED\")\n\n @@map(\"task_status\")\n}\n\nmodel Task {\n id String @id @default(uuid())\n type TaskType\n status TaskStatus @default(PENDING)\n payload Json @default(\"{}\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n scheduledAt DateTime @map(\"scheduled_at\") @db.Timestamptz(3)\n processedAt DateTime? @map(\"processed_at\") @db.Timestamptz(3)\n\n recurringTaskId String? @map(\"recurring_task_id\")\n recurringTask RecurringTask? @relation(fields: [recurringTaskId], references: [id], onDelete: SetNull)\n\n attendancePools AttendancePool[]\n\n @@index([scheduledAt, status], name: \"idx_job_scheduled_at_status\")\n @@map(\"task\")\n}\n\nmodel RecurringTask {\n id String @id @default(uuid())\n type TaskType\n payload Json @default(\"{}\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n schedule String\n lastRunAt DateTime? @map(\"last_run_at\") @db.Timestamptz(3)\n nextRunAt DateTime @map(\"next_run_at\") @db.Timestamptz(3)\n\n tasks Task[]\n\n @@index([nextRunAt])\n @@map(\"recurring_task\")\n}\n\nenum FeedbackQuestionType {\n TEXT @map(\"TEXT\")\n LONGTEXT @map(\"LONGTEXT\")\n RATING @map(\"RATING\")\n CHECKBOX @map(\"CHECKBOX\")\n SELECT @map(\"SELECT\")\n MULTISELECT @map(\"MULTISELECT\")\n\n @@map(\"feedback_question_type\")\n}\n\nenum DeregisterReasonType {\n SCHOOL @map(\"SCHOOL\")\n WORK @map(\"WORK\")\n ECONOMY @map(\"ECONOMY\")\n TIME @map(\"TIME\")\n SICK @map(\"SICK\")\n NO_FAMILIAR_FACES @map(\"NO_FAMILIAR_FACES\")\n OTHER @map(\"OTHER\")\n\n @@map(\"deregister_reason_type\")\n}\n\nmodel FeedbackForm {\n id String @id @default(uuid())\n publicResultsToken String @unique @default(uuid()) @map(\"public_results_token\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n answerDeadline DateTime @map(\"answer_deadline\") @db.Timestamptz(3)\n\n eventId String @unique @map(\"event_id\")\n event Event @relation(fields: [eventId], references: [id])\n\n questions FeedbackQuestion[]\n answers FeedbackFormAnswer[]\n\n @@map(\"feedback_form\")\n}\n\nmodel FeedbackQuestion {\n id String @id @default(uuid())\n label String\n required Boolean @default(false)\n showInPublicResults Boolean @default(true) @map(\"show_in_public_results\")\n type FeedbackQuestionType\n order Int\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n feedbackFormId String @map(\"feedback_form_id\")\n feedbackForm FeedbackForm @relation(fields: [feedbackFormId], references: [id], onDelete: Cascade)\n\n options FeedbackQuestionOption[]\n answers FeedbackQuestionAnswer[] @relation(\"QuestionAnswers\")\n\n @@map(\"feedback_question\")\n}\n\nmodel FeedbackQuestionOption {\n id String @id @default(uuid())\n name String\n\n questionId String @map(\"question_id\")\n question FeedbackQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)\n\n selectedInAnswers FeedbackQuestionAnswerOptionLink[]\n\n @@unique([questionId, name])\n @@map(\"feedback_question_option\")\n}\n\nmodel FeedbackQuestionAnswer {\n id String @id @default(uuid())\n value Json?\n\n questionId String @map(\"question_id\")\n formAnswerId String @map(\"form_answer_id\")\n question FeedbackQuestion @relation(\"QuestionAnswers\", fields: [questionId], references: [id])\n formAnswer FeedbackFormAnswer @relation(\"FormAnswers\", fields: [formAnswerId], references: [id], onDelete: Cascade)\n\n selectedOptions FeedbackQuestionAnswerOptionLink[]\n\n @@map(\"feedback_question_answer\")\n}\n\nmodel FeedbackQuestionAnswerOptionLink {\n feedbackQuestionOptionId String @map(\"feedback_question_option_id\")\n feedbackQuestionAnswerId String @map(\"feedback_question_answer_id\")\n feedbackQuestionOption FeedbackQuestionOption @relation(fields: [feedbackQuestionOptionId], references: [id])\n feedbackQuestionAnswer FeedbackQuestionAnswer @relation(fields: [feedbackQuestionAnswerId], references: [id], onDelete: Cascade)\n\n @@id([feedbackQuestionOptionId, feedbackQuestionAnswerId])\n @@map(\"feedback_answer_option_link\")\n}\n\nmodel FeedbackFormAnswer {\n id String @id @default(uuid())\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n feedbackFormId String @map(\"feedback_form_id\")\n attendeeId String @unique @map(\"attendee_id\")\n feedbackForm FeedbackForm @relation(fields: [feedbackFormId], references: [id])\n attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)\n\n answers FeedbackQuestionAnswer[] @relation(\"FormAnswers\")\n\n @@map(\"feedback_form_answer\")\n}\n\nmodel AuditLog {\n id String @id @default(uuid())\n tableName String @map(\"table_name\")\n rowId String? @map(\"row_id\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n operation String\n rowData Json @map(\"row_data\")\n /// Database transaction id\n transactionId BigInt @map(\"transaction_id\")\n\n /// User relation is optional because the system can execute operations without a user to link it to. For example with\n /// recurring tasks.\n user User? @relation(fields: [userId], references: [id])\n userId String? @map(\"user_id\")\n\n @@map(\"audit_log\")\n}\n\nmodel DeregisterReason {\n id String @id @default(uuid())\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n registeredAt DateTime @map(\"registered_at\") @db.Timestamptz(3)\n type DeregisterReasonType\n details String?\n userGrade Int? @map(\"user_grade\")\n\n userId String @map(\"user_id\")\n eventId String @map(\"event_id\")\n user User @relation(fields: [userId], references: [id])\n event Event @relation(fields: [eventId], references: [id])\n\n @@map(\"deregister_reason\")\n}\n", + "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n\n previewFeatures = [\"relationJoins\", \"typedSql\"]\n}\n\ngenerator zod {\n provider = \"prisma-zod-generator\"\n output = \"../generated/schema\"\n config = \"../zod-generator.config.json\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nenum MembershipType {\n BACHELOR_STUDENT @map(\"BACHELOR_STUDENT\")\n MASTER_STUDENT @map(\"MASTER_STUDENT\")\n PHD_STUDENT @map(\"PHD_STUDENT\")\n KNIGHT @map(\"KNIGHT\")\n SOCIAL_MEMBER @map(\"SOCIAL_MEMBER\")\n OTHER @map(\"OTHER\")\n\n @@map(\"membership_type\")\n}\n\n/// Taken from the Feide API. The values were found by digging around in our Auth0 user profiles.\n///\n/// We have an additional value `UNKNOWN` to represent users that do not have a specialization or if some new value is\n/// suddenly added to the Feide API that we do not yet know about.\nenum MembershipSpecialization {\n ARTIFICIAL_INTELLIGENCE @map(\"ARTIFICIAL_INTELLIGENCE\")\n DATABASE_AND_SEARCH @map(\"DATABASE_AND_SEARCH\")\n INTERACTION_DESIGN @map(\"INTERACTION_DESIGN\")\n SOFTWARE_ENGINEERING @map(\"SOFTWARE_ENGINEERING\")\n UNKNOWN @map(\"UNKNOWN\")\n\n @@map(\"membership_specialization\")\n}\n\nmodel Membership {\n id String @id @default(uuid())\n userId String @map(\"user_id\")\n user User @relation(fields: [userId], references: [id])\n type MembershipType\n specialization MembershipSpecialization? @default(UNKNOWN)\n start DateTime @db.Timestamptz(3)\n end DateTime @db.Timestamptz(3)\n\n @@map(\"membership\")\n}\n\nmodel User {\n /// OpenID Connect Subject claim - for this reason there is no @default(uuid()) here.\n id String @id\n profileSlug String @unique @map(\"username\")\n name String?\n email String?\n imageUrl String? @map(\"image_url\")\n biography String?\n phone String?\n gender String?\n dietaryRestrictions String? @map(\"dietary_restrictions\")\n ntnuUsername String? @map(\"ntnu_username\")\n flags String[]\n /// Used for identifying the user in Google Workspace (my.name@online.ntnu.no)\n workspaceUserId String? @unique @map(\"workspace_user_id\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n privacyPermissionsId String? @unique @map(\"privacy_permissions_id\")\n privacyPermissions PrivacyPermissions?\n notificationPermissionsId String? @unique @map(\"notification_permissions_id\")\n notificationPermissions NotificationPermissions?\n\n attendee Attendee[]\n personalMark PersonalMark[]\n groupMemberships GroupMembership[]\n memberships Membership[]\n givenMarks PersonalMark[] @relation(\"GivenBy\")\n attendeesRefunded Attendee[] @relation(name: \"RefundedBy\")\n auditLogs AuditLog[]\n deregisterReasons DeregisterReason[]\n\n @@map(\"ow_user\")\n}\n\nmodel Company {\n id String @id @default(uuid())\n name String\n slug String @unique\n description String?\n phone String?\n email String?\n website String\n location String?\n imageUrl String? @map(\"image_url\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n events EventCompany[]\n JobListing JobListing[]\n\n @@map(\"company\")\n}\n\nenum GroupType {\n COMMITTEE @map(\"COMMITTEE\")\n NODE_COMMITTEE @map(\"NODE_COMMITTEE\")\n ASSOCIATED @map(\"ASSOCIATED\")\n INTEREST_GROUP @map(\"INTEREST_GROUP\")\n\n @@map(\"group_type\")\n}\n\nenum GroupMemberVisibility {\n ALL_MEMBERS @map(\"ALL_MEMBERS\")\n WITH_ROLES @map(\"WITH_ROLES\")\n LEADER @map(\"LEADER\")\n NONE @map(\"NONE\")\n\n @@map(\"group_member_visibility\")\n}\n\nenum GroupRecruitmentMethod {\n NONE @map(\"NONE\")\n SPRING_APPLICATION @map(\"SPRING_APPLICATION\")\n AUTUMN_APPLICATION @map(\"AUTUMN_APPLICATION\")\n GENERAL_ASSEMBLY @map(\"GENERAL_ASSEMBLY\")\n NOMINATION @map(\"NOMINATION\")\n OTHER @map(\"OTHER\")\n}\n\nmodel Group {\n slug String @id @unique\n abbreviation String\n name String?\n shortDescription String? @map(\"short_description\")\n description String\n imageUrl String? @map(\"image_url\")\n email String?\n contactUrl String? @map(\"contact_url\")\n showLeaderAsContact Boolean @default(false) @map(\"show_leader_as_contact\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n deactivatedAt DateTime? @map(\"deactivated_at\")\n workspaceGroupId String? @unique @map(\"workspace_group_id\")\n memberVisibility GroupMemberVisibility @default(ALL_MEMBERS) @map(\"member_visibility\")\n recruitmentMethod GroupRecruitmentMethod @default(NONE) @map(\"recruitment_method\")\n\n events EventHostingGroup[]\n type GroupType\n memberships GroupMembership[]\n marks MarkGroup[]\n roles GroupRole[]\n\n @@map(\"group\")\n}\n\nmodel GroupMembership {\n id String @id @default(uuid())\n start DateTime @db.Timestamptz(3)\n end DateTime? @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n groupId String @map(\"group_id\")\n userId String @map(\"user_id\")\n group Group @relation(fields: [groupId], references: [slug], onDelete: Cascade)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n roles GroupMembershipRole[]\n\n @@map(\"group_membership\")\n}\n\nmodel GroupMembershipRole {\n membershipId String @map(\"membership_id\")\n roleId String @map(\"role_id\")\n membership GroupMembership @relation(fields: [membershipId], references: [id], onDelete: Cascade)\n role GroupRole @relation(fields: [roleId], references: [id], onDelete: Cascade)\n\n @@id([membershipId, roleId])\n @@map(\"group_membership_role\")\n}\n\nenum GroupRoleType {\n LEADER @map(\"LEADER\")\n PUNISHER @map(\"PUNISHER\")\n TREASURER @map(\"TREASURER\")\n COSMETIC @map(\"COSMETIC\")\n DEPUTY_LEADER @map(\"DEPUTY_LEADER\")\n TRUSTEE @map(\"TRUSTEE\")\n EMAIL_ONLY @map(\"EMAIL_ONLY\")\n\n @@map(\"group_role_type\")\n}\n\nmodel GroupRole {\n id String @id @default(uuid())\n name String\n type GroupRoleType @default(COSMETIC)\n\n groupId String @map(\"group_id\")\n group Group @relation(fields: [groupId], references: [slug], onDelete: Cascade)\n\n groupMembershipRoles GroupMembershipRole[]\n\n @@unique([groupId, name])\n @@map(\"group_role\")\n}\n\nenum EventStatus {\n DRAFT @map(\"DRAFT\")\n PUBLIC @map(\"PUBLIC\")\n DELETED @map(\"DELETED\")\n\n @@map(\"event_status\")\n}\n\nenum EventType {\n /// Generalforsamling\n GENERAL_ASSEMBLY @map(\"GENERAL_ASSEMBLY\")\n /// Bedriftspresentasjon\n COMPANY @map(\"COMPANY\")\n /// Kurs\n ACADEMIC @map(\"ACADEMIC\")\n /// Sosialt\n SOCIAL @map(\"SOCIAL\")\n // This type is for the rare occation we have an event that is only open to committee members.\n /// Komitéarrangement\n INTERNAL @map(\"INTERNAL\")\n OTHER @map(\"OTHER\")\n // This type is for a committe called \"velkom\" and are special social events for new students.\n // These have a separate type because we have historically hid these from event lists to not\n // spam students that are not new with these events. In older versions of OnlineWeb these\n // were even treated as a completely separate event entity.\n /// Velkom/Fadderukene\n WELCOME @map(\"WELCOME\")\n\n @@map(\"event_type\")\n}\n\nmodel Attendance {\n id String @id @default(uuid())\n registerStart DateTime @map(\"register_start\") @db.Timestamptz(3)\n registerEnd DateTime @map(\"register_end\") @db.Timestamptz(3)\n deregisterDeadline DateTime @map(\"deregister_deadline\") @db.Timestamptz(3)\n selections Json @default(\"[]\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n /// The price as a whole integer in NOK (value 100 means NOK100.00)\n attendancePrice Int? @map(\"attendance_price\")\n\n pools AttendancePool[]\n attendees Attendee[]\n events Event[]\n\n @@map(\"attendance\")\n}\n\nmodel AttendancePool {\n id String @id @default(uuid())\n title String\n mergeDelayHours Int? @map(\"merge_delay_hours\")\n yearCriteria Json @map(\"year_criteria\")\n capacity Int\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n attendanceId String @map(\"attendance_id\")\n taskId String? @map(\"task_id\")\n attendance Attendance @relation(fields: [attendanceId], references: [id])\n task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade)\n\n attendees Attendee[]\n\n @@map(\"attendance_pool\")\n}\n\nmodel Attendee {\n id String @id @default(uuid())\n /// To preserve the user's grade at the time of registration\n userGrade Int? @map(\"user_grade\")\n feedbackFormAnswer FeedbackFormAnswer?\n /// Which options the user has selected from the Attendance selections\n selections Json @default(\"[]\")\n reserved Boolean\n earliestReservationAt DateTime @map(\"earliest_reservation_at\") @db.Timestamptz(3)\n attendedAt DateTime? @map(\"attended_at\") @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n paymentDeadline DateTime? @map(\"payment_deadline\")\n paymentLink String? @map(\"payment_link\")\n paymentId String? @map(\"payment_id\")\n paymentReservedAt DateTime? @map(\"payment_reserved_at\")\n paymentChargeDeadline DateTime? @map(\"payment_charge_deadline\")\n paymentChargedAt DateTime? @map(\"payment_charged_at\")\n paymentRefundedAt DateTime? @map(\"payment_refunded_at\")\n paymentCheckoutUrl String? @map(\"payment_checkout_url\")\n\n attendanceId String @map(\"attendance_id\")\n userId String @map(\"user_id\")\n attendancePoolId String @map(\"attendance_pool_id\")\n paymentRefundedById String? @map(\"payment_refunded_by_id\")\n attendance Attendance @relation(fields: [attendanceId], references: [id])\n user User @relation(fields: [userId], references: [id])\n attendancePool AttendancePool @relation(fields: [attendancePoolId], references: [id])\n paymentRefundedBy User? @relation(fields: [paymentRefundedById], references: [id], name: \"RefundedBy\")\n\n @@unique([attendanceId, userId], name: \"attendee_unique\")\n @@map(\"attendee\")\n}\n\nmodel Event {\n id String @id @default(uuid())\n title String\n start DateTime @db.Timestamptz(3)\n end DateTime @db.Timestamptz(3)\n status EventStatus\n description String\n shortDescription String? @map(\"short_description\")\n imageUrl String? @map(\"image_url\")\n locationTitle String? @map(\"location_title\")\n locationAddress String? @map(\"location_address\")\n locationLink String? @map(\"location_link\")\n type EventType\n feedbackForm FeedbackForm?\n markForMissedAttendance Boolean @default(true) @map(\"mark_for_missed_attendance\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n attendanceId String? @map(\"attendance_id\")\n parentId String? @map(\"parent_id\")\n attendance Attendance? @relation(fields: [attendanceId], references: [id])\n parent Event? @relation(\"children\", fields: [parentId], references: [id], map: \"event_parent_fkey\")\n children Event[] @relation(\"children\")\n\n companies EventCompany[]\n hostingGroups EventHostingGroup[]\n deregisterReasons DeregisterReason[]\n\n /// Historical metadata -- This is the id of the event in the previous version of OnlineWeb, if it was imported from\n /// the previous version\n metadataImportId Int? @map(\"metadata_import_id\")\n\n @@map(\"event\")\n}\n\nmodel EventCompany {\n eventId String @map(\"event_id\")\n companyId String @map(\"company_id\")\n event Event @relation(fields: [eventId], references: [id])\n company Company @relation(fields: [companyId], references: [id])\n\n @@id([eventId, companyId])\n @@map(\"event_company\")\n}\n\nenum MarkType {\n MANUAL\n LATE_ATTENDANCE\n MISSED_ATTENDANCE\n MISSING_FEEDBACK\n MISSING_PAYMENT\n}\n\nmodel Mark {\n id String @id @default(uuid())\n title String\n details String?\n /// Duration in days\n duration Int\n weight Int\n type MarkType @default(MANUAL)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n users PersonalMark[]\n groups MarkGroup[]\n\n @@map(\"mark\")\n}\n\nmodel MarkGroup {\n markId String @map(\"mark_id\")\n groupId String @map(\"group_id\")\n mark Mark @relation(fields: [markId], references: [id])\n group Group @relation(fields: [groupId], references: [slug])\n\n @@id([markId, groupId])\n @@map(\"mark_group\")\n}\n\nmodel PersonalMark {\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n\n markId String @map(\"mark_id\")\n userId String @map(\"user_id\")\n givenById String? @map(\"given_by_id\")\n mark Mark @relation(fields: [markId], references: [id])\n user User @relation(fields: [userId], references: [id])\n givenBy User? @relation(\"GivenBy\", fields: [givenById], references: [id])\n\n @@id([markId, userId])\n @@map(\"personal_mark\")\n}\n\nmodel PrivacyPermissions {\n id String @id @default(uuid())\n user User @relation(fields: [userId], references: [id])\n userId String @unique @map(\"user_id\")\n // TODO: rename to ~\"privateProfile\" and require authentication to view profile if true\n profileVisible Boolean @default(true) @map(\"profile_visible\")\n usernameVisible Boolean @default(true) @map(\"username_visible\")\n emailVisible Boolean @default(false) @map(\"email_visible\")\n phoneVisible Boolean @default(false) @map(\"phone_visible\")\n // TODO: delete this prop -- we do not have an address field on User\n addressVisible Boolean @default(false) @map(\"address_visible\")\n // TODO: default to true\n attendanceVisible Boolean @default(false) @map(\"attendance_visible\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@map(\"privacy_permissions\")\n}\n\nmodel NotificationPermissions {\n id String @id @default(uuid())\n user User @relation(fields: [userId], references: [id])\n userId String @unique @map(\"user_id\")\n applications Boolean @default(true)\n newArticles Boolean @default(true) @map(\"new_articles\")\n standardNotifications Boolean @default(true) @map(\"standard_notifications\")\n groupMessages Boolean @default(true) @map(\"group_messages\")\n markRulesUpdates Boolean @default(true) @map(\"mark_rules_updates\")\n receipts Boolean @default(true)\n registrationByAdministrator Boolean @default(true) @map(\"registration_by_administrator\")\n registrationStart Boolean @default(true) @map(\"registration_start\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@map(\"notification_permissions\")\n}\n\nmodel EventHostingGroup {\n groupId String @map(\"group_id\")\n eventId String @map(\"event_id\")\n group Group @relation(fields: [groupId], references: [slug])\n event Event @relation(fields: [eventId], references: [id])\n\n @@id([groupId, eventId])\n @@map(\"event_hosting_group\")\n}\n\nenum EmploymentType {\n PARTTIME @map(\"PARTTIME\")\n FULLTIME @map(\"FULLTIME\")\n SUMMER_INTERNSHIP @map(\"SUMMER_INTERNSHIP\")\n OTHER @map(\"OTHER\")\n\n @@map(\"employment_type\")\n}\n\nmodel JobListing {\n id String @id @default(uuid())\n title String\n description String\n shortDescription String? @map(\"short_description\")\n start DateTime @db.Timestamptz(3)\n end DateTime @db.Timestamptz(3)\n featured Boolean\n hidden Boolean\n deadline DateTime? @db.Timestamptz(3)\n employment EmploymentType\n applicationLink String? @map(\"application_link\")\n applicationEmail String? @map(\"application_email\")\n ///Applications are reviewed as soon as they are submitted\n rollingAdmission Boolean @map(\"rolling_admission\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n companyId String @map(\"company_id\")\n company Company @relation(fields: [companyId], references: [id])\n\n locations JobListingLocation[]\n\n @@map(\"job_listing\")\n}\n\nmodel JobListingLocation {\n name String\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n\n jobListingId String @map(\"job_listing_id\")\n jobListing JobListing @relation(fields: [jobListingId], references: [id])\n\n @@id([name, jobListingId])\n @@map(\"job_listing_location\")\n}\n\nmodel Offline {\n id String @id @default(uuid())\n title String\n fileUrl String? @map(\"file_url\")\n imageUrl String? @map(\"image_url\")\n publishedAt DateTime @map(\"published_at\") @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@map(\"offline\")\n}\n\nmodel Article {\n id String @id @default(uuid())\n slug String @unique\n title String\n author String\n photographer String\n imageUrl String @map(\"image_url\")\n excerpt String\n content String\n isFeatured Boolean @default(false) @map(\"is_featured\")\n vimeoId String? @map(\"vimeo_id\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n tags ArticleTagLink[]\n\n @@map(\"article\")\n}\n\nmodel ArticleTag {\n name String @id\n\n articles ArticleTagLink[]\n\n @@map(\"article_tag\")\n}\n\nmodel ArticleTagLink {\n articleId String @map(\"article_id\")\n tagName String @map(\"tag_name\")\n article Article @relation(fields: [articleId], references: [id])\n tag ArticleTag @relation(fields: [tagName], references: [name])\n\n @@id([articleId, tagName])\n @@map(\"article_tag_link\")\n}\n\nenum TaskType {\n RESERVE_ATTENDEE @map(\"RESERVE_ATTENDEE\")\n CHARGE_ATTENDEE @map(\"CHARGE_ATTENDEE\")\n MERGE_ATTENDANCE_POOLS @map(\"MERGE_ATTENDANCE_POOLS\")\n VERIFY_PAYMENT @map(\"VERIFY_PAYMENT\")\n VERIFY_FEEDBACK_ANSWERED @map(\"VERIFY_FEEDBACK_ANSWERED\")\n SEND_FEEDBACK_FORM_EMAILS @map(\"SEND_FEEDBACK_FORM_EMAILS\")\n VERIFY_ATTENDEE_ATTENDED @map(\"VERIFY_ATTENDEE_ATTENDED\")\n\n @@map(\"task_type\")\n}\n\nenum TaskStatus {\n PENDING @map(\"PENDING\")\n RUNNING @map(\"RUNNING\")\n COMPLETED @map(\"COMPLETED\")\n FAILED @map(\"FAILED\")\n CANCELED @map(\"CANCELED\")\n\n @@map(\"task_status\")\n}\n\nmodel Task {\n id String @id @default(uuid())\n type TaskType\n status TaskStatus @default(PENDING)\n payload Json @default(\"{}\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n scheduledAt DateTime @map(\"scheduled_at\") @db.Timestamptz(3)\n processedAt DateTime? @map(\"processed_at\") @db.Timestamptz(3)\n\n recurringTaskId String? @map(\"recurring_task_id\")\n recurringTask RecurringTask? @relation(fields: [recurringTaskId], references: [id], onDelete: SetNull)\n\n attendancePools AttendancePool[]\n\n @@index([scheduledAt, status], name: \"idx_job_scheduled_at_status\")\n @@map(\"task\")\n}\n\nmodel RecurringTask {\n id String @id @default(uuid())\n type TaskType\n payload Json @default(\"{}\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n schedule String\n lastRunAt DateTime? @map(\"last_run_at\") @db.Timestamptz(3)\n nextRunAt DateTime @map(\"next_run_at\") @db.Timestamptz(3)\n\n tasks Task[]\n\n @@index([nextRunAt])\n @@map(\"recurring_task\")\n}\n\nenum FeedbackQuestionType {\n TEXT @map(\"TEXT\")\n LONGTEXT @map(\"LONGTEXT\")\n RATING @map(\"RATING\")\n CHECKBOX @map(\"CHECKBOX\")\n SELECT @map(\"SELECT\")\n MULTISELECT @map(\"MULTISELECT\")\n\n @@map(\"feedback_question_type\")\n}\n\nenum DeregisterReasonType {\n SCHOOL @map(\"SCHOOL\")\n WORK @map(\"WORK\")\n ECONOMY @map(\"ECONOMY\")\n TIME @map(\"TIME\")\n SICK @map(\"SICK\")\n NO_FAMILIAR_FACES @map(\"NO_FAMILIAR_FACES\")\n OTHER @map(\"OTHER\")\n\n @@map(\"deregister_reason_type\")\n}\n\nmodel FeedbackForm {\n id String @id @default(uuid())\n publicResultsToken String @unique @default(uuid()) @map(\"public_results_token\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n answerDeadline DateTime @map(\"answer_deadline\") @db.Timestamptz(3)\n\n eventId String @unique @map(\"event_id\")\n event Event @relation(fields: [eventId], references: [id])\n\n questions FeedbackQuestion[]\n answers FeedbackFormAnswer[]\n\n @@map(\"feedback_form\")\n}\n\nmodel FeedbackQuestion {\n id String @id @default(uuid())\n label String\n required Boolean @default(false)\n showInPublicResults Boolean @default(true) @map(\"show_in_public_results\")\n type FeedbackQuestionType\n order Int\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n feedbackFormId String @map(\"feedback_form_id\")\n feedbackForm FeedbackForm @relation(fields: [feedbackFormId], references: [id], onDelete: Cascade)\n\n options FeedbackQuestionOption[]\n answers FeedbackQuestionAnswer[] @relation(\"QuestionAnswers\")\n\n @@map(\"feedback_question\")\n}\n\nmodel FeedbackQuestionOption {\n id String @id @default(uuid())\n name String\n\n questionId String @map(\"question_id\")\n question FeedbackQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)\n\n selectedInAnswers FeedbackQuestionAnswerOptionLink[]\n\n @@unique([questionId, name])\n @@map(\"feedback_question_option\")\n}\n\nmodel FeedbackQuestionAnswer {\n id String @id @default(uuid())\n value Json?\n\n questionId String @map(\"question_id\")\n formAnswerId String @map(\"form_answer_id\")\n question FeedbackQuestion @relation(\"QuestionAnswers\", fields: [questionId], references: [id])\n formAnswer FeedbackFormAnswer @relation(\"FormAnswers\", fields: [formAnswerId], references: [id], onDelete: Cascade)\n\n selectedOptions FeedbackQuestionAnswerOptionLink[]\n\n @@map(\"feedback_question_answer\")\n}\n\nmodel FeedbackQuestionAnswerOptionLink {\n feedbackQuestionOptionId String @map(\"feedback_question_option_id\")\n feedbackQuestionAnswerId String @map(\"feedback_question_answer_id\")\n feedbackQuestionOption FeedbackQuestionOption @relation(fields: [feedbackQuestionOptionId], references: [id])\n feedbackQuestionAnswer FeedbackQuestionAnswer @relation(fields: [feedbackQuestionAnswerId], references: [id], onDelete: Cascade)\n\n @@id([feedbackQuestionOptionId, feedbackQuestionAnswerId])\n @@map(\"feedback_answer_option_link\")\n}\n\nmodel FeedbackFormAnswer {\n id String @id @default(uuid())\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n feedbackFormId String @map(\"feedback_form_id\")\n attendeeId String @unique @map(\"attendee_id\")\n feedbackForm FeedbackForm @relation(fields: [feedbackFormId], references: [id])\n attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)\n\n answers FeedbackQuestionAnswer[] @relation(\"FormAnswers\")\n\n @@map(\"feedback_form_answer\")\n}\n\nmodel AuditLog {\n id String @id @default(uuid())\n tableName String @map(\"table_name\")\n rowId String? @map(\"row_id\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n operation String\n rowData Json @map(\"row_data\")\n /// Database transaction id\n transactionId BigInt @map(\"transaction_id\")\n\n /// User relation is optional because the system can execute operations without a user to link it to. For example with\n /// recurring tasks.\n user User? @relation(fields: [userId], references: [id])\n userId String? @map(\"user_id\")\n\n @@map(\"audit_log\")\n}\n\nmodel DeregisterReason {\n id String @id @default(uuid())\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n registeredAt DateTime @map(\"registered_at\") @db.Timestamptz(3)\n type DeregisterReasonType\n details String?\n userGrade Int? @map(\"user_grade\")\n\n userId String @map(\"user_id\")\n eventId String @map(\"event_id\")\n user User @relation(fields: [userId], references: [id])\n event Event @relation(fields: [eventId], references: [id])\n\n @@map(\"deregister_reason\")\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -154,6 +155,16 @@ export interface PrismaClient< */ $queryRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + /** + * Executes a typed SQL query and returns a typed result + * @example + * ``` + * import { myQuery } from '@prisma/client/sql' + * + * const result = await prisma.$queryRawTyped(myQuery()) + * ``` + */ + $queryRawTyped(typedSql: runtime.TypedSql): Prisma.PrismaPromise /** * Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole. diff --git a/packages/db/generated/prisma/internal/prismaNamespace.ts b/packages/db/generated/prisma/internal/prismaNamespace.ts index 92b8976574..0ca61eb93c 100644 --- a/packages/db/generated/prisma/internal/prismaNamespace.ts +++ b/packages/db/generated/prisma/internal/prismaNamespace.ts @@ -2974,6 +2974,10 @@ export type TypeMap 0\nAND COALESCE(attendees.count, 0) < COALESCE(capacities.sum, 0)\nTHEN 1\n\nWHEN event.attendance_id IS NOT NULL\nAND NOW() < attendance.register_start\nTHEN 2\n\nWHEN event.attendance_id IS NULL\nOR COALESCE(capacities.sum, 0) = 0\nTHEN 3\n\nWHEN event.attendance_id IS NOT NULL\nAND COALESCE(capacities.sum, 0) > 0\nAND COALESCE(attendees.count, 0) >= COALESCE(capacities.sum, 0)\nTHEN 4\n\nELSE 4\nEND AS registration_bucket\n\nFROM event\nLEFT JOIN \"attendance\"\nON \"attendance\".\"id\" = event.attendance_id\nLEFT JOIN capacities\nON capacities.attendance_id = event.attendance_id\nLEFT JOIN attendees\nON attendees.attendance_id = event.attendance_id\n\nWHERE\nevent.status = 'PUBLIC'\nAND event.start > NOW()\n\nORDER BY\ntype_rank ASC,\nregistration_bucket ASC,\nevent.\"start\" ASC\n\nOFFSET $1\nLIMIT $2;") as (offset: number, limit: number) => $runtime.TypedSql + +export namespace findFeaturedEvents { + export type Parameters = [offset: number, limit: number] + export type Result = { + id: string + title: string + start: Date + end: Date + status: $DbEnums["event_status"] + description: string + short_description: string | null + image_url: string | null + location_title: string | null + location_address: string | null + location_link: string | null + attendance_id: string | null + type: $DbEnums["event_type"] + created_at: Date + updated_at: Date + metadata_import_id: number | null + parent_id: string | null + mark_for_missed_attendance: boolean + total_capacity: bigint | null + attendee_count: bigint | null + type_rank: number | null + registration_bucket: number | null + } +} diff --git a/packages/db/package.json b/packages/db/package.json index f6fb5ea018..c8a9b68241 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,7 +23,7 @@ "type-check": "tsc --noEmit", "prisma": "prisma", "migrate": "prisma migrate dev", - "generate": "prisma generate", + "generate": "prisma generate --sql", "apply-fixtures": "tsx src/fixtures.ts", "vinstraff:user-db-sync": "tsx src/vinstraff-user-db-sync.ts" }, diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts index 268bab784f..512aa9e27f 100644 --- a/packages/db/prisma.config.ts +++ b/packages/db/prisma.config.ts @@ -12,4 +12,7 @@ export default defineConfig({ migrations: { path: "./prisma/migrations/", }, + typedSql: { + path: "./prisma/sql", + }, }) diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 71b27d857e..16f82626f3 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -2,7 +2,7 @@ generator client { provider = "prisma-client" output = "../generated/prisma" - previewFeatures = ["relationJoins"] + previewFeatures = ["relationJoins", "typedSql"] } generator zod { diff --git a/packages/db/prisma/sql/findFeaturedEvents.sql b/packages/db/prisma/sql/findFeaturedEvents.sql new file mode 100644 index 0000000000..76967271e8 --- /dev/null +++ b/packages/db/prisma/sql/findFeaturedEvents.sql @@ -0,0 +1,101 @@ +-- @param {Int} $1:offset +-- @param {Int} $2:limit + +-- DOCS: https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/typedsql + +-- This SQL query is used in EventRepository#findFeaturedEvents to find featured events, as this is too complex to do +-- with Prisma's normal query API. + +-- Events will primarily be ranked by their type in the following order (lower number is higher ranking): +-- 1. GENERAL_ASSEMBLY +-- 2. COMPANY, ACADEMIC +-- 3. SOCIAL, INTERNAL, OTHER, WELCOME +-- +-- Within each bucket they will be ranked like this: +-- 1. Event in future, registration open and not full AND attendance capacities is limited (>0) +-- 2. Event in future, registration not started yet (attendance capacities does not matter) +-- 3. Event in future, no attendance registration OR attendance capacities is unlimited (=0) +-- 4. Event in future, registration full (registration status does not matter) +-- +-- Past events are not featured. We would rather have no featured events than "stale" events. + +WITH + capacities AS ( + SELECT + attendance_id, + SUM("capacity") AS sum + FROM attendance_pool + GROUP BY attendance_id + ), + + attendees AS ( + SELECT + attendance_id, + COUNT(*) AS count + FROM attendee + GROUP BY attendance_id + ) + +SELECT + event.*, + COALESCE(capacities.sum, 0) AS total_capacity, + COALESCE(attendees.count, 0) AS attendee_count, + + -- 1,2,3: event type buckets + CASE event."type" + WHEN 'GENERAL_ASSEMBLY' THEN 1 + WHEN 'COMPANY' THEN 2 + WHEN 'ACADEMIC' THEN 2 + ELSE 3 + END AS type_rank, + + -- 1-4: registration buckets + CASE + -- 1. Future, registration open and not full AND capacities limited (> 0) + WHEN event.attendance_id IS NOT NULL + AND NOW() BETWEEN attendance.register_start AND attendance.register_end + AND COALESCE(capacities.sum, 0) > 0 + AND COALESCE(attendees.count, 0) < COALESCE(capacities.sum, 0) + THEN 1 + + -- 2. Future, registration not started yet (capacities doesn't matter) + WHEN event.attendance_id IS NOT NULL + AND NOW() < attendance.register_start + THEN 2 + + -- 3. Future, no registration OR unlimited capacities (total capacities = 0) + WHEN event.attendance_id IS NULL + OR COALESCE(capacities.sum, 0) = 0 + THEN 3 + + -- 4. Future, registration full (status doesn't matter) + WHEN event.attendance_id IS NOT NULL + AND COALESCE(capacities.sum, 0) > 0 + AND COALESCE(attendees.count, 0) >= COALESCE(capacities.sum, 0) + THEN 4 + + -- Fallback: treat as bucket 4 + ELSE 4 + END AS registration_bucket + +FROM event +LEFT JOIN "attendance" + ON "attendance"."id" = event.attendance_id +LEFT JOIN capacities + ON capacities.attendance_id = event.attendance_id +LEFT JOIN attendees + ON attendees.attendance_id = event.attendance_id + +WHERE + event.status = 'PUBLIC' + -- Past events are not featured + AND event.start > NOW() + +ORDER BY + type_rank ASC, + registration_bucket ASC, + -- Tie breaker with earlier events first + event."start" ASC + +OFFSET $1 +LIMIT $2; \ No newline at end of file diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 0ca75bd94b..c053e04ab6 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -10,4 +10,5 @@ export const createPrisma = (databaseUrl: string) => { return new PrismaClient({ adapter }) } +export * as sql from "../generated/prisma/sql" export * from "../generated/prisma/client"