Skip to content

Commit

Permalink
feat: add api for courses
Browse files Browse the repository at this point in the history
  • Loading branch information
Rei-x committed Sep 8, 2024
1 parent 3af521a commit bcb6d45
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 18 deletions.
15 changes: 11 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"fetch-cookie": "^3.0.1",
"framer-motion": "^11.3.28",
"jotai": "^2.9.3",
"lru-cache": "^11.0.1",
"lucide-react": "^0.426.0",
"next": "14.2.3",
"next-sitemap": "^4.2.3",
Expand Down
36 changes: 36 additions & 0 deletions src/app/api/data/[facultyId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextResponse } from "next/server";

import type { ApiResponse } from "@/lib/types";
import { createUsosService } from "@/lib/usos";

export const revalidate = 3600;

export async function GET(
_request: Request,
{ params }: { params: { facultyId: string } },
) {
const service = createUsosService();

return NextResponse.json(
{
registrations: await Promise.all(
(await service.getRegistrations(params.facultyId)).map(async (r) => ({
registration: r,
courses: await Promise.all(
r.related_courses.flatMap(async (c) => ({
course: await service.getCourse(c.course_id),
groups: await service.getGroups(c.course_id, c.term_id),
})),
),
})),
),
},
{
headers: {
"Cache-Control": "public, max-age=3600",
},
},
);
}

export type ApiProfileGet = ApiResponse<typeof GET>;

Check failure on line 36 in src/app/api/data/[facultyId]/route.ts

View workflow job for this annotation

GitHub Actions / lint

Type '(_request: Request, { params }: { params: { facultyId: string; }; }) => Promise<NextResponse<{ registrations: { registration: Registration; courses: { course: { id: string; name: string; }; groups: { ...; }[]; }[]; }[]; }>>' does not satisfy the constraint '(...args: unknown[]) => NextResponse<unknown> | Promise<NextResponse<unknown>>'.
10 changes: 0 additions & 10 deletions src/app/api/profile/route.ts

This file was deleted.

44 changes: 44 additions & 0 deletions src/services/usos/getRegistrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export type GetRegistrations = Registration[];
export interface Registration {
id: string;
description: Description;
message: Description;
type: Type;
status: RegistrationStatus;
is_linkage_required: boolean;
www_instance: WWWInstance;
related_courses: RelatedCourse[];
}

export interface Description {
pl: string;
en: string;
}

export interface RelatedCourse {
course_id: string;
term_id: TermID;
status: RelatedCourseStatus;
limits: null;
}

export enum RelatedCourseStatus {
RegisterAndUnregister = "register_and_unregister",
}

export enum TermID {
The202425Z = "2024/25-Z",
}

export enum RegistrationStatus {
Active = "active",
Prepared = "prepared",
}

export enum Type {
NotToken = "not_token",
}

export enum WWWInstance {
PwrWWW = "PWR_WWW",
}
61 changes: 58 additions & 3 deletions src/services/usos/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as cheerio from "cheerio";
import makeFetchCookie from "fetch-cookie";
import { LRUCache } from "lru-cache";

import type { GetCoursesCart } from "./getCoursesCart";
import type { GetCoursesEditions } from "./getCoursesEditions";
import type { GetProfile } from "./getProfile";
import type { GetRegistrationRoundCourses } from "./getRegistrationRoundCourses";
import type { GetRegistrations } from "./getRegistrations";
import { type GetTerms } from "./getTerms";
import type { GetUserRegistrations } from "./getUserRegistrations";
import { Day, Frequency, LessonType } from "./types";
Expand Down Expand Up @@ -40,6 +42,11 @@ const calculateDifference = (start: Time, end: Time) => {
};
};

const cache = new LRUCache<string, object>({
ttl: 60 * 1000,
ttlAutopurge: true,
});

export const usosService = (usosClient: UsosClient) => {
return {
getProfile: async () => {
Expand All @@ -57,7 +64,7 @@ export const usosService = (usosClient: UsosClient) => {

getUserRegistrations: async () => {
const data = await usosClient.get<GetUserRegistrations>(
"registrations/user_registrations?fields=id|description|message|type|status|is_linkage_required|www_instance|faculty|rounds|related_courses",
"registrations/user_registrations?fields=id|description|message|type|status|is_linkage_required|www_instance|faculty|rounds|related_courses&active=false",
);

return data;
Expand Down Expand Up @@ -88,6 +95,16 @@ export const usosService = (usosClient: UsosClient) => {

return data;
},
getRegistrations: async (facultyId: string) => {
return usosClient.get<GetRegistrations>(
`registrations/faculty_registrations?faculty_id=${facultyId}&fields=id|description|message|type|status|is_linkage_required|www_instance|related_courses`,
);
},
getCourses: async (coursesIds: string[]) => {
return usosClient.get(
`courses/courses&course_ids=${coursesIds.join("|")}`,
);
},
getClassGroupTimetable: async (
courseUnitId: string,
groupNumber: string,
Expand All @@ -111,6 +128,15 @@ export const usosService = (usosClient: UsosClient) => {
return data;
},
getCourse: async (courseId: string) => {
const cacheKey = `course-${courseId}`;

if (cache.has(cacheKey)) {
return cache.get(cacheKey) as Promise<{
id: string;
name: string;
}>;
}

const data = await fetchWithCookie(
`https://web.usos.pwr.edu.pl/kontroler.php?_action=katalog2/przedmioty/pokazPrzedmiot&prz_kod=${courseId}`,
);
Expand All @@ -119,13 +145,40 @@ export const usosService = (usosClient: UsosClient) => {

const name = $("h1").text();

return {
cache.set(cacheKey, {
id: courseId,
name,
};
});

return cache.get(cacheKey) as Promise<{
id: string;
name: string;
}>;
},

getGroups: async (courseId: string, term?: string) => {
const cacheKey = `groups-${courseId}-${term}`;

if (cache.has(cacheKey)) {
return cache.get(cacheKey) as Promise<
Array<{
hourStartTime: Time;
hourEndTime: Time;
duration: Time;
person: string;
personLink: string;
groupLink: string;
day: Day;
courseId: string;
type: LessonType;
nameExtended: string;
groupNumber: number;
frequency: Frequency;
name: string;
}>
>;
}

const data = await fetchWithCookie(
`https://web.usos.pwr.edu.pl/kontroler.php?_action=katalog2/przedmioty/pokazPlanZajecPrzedmiotu&prz_kod=${courseId}&plan_division=semester&plan_format=new-ui${
typeof term === "string" ? `&cdyd_kod=${term}` : ""
Expand Down Expand Up @@ -239,6 +292,8 @@ export const usosService = (usosClient: UsosClient) => {
};
});

cache.set(cacheKey, groups);

return groups;
},
};
Expand Down
19 changes: 18 additions & 1 deletion src/services/usos/usosClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { LRUCache } from "lru-cache";

import { USOS_APPS_URL } from "@/env.mjs";
import { oauth } from "@/lib/auth";

const baseUrl = `${USOS_APPS_URL}/services`;

const cache = new LRUCache<string, object>({
ttl: 60 * 60 * 1000,
ttlAutopurge: false,
max: 10000,
});

export const createClient = ({
token,
secret,
Expand All @@ -19,6 +27,10 @@ export const createClient = ({
async get<R = unknown>(endpoint: string): Promise<R> {
const url = `${baseUrl}/${endpoint}`;

if (cache.has(url)) {
return cache.get(url) as R;
}

const data = oauth.authorize(
{
url,
Expand Down Expand Up @@ -46,7 +58,12 @@ export const createClient = ({
throw new Error("Unauthorized");
}

return response.json() as Promise<R>;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const json: object = await response.json();

cache.set(url, json);

return json as Promise<R>;
},
async post<R = unknown>(endpoint: string, body: unknown): Promise<R> {
const url = `${baseUrl}/${endpoint}`;
Expand Down

0 comments on commit bcb6d45

Please sign in to comment.