Skip to content

Commit

Permalink
feat: migrate to Anteater API (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
ecxyzzy authored Nov 13, 2024
1 parent a4a1b8d commit 97265ba
Show file tree
Hide file tree
Showing 47 changed files with 541 additions and 788 deletions.
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

- [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

0 comments on commit 97265ba

Please sign in to comment.