Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate to Anteater API #505

Merged
merged 20 commits into from
Nov 13, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ jobs:
env:
CI: false
PUBLIC_API_URL: ${{secrets.PUBLIC_API_URL}}
PUBLIC_API_GRAPHQL_URL: ${{secrets.PUBLIC_API_GRAPHQL_URL}}
DATABASE_URL: ${{ github.event_name == 'pull_request' && secrets.DEV_DATABASE_URL || secrets.PROD_DATABASE_URL }}
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
GOOGLE_CLIENT: ${{secrets.GOOGLE_CLIENT}}
Expand All @@ -72,3 +71,4 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
NODE_ENV: ${{ github.event_name == 'pull_request' && 'staging' || 'production' }}
ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }}
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
types/src/generated/
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ Features include:

## 🔨 Built with
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved

- [PeterPortal API](https://github.com/icssc/peterportal-api-next)
- [Anteater API](https://github.com/icssc/anteater-api)
- Express
- React
- tRPC
- SST and AWS CDK
- PostgreSQL
- Drizzle ORM
- GraphQL
- TypeScript
- Vite

Expand Down Expand Up @@ -109,7 +108,7 @@ Optionally, you can run the site/api separately by changing into their respectiv

## Where does the data come from?

We consolidate our data directly from official UCI sources such as: UCI Catalogue, UCI Public Records Office, and UCI WebReg (courtesy of [PeterPortal API](https://github.com/icssc/peterportal-api-next)).
We consolidate our data directly from official UCI sources such as: UCI Catalogue, UCI Public Records Office, and UCI WebReg (courtesy of [Anteater API](https://github.com/icssc/anteater-api)).

## Bug Report

Expand Down
6 changes: 3 additions & 3 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# these are the minimum variables required to run the backend
PUBLIC_API_URL=https://api-next.peterportal.org/v1/rest/
PUBLIC_API_GRAPHQL_URL=https://api-next.peterportal.org/v1/graphql
PUBLIC_API_URL=https://anteaterapi.com/v2/rest/
PORT=8080 # should match the port on the frontend proxy under site/vite.config.ts

# below are stubs of variables/secrets for the PostgreSQL database, google oauth, and recaptcha
Expand All @@ -10,4 +9,5 @@ PORT=8080 # should match the port on the frontend proxy under site/vite.config.t
# GOOGLE_CLIENT=<client>
# GOOGLE_SECRET=<secret>
# GRECAPTCHA_SECRET=<secret>
# ADMIN_EMAILS=["<your email>"]
# ADMIN_EMAILS=["<your email>"]
# ANTEATER_API_KEY=<secret>
4 changes: 4 additions & 0 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ if (process.env.GOOGLE_CLIENT && process.env.GOOGLE_SECRET) {
console.log('GOOGLE_CLIENT and/or GOOGLE_SECRET env var(s) not defined! Google login will not be available.');
}

if (!process.env.ANTEATER_API_KEY) {
console.log('ANTEATER_API_KEY env var is not defined. You will not be able to test search functionality.');
}

/**
* Configure Express.js Middleware
*/
Expand Down
48 changes: 16 additions & 32 deletions api/src/controllers/courses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,53 @@
*/

import { z } from 'zod';
import { getCourseQuery } from '../helpers/gql';
import { publicProcedure, router } from '../helpers/trpc';
import { CourseAAPIResponse, CourseBatchAAPIResponse, GradesRaw } from '@peterportal/types';
import { ANTEATER_API_REQUEST_HEADERS } from '../helpers/headers';

const coursesRouter = router({
/**
* PPAPI proxy for getting course data
* Anteater API proxy for getting course data
*/
get: publicProcedure.input(z.object({ courseID: z.string() })).query(async ({ input }) => {
const r = fetch(process.env.PUBLIC_API_URL + 'courses/' + encodeURIComponent(input.courseID), {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
const r = fetch(`${process.env.PUBLIC_API_URL}courses/${encodeURIComponent(input.courseID)}`, {
headers: ANTEATER_API_REQUEST_HEADERS,
});

return r.then((response) => response.json()).then((data) => data.payload as CourseAAPIResponse);
return r.then((response) => response.json()).then((data) => data.data as CourseAAPIResponse);
}),

/**
* PPAPI proxy for batch course data
* Anteater API proxy for batch course data
*/
batch: publicProcedure.input(z.object({ courses: z.string().array() })).mutation(async ({ input }) => {
if (input.courses.length == 0) {
return {};
} else {
const r = fetch(process.env.PUBLIC_API_GRAPHQL_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getCourseQuery(input.courses),
}),
});
const r = fetch(
`${process.env.PUBLIC_API_URL}courses/batch?ids=${input.courses.map(encodeURIComponent).join(',')}`,
{ headers: ANTEATER_API_REQUEST_HEADERS },
);

// change keys from _0,...,_x to course IDs
return r
.then((response) => response.json())
.then(
(data: CourseBatchAAPIResponse) =>
Object.fromEntries(
(Object.values(data.data) as CourseAAPIResponse[])
.filter((course) => course !== null)
.map((course) => [course.id, course]),
) as CourseBatchAAPIResponse,
(data: { data: CourseAAPIResponse[] }) =>
Object.fromEntries(data.data.map((x) => [x.id, x])) as CourseBatchAAPIResponse,
);
}
}),

/**
* PPAPI proxy for grade distribution
* Anteater API proxy for grade distribution
*/
grades: publicProcedure.input(z.object({ department: z.string(), number: z.string() })).query(async ({ input }) => {
const r = fetch(
process.env.PUBLIC_API_URL +
'grades/raw?department=' +
encodeURIComponent(input.department) +
'&courseNumber=' +
input.number,
`${process.env.PUBLIC_API_URL}grades/raw?department=${encodeURIComponent(input.department)}&courseNumber=${input.number}`,
{ headers: ANTEATER_API_REQUEST_HEADERS },
);

return r.then((response) => response.json()).then((data) => data.payload as GradesRaw);
return r.then((response) => response.json()).then((data) => data.data as GradesRaw);
}),
});

Expand Down
2 changes: 2 additions & 0 deletions api/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import roadmapsRouter from './roadmap';
import { savedCoursesRouter } from './savedCourses';
import scheduleRouter from './schedule';
import usersRouter from './users';
import searchRouter from './search';

export const appRouter = router({
courses: coursesRouter,
Expand All @@ -15,6 +16,7 @@ export const appRouter = router({
reports: reportsRouter,
reviews: reviewsRouter,
savedCourses: savedCoursesRouter,
search: searchRouter,
schedule: scheduleRouter,
users: usersRouter,
});
Expand Down
46 changes: 23 additions & 23 deletions api/src/controllers/professors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,52 @@
*/

import { z } from 'zod';
import { getProfessorQuery } from '../helpers/gql';
import { publicProcedure, router } from '../helpers/trpc';
import { GradesRaw, ProfessorAAPIResponse, ProfessorBatchAAPIResponse } from '@peterportal/types';
import { ANTEATER_API_REQUEST_HEADERS } from '../helpers/headers';

const professorsRouter = router({
/**
* PPAPI proxy for getting professor data
* Anteater API proxy for getting professor data
*/
get: publicProcedure.input(z.object({ ucinetid: z.string() })).query(async ({ input }) => {
const r = fetch(process.env.PUBLIC_API_URL + 'instructors/' + input.ucinetid);
const r = fetch(`${process.env.PUBLIC_API_URL}instructors/${input.ucinetid}`, {
headers: ANTEATER_API_REQUEST_HEADERS,
});

return r.then((response) => response.json()).then((data) => data.payload as ProfessorAAPIResponse);
return r.then((response) => response.json()).then((data) => data.data as ProfessorAAPIResponse);
}),

/**
* PPAPI proxy for batch professor data
* Anteater API proxy for batch professor data
*/
batch: publicProcedure.input(z.object({ professors: z.array(z.string()) })).mutation(async ({ input }) => {
if (input.professors.length == 0) {
return {};
} else {
const r = fetch(process.env.PUBLIC_API_GRAPHQL_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getProfessorQuery(input.professors),
}),
});

return r.then((response) => response.json()).then((data) => data.data as ProfessorBatchAAPIResponse);
const r = fetch(
`${process.env.PUBLIC_API_URL}instructors/batch?ucinetids=${input.professors.map(encodeURIComponent).join(',')}`,
{ headers: ANTEATER_API_REQUEST_HEADERS },
);

return r
.then((response) => response.json())
.then(
(data: { data: ProfessorAAPIResponse[] }) =>
Object.fromEntries(data.data.map((x) => [x.ucinetid, x])) as ProfessorBatchAAPIResponse,
);
}
}),

/**
* PPAPI proxy for grade distribution
* Anteater API proxy for grade distribution
*/
grades: publicProcedure.input(z.object({ name: z.string() })).query(async ({ input }) => {
const r = fetch(process.env.PUBLIC_API_URL + 'grades/raw?instructor=' + encodeURIComponent(input.name));
const r = fetch(`${process.env.PUBLIC_API_URL}grades/raw?instructor=${encodeURIComponent(input.name)}`, {
headers: ANTEATER_API_REQUEST_HEADERS,
});

return r
.then((response) => {
return response.json();
})
.then((data) => data.payload as GradesRaw);
return r.then((response) => response.json()).then((data) => data.data as GradesRaw);
}),
});

Expand Down
26 changes: 14 additions & 12 deletions api/src/controllers/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,43 @@
import { z } from 'zod';
import { publicProcedure, router } from '../helpers/trpc';
import { TermResponse, WebsocAPIResponse, WeekData } from '@peterportal/types';
import { ANTEATER_API_REQUEST_HEADERS } from '../helpers/headers';

const callPPAPIWebSoc = async (params: Record<string, string>) => {
const url: URL = new URL(process.env.PUBLIC_API_URL + 'websoc?' + new URLSearchParams(params));
return await fetch(url)
const callAAPIWebSoc = async (params: Record<string, string>) => {
return await fetch(`${process.env.PUBLIC_API_URL}websoc?${new URLSearchParams(params)}`, {
headers: ANTEATER_API_REQUEST_HEADERS,
})
.then((response) => response.json())
.then((json) => json.payload as WebsocAPIResponse);
.then((json) => json.data as WebsocAPIResponse);
};

const scheduleRouter = router({
/**
* Get the current week
*/
currentWeek: publicProcedure.query(async () => {
const apiResp = await fetch(`${process.env.PUBLIC_API_URL}week`);
const apiResp = await fetch(`${process.env.PUBLIC_API_URL}week`, { headers: ANTEATER_API_REQUEST_HEADERS });
const json = await apiResp.json();
return json.payload as WeekData;
return json.data as WeekData;
}),

/**
* Get the current quarter on websoc
*/
currentQuarter: publicProcedure.query(async () => {
const apiResp = await fetch(`${process.env.PUBLIC_API_URL}websoc/terms`);
const apiResp = await fetch(`${process.env.PUBLIC_API_URL}websoc/terms`, { headers: ANTEATER_API_REQUEST_HEADERS });
const json = await apiResp.json();
return (json.payload as TermResponse)[0].shortName;
return (json.data as TermResponse)[0].shortName;
}),

/**
* Proxy for WebSOC, using PeterPortal API
* Proxy for WebSOC, using Anteater API
*/
getTermDeptNum: publicProcedure
.input(z.object({ term: z.string(), department: z.string(), number: z.string() }))
.query(async ({ input }) => {
const [year, quarter] = input.term.split(' ');
const result = await callPPAPIWebSoc({
const result = await callAAPIWebSoc({
year,
quarter,
department: input.department,
Expand All @@ -49,11 +51,11 @@ const scheduleRouter = router({
}),

/**
* Proxy for WebSOC, using PeterPortal API
* Proxy for WebSOC, using Anteater API
*/
getTermProf: publicProcedure.input(z.object({ term: z.string(), professor: z.string() })).query(async ({ input }) => {
const [year, quarter] = input.term.split(' ');
const result = await callPPAPIWebSoc({
const result = await callAAPIWebSoc({
year,
quarter,
instructorName: input.professor,
Expand Down
38 changes: 38 additions & 0 deletions api/src/controllers/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
@module SearchRoute
*/

import { z } from 'zod';
import { publicProcedure, router } from '../helpers/trpc';
import { SearchAAPIResponse } from '@peterportal/types';
import { ANTEATER_API_REQUEST_HEADERS } from '../helpers/headers';

const searchRouter = router({
/**
* Anteater API proxy for fuzzy search
*/
get: publicProcedure
.input(
z.object({
query: z.string(),
skip: z
.number()
.int()
.transform((x) => x.toString()),
take: z
.number()
.int()
.transform((x) => x.toString()),
resultType: z.union([z.literal('course'), z.literal('instructor')]),
}),
)
.query(async ({ input }) => {
const r = fetch(`${process.env.PUBLIC_API_URL}search?${new URLSearchParams(input).toString()}`, {
headers: ANTEATER_API_REQUEST_HEADERS,
});

return r.then((response) => response.json()).then((data) => data.data as SearchAAPIResponse);
}),
});

export default searchRouter;
Loading
Loading