diff --git a/src/lib/events/getEvents.ts b/src/lib/events/getEvents.ts index c9b045e81..353142aa4 100644 --- a/src/lib/events/getEvents.ts +++ b/src/lib/events/getEvents.ts @@ -1,6 +1,5 @@ import { BASIC_EVENT_FILTER } from "$lib/events/events"; import type { Prisma, PrismaClient } from "@prisma/client"; -import * as m from "$paraglide/messages"; type EventFilters = { tags?: string[]; @@ -111,19 +110,19 @@ export const getAllEvents = async ( }, ], }; - const [events, count] = await prisma.$transaction(async (tx) => { - const events = tx.event.findMany({ + // Don't run as transaction, a little read only data race is fine + const [events, count] = await Promise.all([ + prisma.event.findMany({ where, orderBy: { startDatetime: filters.pastEvents ? "desc" : "asc", }, - skip: pageNumber * pageSize, + skip: Math.max(pageNumber - 1, 0) * pageSize, take: pageSize, include, - }); - const count = tx.event.count({ where }); - return [await events, await count]; - }); + }), + prisma.event.count({ where }), + ]); return [events, Math.ceil(count / pageSize)]; }; @@ -140,11 +139,3 @@ export const getEvent = async (prisma: PrismaClient, slug: string) => { export type EventWithIncludes = NonNullable< Awaited> >; - -export const getAndValidatePage = (url: URL) => { - const page = url.searchParams.get("page"); - if (page && Number.isNaN(Number.parseInt(page))) { - throw new Error(m.events_errors_invalidPage()); - } - return page ? Math.max(Number.parseInt(page) - 1, 0) : undefined; -}; diff --git a/src/lib/news/getArticles.ts b/src/lib/news/getArticles.ts index d29cc4773..e686297a5 100644 --- a/src/lib/news/getArticles.ts +++ b/src/lib/news/getArticles.ts @@ -106,19 +106,19 @@ export const getAllArticles = async ( } : {}), }; - const [articles, count] = await prisma.$transaction(async (tx) => { - const articles = tx.article.findMany({ + // Don't run as transaction, a little read only data race is fine + const [articles, count] = await Promise.all([ + prisma.article.findMany({ where, orderBy: { publishedAt: "desc", }, - skip: pageNumber * pageSize, + skip: Math.max(pageNumber - 1, 0) * pageSize, take: pageSize, include, - }); - const count = tx.article.count({ where }); - return await Promise.all([articles, count]); - }); + }), + prisma.article.count({ where }), + ]); return [articles, Math.ceil(count / pageSize)]; }; diff --git a/src/lib/utils/semesters.test.ts b/src/lib/utils/semesters.test.ts index f1e95a3f8..c2a6c0235 100644 --- a/src/lib/utils/semesters.test.ts +++ b/src/lib/utils/semesters.test.ts @@ -1,9 +1,9 @@ import { toString, - parseSemester, semesterRange, startDate, endDate, + parseSemesterFromString, } from "./semesters"; import { describe, expect, it } from "vitest"; @@ -11,8 +11,6 @@ import { describe, expect, it } from "vitest"; describe("semester", () => { it("to string", () => expect(toString(4048)).toBe("VT 2024")); - it("parse semester", () => expect(parseSemester("VT 2024")).toBe(4048)); - it("range", () => expect(semesterRange(4048, 4050)).toEqual([4048, 4049, 4050])); @@ -28,3 +26,40 @@ describe("semester", () => { it("end date fall", () => expect(endDate(4049)).toEqual(new Date(2025, 0, 1))); }); + +describe("parseSemester", () => { + it("parses VT 2024", () => + expect(parseSemesterFromString("VT 2024", () => new Error())).toBe(4048)); + it("parses HT 2024", () => + expect(parseSemesterFromString("HT 2024", () => new Error())).toBe(4049)); + it("parses VT 2025", () => + expect(parseSemesterFromString("VT 2025", () => new Error())).toBe(4050)); + it("parses HT 2025", () => + expect(parseSemesterFromString("HT 2025", () => new Error())).toBe(4051)); + it("parses VT 2026", () => + expect(parseSemesterFromString("VT 2026", () => new Error())).toBe(4052)); + it("parses HT 2026", () => + expect(parseSemesterFromString("HT 2026", () => new Error())).toBe(4053)); + + it("throws on invalid semester", () => { + expect(() => + parseSemesterFromString("VT 202", () => new Error()), + ).toThrow(); + expect(() => + parseSemesterFromString("VT 2024a", () => new Error()), + ).toThrow(); + expect(() => + parseSemesterFromString("VT 2024 ", () => new Error()), + ).toThrow(); + expect(() => + parseSemesterFromString("XT 2024", () => new Error()), + ).toThrow(); + expect(() => + parseSemesterFromString("VT 2024 2024", () => new Error()), + ).toThrow(); + expect(() => parseSemesterFromString("VT24", () => new Error())).toThrow(); + expect(() => parseSemesterFromString("VT", () => new Error())).toThrow(); + expect(() => parseSemesterFromString("2024", () => new Error())).toThrow(); + expect(() => parseSemesterFromString("", () => new Error())).toThrow(); + }); +}); diff --git a/src/lib/utils/semesters.ts b/src/lib/utils/semesters.ts index b11124e35..51ae7b147 100644 --- a/src/lib/utils/semesters.ts +++ b/src/lib/utils/semesters.ts @@ -43,6 +43,31 @@ export const coveredSemesters = ( export const toString = (semester: Semester): string => semesterTerm(semester) + " " + semesterYear(semester); -export const parseSemester = (string: string): Semester => - (string.slice(0, 2) === "HT" ? 1 : 0) + - (parseInt(string.slice(3, 7)) ?? 2024) * 2; +/** + * Parse a semester from a string. + * @param string The string to parse + * @param errorFunction The function to call if the string is invalid + * @returns The parsed semester + * @throws If the string is invalid + */ +export const parseSemesterFromString = ( + string: string, + errorFunction: (() => Error) | (() => never), +): Semester => { + const match = string.match(/^(VT|HT) (\d{4})$/); + if (match === null) { + throw errorFunction(); + } + const [, term, year] = match; + if (term !== "VT" && term !== "HT") { + throw errorFunction(); + } + if (year === undefined) { + throw errorFunction(); + } + const yearInt = parseInt(year); + if (isNaN(yearInt)) { + throw errorFunction(); + } + return semesterFromYearAndTerm(yearInt, term); +}; diff --git a/src/lib/utils/url.server.ts b/src/lib/utils/url.server.ts new file mode 100644 index 000000000..ac98f691e --- /dev/null +++ b/src/lib/utils/url.server.ts @@ -0,0 +1,109 @@ +import { error } from "@sveltejs/kit"; +import * as m from "$paraglide/messages"; +import { + dateToSemester, + parseSemesterFromString, + type Semester, +} from "$lib/utils/semesters"; + +interface Options { + fallbackValue: number; + lowerBound: number; + upperBound: number; + errorMessage: string; +} + +/** + * Get `year` from the URL search params or throw a Svelte error if `year` is invalid. + * If `year` is not set, the default is the current year. + * @param url The URL object + * @param options The options for the function. Default is `{ fallbackValue: new Date().getFullYear(), lowerBound: 1982, upperBound: new Date().getFullYear(), errorMessage: m.error_invalid_year() }` + * @returns `year`, or the {@link fallbackValue} if `year` is not set + * @throws Svelte error if `year` is invalid + */ +export const getYearOrThrowSvelteError = ( + url: URL, + options?: Partial, +) => { + return getIntegerParamOrThrowSvelteError(url, "year", { + fallbackValue: options?.fallbackValue ?? new Date().getFullYear(), + lowerBound: options?.lowerBound ?? 1982, + upperBound: options?.upperBound ?? new Date().getFullYear(), + errorMessage: options?.errorMessage ?? m.error_invalid_year(), + }); +}; + +/** + * Get `page` from the URL search params or throw a Svelte error if `page` is invalid. + * If `page` is not set, the default is 1. + * @param url The URL object + * @param options The options for the function. Default is `{ fallbackValue: 1, lowerBound: 1, upperBound: Number.MAX_SAFE_INTEGER, errorMessage: m.error_invalid_page() }` + * @returns `page`, or the {@link fallbackValue} if `page` is not set + * @throws Svelte error if `page` is invalid + */ +export const getPageOrThrowSvelteError = ( + url: URL, + options?: Partial, +) => { + return getIntegerParamOrThrowSvelteError(url, "page", { + fallbackValue: options?.fallbackValue ?? 1, + lowerBound: options?.lowerBound ?? 1, + upperBound: options?.upperBound ?? Number.MAX_SAFE_INTEGER, + errorMessage: options?.errorMessage ?? m.error_invalid_page(), + }); +}; + +/** + * Get `pageSize` from the URL search params or throw a Svelte error if `pageSize` is invalid + * If `pageSize` is not set, the default is 10. + * @param url The URL object + * @param options The options for the function. Default is `{ fallbackValue: 10, lowerBound: 1, upperBound: 100, errorMessage: m.error_invalid_page_size() }` + * @returns `pageSize`, or the {@link fallbackValue} if `pageSize` is not set + * @throws Svelte error if `pageSize` is invalid + */ +export const getPageSizeOrThrowSvelteError = ( + url: URL, + options?: Partial, +) => { + return getIntegerParamOrThrowSvelteError(url, "pageSize", { + fallbackValue: options?.fallbackValue ?? 10, + lowerBound: options?.lowerBound ?? 1, + upperBound: options?.upperBound ?? 100, + errorMessage: options?.errorMessage ?? m.error_invalid_page_size(), + }); +}; + +/** + * Get an integer parameter from the URL object or throw a Svelte error if the parameter is invalid. + * If the parameter is not set, the default is the fallback value. + * @param url The URL object + * @param param The parameter to get + * @param options The options for the function. + * @returns The parameter, or the {@link fallbackValue} if the parameter is not set + * @throws Svelte error if the parameter is invalid + */ +export const getIntegerParamOrThrowSvelteError = ( + url: URL, + param: string, + options: Options, +): number => { + const value = parseInt( + url.searchParams.get(param) || options.fallbackValue.toString(), + ); + if (isNaN(value)) throw error(400, options.errorMessage); + if (value < options.lowerBound || value > options.upperBound) + throw error(400, options.errorMessage); + return value; +}; + +export const getSemesterOrThrowSvelteError = ( + url: URL, + fallbackValue = dateToSemester(new Date()), +): Semester => { + const semester = url.searchParams.get("semester"); + if (semester === null) return fallbackValue; + const parsed = parseSemesterFromString(semester, () => + error(400, m.error_invalid_semester()), + ); + return parsed; +}; diff --git a/src/routes/(app)/api/members/phadders/+server.ts b/src/routes/(app)/api/members/phadders/+server.ts index d328193e9..c6e707fd1 100644 --- a/src/routes/(app)/api/members/phadders/+server.ts +++ b/src/routes/(app)/api/members/phadders/+server.ts @@ -2,20 +2,16 @@ import { error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import { searchForMembers } from "../membersSearch"; import { phadderMandateFilter } from "$lib/nollning/groups/types"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; // Like member search but filters on members who were phadders during the given year export const GET: RequestHandler = async ({ locals, url }) => { const { prisma } = locals; const search = url.searchParams.get("search")?.toLowerCase(); - const year = Number.parseInt( - url.searchParams.get("year") ?? new Date().getFullYear().toString(), - ); + const year = getYearOrThrowSvelteError(url); if (search == undefined || search.length === 0) { throw error(400, "you need to provide a search value"); } - if (Number.isNaN(year)) { - throw error(400, "invalid year"); - } return new Response( JSON.stringify( diff --git a/src/routes/(app)/committees/committee.server.ts b/src/routes/(app)/committees/committee.server.ts index 2d372e4c0..69ce22724 100644 --- a/src/routes/(app)/committees/committee.server.ts +++ b/src/routes/(app)/committees/committee.server.ts @@ -6,16 +6,13 @@ import { zod } from "sveltekit-superforms/adapters"; import { message, superValidate, withFiles } from "sveltekit-superforms/server"; import { updateSchema } from "./types"; import { updateMarkdown } from "$lib/news/markdown/mutations.server"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; -export const getYear = (url: URL) => { - const yearQuery = url.searchParams.get("year"); - const parsedYear = parseInt(yearQuery ?? ""); - const year = isNaN(parsedYear) ? new Date().getFullYear() : parsedYear; - return year; -}; /** + * Load all data that every committee load function needs + * @param prisma The Prisma client * @param shortName The committee's short name - * @param year The year to load the committee for, defaults to current year + * @param url The URL object * @returns All data that the every committee load function needs */ export const committeeLoad = async ( @@ -23,7 +20,11 @@ export const committeeLoad = async ( shortName: string, url: URL, ) => { - const year = getYear(url); + const currentYear = new Date().getFullYear(); + // Allow to see committees from 1982 to the NEXT year + const year = getYearOrThrowSvelteError(url, { + upperBound: currentYear + 1, + }); const firstDayOfYear = new Date(`${year}-01-01`); const lastDayOfYear = new Date(`${year}-12-31`); diff --git a/src/routes/(app)/committees/nollu/+page.server.ts b/src/routes/(app)/committees/nollu/+page.server.ts index c75d0f593..9aeabb8f4 100644 --- a/src/routes/(app)/committees/nollu/+page.server.ts +++ b/src/routes/(app)/committees/nollu/+page.server.ts @@ -1,9 +1,14 @@ import type { PageServerLoad } from "./$types"; -import { committeeActions, committeeLoad, getYear } from "../committee.server"; +import { committeeActions, committeeLoad } from "../committee.server"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; export const load: PageServerLoad = async ({ locals, url }) => { const { prisma } = locals; - const year = getYear(url); + const currentYear = new Date().getFullYear(); + // Allow to see committees from 1982 to the NEXT year + const year = getYearOrThrowSvelteError(url, { + upperBound: currentYear + 1, + }); const phadderGroups = prisma.phadderGroup.findMany({ where: { year, diff --git a/src/routes/(app)/documents/+page.server.ts b/src/routes/(app)/documents/+page.server.ts index 868f7a469..05bc639bf 100644 --- a/src/routes/(app)/documents/+page.server.ts +++ b/src/routes/(app)/documents/+page.server.ts @@ -14,12 +14,16 @@ import { zod } from "sveltekit-superforms/adapters"; import { z } from "zod"; import type { Actions, PageServerLoad } from "./$types"; import * as m from "$paraglide/messages"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; + +const validDocumentTypes = [ + "board-meeting", + "guild-meeting", + "SRD-meeting", + "other", +] as const; +export type DocumentType = (typeof validDocumentTypes)[number]; -export type DocumentType = - | "board-meeting" - | "guild-meeting" - | "SRD-meeting" - | "other"; const prefixByType: Record = { "board-meeting": "S", "guild-meeting": "", @@ -28,9 +32,12 @@ const prefixByType: Record = { }; export const load: PageServerLoad = async ({ locals, url }) => { const { user } = locals; - const year = url.searchParams.get("year") || new Date().getFullYear(); - const type: DocumentType = - (url.searchParams.get("type") as DocumentType) || "board-meeting"; + const year = getYearOrThrowSvelteError(url); + + const type = url.searchParams.get("type") || "board-meeting"; + if (!isValidDocumentType(type)) { + throw error(400, m.documents_errors_invalidType()); + } const files = await fileHandler.getInBucket( user, @@ -80,7 +87,7 @@ export const load: PageServerLoad = async ({ locals, url }) => { !meeting.startsWith("HTM") && !meeting.startsWith("VTM") && !meeting.startsWith("S") && - meeting != year + meeting != year.toString() ); }); break; @@ -137,3 +144,6 @@ export const actions: Actions = { }); }, }; + +const isValidDocumentType = (type: string): type is DocumentType => + (validDocumentTypes as unknown as string[]).includes(type); diff --git a/src/routes/(app)/documents/governing/+page.server.ts b/src/routes/(app)/documents/governing/+page.server.ts index e0f463374..e9cbd6eeb 100644 --- a/src/routes/(app)/documents/governing/+page.server.ts +++ b/src/routes/(app)/documents/governing/+page.server.ts @@ -4,11 +4,11 @@ import { message, superValidate } from "sveltekit-superforms/server"; import { zod } from "sveltekit-superforms/adapters"; import { z } from "zod"; import * as m from "$paraglide/messages"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; export const load: PageServerLoad = async ({ locals, url }) => { const { prisma } = locals; - let year = url.searchParams.get("year") || new Date().getFullYear(); - year = typeof year === "string" ? parseInt(year) : year; + const year = getYearOrThrowSvelteError(url); const governingDocuments = await prisma.document .findMany() .then((documents) => { diff --git a/src/routes/(app)/documents/requirements/+page.server.ts b/src/routes/(app)/documents/requirements/+page.server.ts index 4ada695ce..f07026487 100644 --- a/src/routes/(app)/documents/requirements/+page.server.ts +++ b/src/routes/(app)/documents/requirements/+page.server.ts @@ -11,6 +11,7 @@ import { zod } from "sveltekit-superforms/adapters"; import { message, superValidate } from "sveltekit-superforms/server"; import { z } from "zod"; import type { Actions, PageServerLoad } from "./$types"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; export type FolderType = { id: string; @@ -22,7 +23,7 @@ export type FolderType = { export const load: PageServerLoad = async ({ locals, url }) => { const { user } = locals; - const year = url.searchParams.get("year") || new Date().getFullYear(); + const year = getYearOrThrowSvelteError(url); const files = await fileHandler.getInBucket( user, PUBLIC_BUCKETS_FILES, diff --git a/src/routes/(app)/events/EventPageLoad.ts b/src/routes/(app)/events/EventPageLoad.ts index d284e7860..c0c18ce08 100644 --- a/src/routes/(app)/events/EventPageLoad.ts +++ b/src/routes/(app)/events/EventPageLoad.ts @@ -1,6 +1,10 @@ -import { getAllEvents, getAndValidatePage } from "$lib/events/getEvents"; +import { getAllEvents } from "$lib/events/getEvents"; import { interestedGoingSchema } from "$lib/events/schema"; import { getAllTags } from "$lib/news/tags"; +import { + getPageOrThrowSvelteError, + getPageSizeOrThrowSvelteError, +} from "$lib/utils/url.server"; import type { ServerLoadEvent } from "@sveltejs/kit"; import { zod } from "sveltekit-superforms/adapters"; import { superValidate } from "sveltekit-superforms/server"; @@ -9,13 +13,21 @@ const eventPageLoad = (adminMode = false) => async ({ locals, url }: ServerLoadEvent) => { const { prisma } = locals; + const eventCount = await prisma.event.count(); + const pageSize = getPageSizeOrThrowSvelteError(url); + const page = getPageOrThrowSvelteError(url, { + fallbackValue: 1, + lowerBound: 1, + upperBound: Math.ceil(eventCount / pageSize), + }); const [[events, pageCount], allTags] = await Promise.all([ getAllEvents( prisma, { tags: url.searchParams.getAll("tags"), search: url.searchParams.get("search") ?? undefined, - page: getAndValidatePage(url), + page, + pageSize, pastEvents: url.searchParams.get("past") === "on", }, !adminMode, diff --git a/src/routes/(app)/expenses/all/+page.server.ts b/src/routes/(app)/expenses/all/+page.server.ts index 592bcdf59..95eb7c55e 100644 --- a/src/routes/(app)/expenses/all/+page.server.ts +++ b/src/routes/(app)/expenses/all/+page.server.ts @@ -1,14 +1,10 @@ import { type Actions } from "@sveltejs/kit"; import { expensesInclusion } from "../getExpenses"; import type { Prisma } from "@prisma/client"; - -const extractNumberParam = (url: URL, param: string, defaultValue: number) => { - const value = url.searchParams.get(param); - if (value == null) return defaultValue; - const parsed = parseInt(value); - if (isNaN(parsed)) return defaultValue; - return parsed; -}; +import { + getPageOrThrowSvelteError, + getPageSizeOrThrowSvelteError, +} from "$lib/utils/url.server"; const allowedFilters = ["all", "signed", "not-signed", "in-book"] as const; type Filter = (typeof allowedFilters)[number]; @@ -50,8 +46,14 @@ const whereGivenFilter = (filter: Filter): Prisma.ExpenseWhereInput => { export const load = async ({ locals, url }) => { const { prisma } = locals; - const page = extractNumberParam(url, "page", 0); - const pageSize = extractNumberParam(url, "pageSize", 10); + const allExpensesCount = await prisma.expense.count(); + const pageSize = getPageSizeOrThrowSvelteError(url); + const pageCount = Math.ceil(allExpensesCount / pageSize); + const page = getPageOrThrowSvelteError(url, { + fallbackValue: 1, + lowerBound: 1, + upperBound: pageCount, + }); const filter = extractFilter(url); const allExpenses = await prisma.expense.findMany({ @@ -61,12 +63,11 @@ export const load = async ({ locals, url }) => { }, include: expensesInclusion, take: pageSize, - skip: page * pageSize, + skip: Math.max(page - 1, 0) * pageSize, }); - const allExpensesCount = await prisma.expense.count(); return { allExpenses, - pageCount: Math.ceil(allExpensesCount / pageSize), + pageCount, }; }; diff --git a/src/routes/(app)/medals/+page.server.ts b/src/routes/(app)/medals/+page.server.ts index 8ba45aca5..2e10eaa11 100644 --- a/src/routes/(app)/medals/+page.server.ts +++ b/src/routes/(app)/medals/+page.server.ts @@ -1,17 +1,12 @@ import type { PageServerLoad } from "./$types"; -import { - type Semester, - dateToSemester, - parseSemester, -} from "$lib/utils/semesters"; +import { type Semester } from "$lib/utils/semesters"; import { medalRecipients } from "$lib/server/medals/medals"; +import { getSemesterOrThrowSvelteError } from "$lib/utils/url.server"; export const load: PageServerLoad = async ({ locals, url }) => { const { prisma } = locals; - const semester: Semester = - parseSemester(url.searchParams.get("semester") ?? "") || - dateToSemester(new Date()); + const semester: Semester = getSemesterOrThrowSvelteError(url); const recipients = await medalRecipients(prisma, semester); diff --git a/src/routes/(app)/medals/+page.svelte b/src/routes/(app)/medals/+page.svelte index 8bfaff598..e5586a4e8 100644 --- a/src/routes/(app)/medals/+page.svelte +++ b/src/routes/(app)/medals/+page.svelte @@ -11,11 +11,12 @@ type Semester, dateToSemester, toString, - parseSemester, semesterFromYearAndTerm, + parseSemesterFromString, } from "$lib/utils/semesters"; import type { PageData } from "./$types"; + import { error } from "@sveltejs/kit"; export let data: PageData; const firstSemester: Semester = semesterFromYearAndTerm(1982, "HT"); @@ -29,7 +30,11 @@ toString(currentSemester - i)} - getPageNumber={(page) => currentSemester - parseSemester(page)} + getPageNumber={(page) => + currentSemester - + parseSemesterFromString(page, () => { + throw error(400, "Invalid semester"); + })} fieldName="semester" showFirst={true} showLast={true} diff --git a/src/routes/(app)/medals/download-csv/+server.ts b/src/routes/(app)/medals/download-csv/+server.ts index b53d2ce67..ad47c8f07 100644 --- a/src/routes/(app)/medals/download-csv/+server.ts +++ b/src/routes/(app)/medals/download-csv/+server.ts @@ -1,18 +1,12 @@ import type { Member } from "@prisma/client"; -import { - type Semester, - toString, - parseSemester, - dateToSemester, -} from "$lib/utils/semesters"; +import { type Semester, toString } from "$lib/utils/semesters"; import { medalRecipients } from "$lib/server/medals/medals"; +import { getSemesterOrThrowSvelteError } from "$lib/utils/url.server"; export const GET = async ({ locals, url }) => { const { prisma } = locals; - const semester: Semester = - parseSemester(url.searchParams.get("semester") ?? "") || - dateToSemester(new Date()); + const semester: Semester = getSemesterOrThrowSvelteError(url); const recipientLines: string[] = ( await medalRecipients(prisma, semester) diff --git a/src/routes/(app)/members/+page.server.ts b/src/routes/(app)/members/+page.server.ts index 5d8eef372..ec959ec01 100644 --- a/src/routes/(app)/members/+page.server.ts +++ b/src/routes/(app)/members/+page.server.ts @@ -1,6 +1,7 @@ import { error } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; import * as m from "$paraglide/messages"; +import { getYearOrThrowSvelteError } from "$lib/utils/url.server"; const allowedProgrammes = ["D", "C", "VR/AR"]; @@ -13,10 +14,7 @@ export const load: PageServerLoad = async (request) => { if (!classProgramme || !allowedProgrammes.includes(classProgramme)) { classProgramme = "all"; } - let classYear = parseInt(request.url.searchParams.get("year") ?? ""); - if (isNaN(classYear)) { - classYear = new Date().getFullYear(); - } + const classYear = getYearOrThrowSvelteError(request.url); const members = await prisma.member.findMany({ where: { classYear, diff --git a/src/routes/(app)/news/+page.server.ts b/src/routes/(app)/news/+page.server.ts index 004f54f68..444525e2f 100644 --- a/src/routes/(app)/news/+page.server.ts +++ b/src/routes/(app)/news/+page.server.ts @@ -1,27 +1,29 @@ import { getAllArticles } from "$lib/news/getArticles"; import { getAllTags } from "$lib/news/tags"; -import * as m from "$paraglide/messages"; -import { error } from "@sveltejs/kit"; import { zod } from "sveltekit-superforms/adapters"; import { superValidate } from "sveltekit-superforms/server"; import type { Actions, PageServerLoad } from "./$types"; import { likeSchema, likesAction } from "./likes"; - -const getAndValidatePage = (url: URL) => { - const page = url.searchParams.get("page"); - if (page && Number.isNaN(Number.parseInt(page))) { - error(422, m.news_errors_invalidPage() + ` "${page}"`); - } - return page ? Math.max(Number.parseInt(page) - 1, 0) : undefined; -}; +import { + getPageOrThrowSvelteError, + getPageSizeOrThrowSvelteError, +} from "$lib/utils/url.server"; export const load: PageServerLoad = async ({ locals, url }) => { const { prisma } = locals; + const articleCount = await prisma.article.count(); + const pageSize = getPageSizeOrThrowSvelteError(url); + const page = getPageOrThrowSvelteError(url, { + fallbackValue: 1, + lowerBound: 1, + upperBound: Math.ceil(articleCount / pageSize), + }); const [[articles, pageCount], allTags] = await Promise.all([ getAllArticles(prisma, { tags: url.searchParams.getAll("tags"), search: url.searchParams.get("search") ?? undefined, - page: getAndValidatePage(url), + page, + pageSize, }), getAllTags(prisma), ]); diff --git a/src/routes/(app)/songbook/+page.server.ts b/src/routes/(app)/songbook/+page.server.ts index 2946b536b..a085dfc24 100644 --- a/src/routes/(app)/songbook/+page.server.ts +++ b/src/routes/(app)/songbook/+page.server.ts @@ -1,12 +1,18 @@ import type { Prisma } from "@prisma/client"; import type { PageServerLoad } from "./$types"; import { canAccessDeletedSongs, getExistingCategories } from "./helpers"; - -const SONGS_PER_PAGE = 10; +import { + getPageOrThrowSvelteError, + getPageSizeOrThrowSvelteError, +} from "$lib/utils/url.server"; export const load: PageServerLoad = async ({ locals, url }) => { const { prisma, user } = locals; - const page = url.searchParams.get("page"); + const songCount = await prisma.song.count(); + const pageSize = getPageSizeOrThrowSvelteError(url); + const page = getPageOrThrowSvelteError(url, { + upperBound: Math.ceil(songCount / pageSize), + }); const search = url.searchParams.get("search"); const categories = url.searchParams.getAll("category"); const accessPolicies = user?.policies ?? []; @@ -74,10 +80,8 @@ export const load: PageServerLoad = async ({ locals, url }) => { const [songs, pageCount, existingCategories] = await Promise.all([ prisma.song.findMany({ - take: SONGS_PER_PAGE, - skip: page - ? Math.max((Number.parseInt(page) - 1) * SONGS_PER_PAGE, 0) - : 0, + take: pageSize, + skip: Math.max((page - 1) * pageSize, 0), // If page is 1, we don't skip anything, otherwise we skip (page - 1) * pageSize orderBy: { title: "asc" }, where, }), @@ -112,7 +116,7 @@ export const load: PageServerLoad = async ({ locals, url }) => { return { songs: songs, - pageCount: Math.max(Math.ceil(pageCount / SONGS_PER_PAGE), 1), + pageCount: Math.ceil(pageCount / pageSize), categories, categoryMap, params: url.searchParams.toString(), diff --git a/src/translations/en.json b/src/translations/en.json index c24da3b47..f5c113e44 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -19,6 +19,7 @@ "documents_guildMeetings": "Guild Meetings", "documents_boardMeetings": "Board Meetings", "documents_srdMeetings": "SRD Meetings", + "documents_errors_invalidType": "Invalid document type", "documents_other": "Other", "theGuild": "The Guild", "theBoard": "The Board", @@ -775,6 +776,10 @@ "error_should_not_happen": "If you think this shouldn't happen please", "error_or": "or", "error_contact": "Contact DWWW", + "error_invalid_year": "Invalid year", + "error_invalid_page": "Invalid page", + "error_invalid_page_size": "Invalid page size", + "error_invalid_semester": "Invalid semester", "back": "Back", "events_cancelEvent": "Cancel event", "events_cancelled": "Cancelled", diff --git a/src/translations/sv.json b/src/translations/sv.json index e2e3c30b3..6b71faa62 100644 --- a/src/translations/sv.json +++ b/src/translations/sv.json @@ -20,6 +20,7 @@ "documents_boardMeetings": "Styrelsemöten", "documents_srdMeetings": "SRD-möten", "documents_other": "Övrigt", + "documents_errors_invalidType": "Ogiltig dokumenttyp", "theGuild": "Sektionen", "theBoard": "Styrelsen", "committees": "Utskott", @@ -771,6 +772,10 @@ "error_should_not_happen": "Om du tror att detta inte bör hända", "error_or": "eller", "error_contact": "Kontakta DWWW", + "error_invalid_year": "Ogiltigt år", + "error_invalid_page": "Ogiltig sida", + "error_invalid_page_size": "Ogiltig sidstorlek", + "error_invalid_semester": "Ogiltig termin", "back": "Tillbaka", "events_cancelEvent": "Ställ in event", "events_cancelled": "Inställt",