Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 2 additions & 71 deletions apps/rpc/src/modules/event/event-repository.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()),
Expand Down
15 changes: 13 additions & 2 deletions packages/db/generated/prisma/internal/class.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/db/generated/prisma/internal/prismaNamespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2974,6 +2974,10 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
args: [query: string, ...values: any[]],
result: any
}
$queryRawTyped: {
args: runtime.UnknownTypedSql,
result: JsonObject
}
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/db/generated/prisma/sql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
export { type $DbEnums } from "./sql/$DbEnums"

export * from "./sql/findFeaturedEvents"
21 changes: 21 additions & 0 deletions packages/db/generated/prisma/sql/$DbEnums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
export interface $DbEnums {
membership_type: "BACHELOR_STUDENT" | "MASTER_STUDENT" | "PHD_STUDENT" | "KNIGHT" | "SOCIAL_MEMBER" | "OTHER"
membership_specialization: "ARTIFICIAL_INTELLIGENCE" | "DATABASE_AND_SEARCH" | "INTERACTION_DESIGN" | "SOFTWARE_ENGINEERING" | "UNKNOWN"
group_type: "COMMITTEE" | "NODE_COMMITTEE" | "ASSOCIATED" | "INTEREST_GROUP"
group_member_visibility: "ALL_MEMBERS" | "WITH_ROLES" | "LEADER" | "NONE"
GroupRecruitmentMethod: "NONE" | "SPRING_APPLICATION" | "AUTUMN_APPLICATION" | "GENERAL_ASSEMBLY" | "NOMINATION" | "OTHER"
group_role_type: "LEADER" | "PUNISHER" | "TREASURER" | "COSMETIC" | "DEPUTY_LEADER" | "TRUSTEE" | "EMAIL_ONLY"
event_status: "DRAFT" | "PUBLIC" | "DELETED"
event_type: "GENERAL_ASSEMBLY" | "COMPANY" | "ACADEMIC" | "SOCIAL" | "INTERNAL" | "OTHER" | "WELCOME"
MarkType: "MANUAL" | "LATE_ATTENDANCE" | "MISSED_ATTENDANCE" | "MISSING_FEEDBACK" | "MISSING_PAYMENT"
employment_type: "PARTTIME" | "FULLTIME" | "SUMMER_INTERNSHIP" | "OTHER"
task_type: "RESERVE_ATTENDEE" | "CHARGE_ATTENDEE" | "MERGE_ATTENDANCE_POOLS" | "VERIFY_PAYMENT" | "VERIFY_FEEDBACK_ANSWERED" | "SEND_FEEDBACK_FORM_EMAILS" | "VERIFY_ATTENDEE_ATTENDED"
task_status: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "CANCELED"
feedback_question_type: "TEXT" | "LONGTEXT" | "RATING" | "CHECKBOX" | "SELECT" | "MULTISELECT"
deregister_reason_type: "SCHOOL" | "WORK" | "ECONOMY" | "TIME" | "SICK" | "NO_FAMILIAR_FACES" | "OTHER"
}
41 changes: 41 additions & 0 deletions packages/db/generated/prisma/sql/findFeaturedEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
import * as $runtime from "@prisma/client/runtime/client"
import { type $DbEnums } from "./$DbEnums"

/**
* @param offset
* @param limit
*/
export const findFeaturedEvents = $runtime.makeTypedQueryFactory("\n\n\n\nWITH\ncapacities AS (\nSELECT\nattendance_id,\nSUM(\"capacity\") AS sum\nFROM attendance_pool\nGROUP BY attendance_id\n),\n\nattendees AS (\nSELECT\nattendance_id,\nCOUNT(*) AS count\nFROM attendee\nGROUP BY attendance_id\n)\n\nSELECT\nevent.*,\nCOALESCE(capacities.sum, 0) AS total_capacity,\nCOALESCE(attendees.count, 0) AS attendee_count,\n\nCASE event.\"type\"\nWHEN 'GENERAL_ASSEMBLY' THEN 1\nWHEN 'COMPANY' THEN 2\nWHEN 'ACADEMIC' THEN 2\nELSE 3\nEND AS type_rank,\n\nCASE\nWHEN event.attendance_id IS NOT NULL\nAND NOW() BETWEEN attendance.register_start AND attendance.register_end\nAND COALESCE(capacities.sum, 0) > 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<findFeaturedEvents.Parameters, findFeaturedEvents.Result>

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
}
}
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/db/prisma.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export default defineConfig({
migrations: {
path: "./prisma/migrations/",
},
typedSql: {
path: "./prisma/sql",
},
})
2 changes: 1 addition & 1 deletion packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ generator client {
provider = "prisma-client"
output = "../generated/prisma"

previewFeatures = ["relationJoins"]
previewFeatures = ["relationJoins", "typedSql"]
}

generator zod {
Expand Down
101 changes: 101 additions & 0 deletions packages/db/prisma/sql/findFeaturedEvents.sql
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/db/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading