This repository has been archived by the owner on Oct 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ migrate graphql endpoint (#97)
Co-authored-by: Aponia <[email protected]>
- Loading branch information
Showing
15 changed files
with
1,240 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { join } from "node:path"; | ||
import { parse } from "node:url"; | ||
|
||
import { ApolloServer, HeaderMap, HTTPGraphQLRequest } from "@apollo/server"; | ||
import { | ||
ApolloServerPluginLandingPageLocalDefault, | ||
ApolloServerPluginLandingPageProductionDefault, | ||
} from "@apollo/server/plugin/landingPage/default"; | ||
import { loadFilesSync } from "@graphql-tools/load-files"; | ||
import { mergeTypeDefs } from "@graphql-tools/merge"; | ||
import { compress } from "@libs/lambda"; | ||
import type { APIGatewayProxyHandler, APIGatewayProxyResult } from "aws-lambda"; | ||
|
||
import { transformBody } from "./lib"; | ||
import { resolvers } from "./resolvers"; | ||
|
||
const responseHeaders: APIGatewayProxyResult["headers"] = { | ||
"Access-Control-Allow-Headers": "Apollo-Require-Preflight, Content-Type", | ||
"Access-Control-Allow-Origin": "*", | ||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS", | ||
}; | ||
|
||
const graphqlServer = new ApolloServer({ | ||
introspection: true, | ||
plugins: [ | ||
process.env.NODE_ENV === "development" | ||
? ApolloServerPluginLandingPageLocalDefault() | ||
: ApolloServerPluginLandingPageProductionDefault({ footer: false }), | ||
], | ||
resolvers, | ||
typeDefs: mergeTypeDefs(loadFilesSync(join(__dirname, "schema/*.graphql"))), | ||
}); | ||
|
||
export const ANY: APIGatewayProxyHandler = async (event) => { | ||
const { body, headers: eventHeaders, httpMethod: method } = event; | ||
|
||
try { | ||
graphqlServer.assertStarted(""); | ||
} catch { | ||
await graphqlServer.start(); | ||
} | ||
|
||
let headers: HeaderMap; | ||
const req: HTTPGraphQLRequest = { | ||
body, | ||
get headers() { | ||
headers ??= Object.entries(eventHeaders).reduce( | ||
(m, [k, v]) => m.set(k, Array.isArray(v) ? v.join(", ") : v ?? ""), | ||
new HeaderMap(), | ||
); | ||
return headers; | ||
}, | ||
method, | ||
search: parse(event.path ?? "").search ?? "", | ||
}; | ||
const res = await graphqlServer.executeHTTPGraphQLRequest({ | ||
httpGraphQLRequest: req, | ||
context: async () => ({}), | ||
}); | ||
|
||
const statusCode = res.status ?? 200; | ||
res.headers.forEach((v, k) => (responseHeaders[k] = v)); | ||
|
||
try { | ||
const { body, method } = compress( | ||
await transformBody(res.body), | ||
req.headers.get("accept-encoding"), | ||
); | ||
if (method) { | ||
responseHeaders["content-encoding"] = method; | ||
} | ||
return { | ||
body, | ||
headers: responseHeaders, | ||
isBase64Encoded: !!method, | ||
statusCode, | ||
}; | ||
} catch { | ||
return { | ||
body: "", | ||
headers: responseHeaders, | ||
statusCode: 500, | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { BaseContext, HTTPGraphQLResponse } from "@apollo/server"; | ||
import type { IFieldResolver } from "@graphql-tools/utils"; | ||
import { isErrorResponse } from "@peterportal-api/types"; | ||
import type { RawResponse } from "@peterportal-api/types"; | ||
import { GraphQLError } from "graphql/error"; | ||
|
||
function getBaseUrl() { | ||
switch (process.env.NODE_ENV) { | ||
case "production": | ||
return `https://api-next.peterportal.org`; | ||
case "staging": | ||
return `https://${process.env.STAGE}.api-next.peterportal.org`; | ||
default: | ||
return `http://localhost:${process.env.API_PORT ?? 8080}`; | ||
} | ||
} | ||
|
||
export async function transformBody(body: HTTPGraphQLResponse["body"]): Promise<string> { | ||
if (body.kind === "complete") { | ||
return body.string; | ||
} | ||
let transformedBody = ""; | ||
for await (const chunk of body.asyncIterator) { | ||
transformedBody += chunk; | ||
} | ||
return transformedBody; | ||
} | ||
|
||
export function geTransform(args: Record<string, string>) { | ||
if (args.ge) return { ...args, ge: args.ge.replace("_", "-") }; | ||
delete args.ge; | ||
return args; | ||
} | ||
|
||
export const proxyRestApi = | ||
( | ||
route: string, | ||
proxyArgs?: { | ||
argsTransform?: (args: Record<string, string>) => Record<string, string>; | ||
pathArg?: string; | ||
}, | ||
): IFieldResolver<never, BaseContext> => | ||
async (_source, args, _context, _info) => { | ||
const { argsTransform = (args: Record<string, string>) => args, pathArg } = proxyArgs ?? {}; | ||
const urlSearchParams = new URLSearchParams(argsTransform(args)); | ||
const query = urlSearchParams.toString(); | ||
|
||
const data: RawResponse<unknown> = await fetch( | ||
pathArg | ||
? `${getBaseUrl()}${route}/${args[pathArg]}` | ||
: `${getBaseUrl()}${route}${query ? "?" + query : ""}`, | ||
) | ||
.then((res) => res.json()) | ||
.catch((err) => { | ||
return { | ||
error: `INTERNAL_SERVER_ERROR: ${err.message}`, | ||
message: err.message, | ||
}; | ||
}); | ||
|
||
if (isErrorResponse(data)) { | ||
throw new GraphQLError(data.message, { | ||
extensions: { | ||
code: data.error.toUpperCase().replace(" ", "_"), | ||
}, | ||
}); | ||
} | ||
|
||
return data.payload; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { ApolloServerOptions, BaseContext } from "@apollo/server"; | ||
|
||
import { geTransform, proxyRestApi } from "./lib"; | ||
|
||
export const resolvers: ApolloServerOptions<BaseContext>["resolvers"] = { | ||
Query: { | ||
calendar: proxyRestApi("/v1/rest/calendar"), | ||
course: proxyRestApi("/v1/rest/courses", { pathArg: "courseId" }), | ||
courses: proxyRestApi("/v1/rest/courses", { argsTransform: geTransform }), | ||
allCourses: proxyRestApi("/v1/rest/courses/all"), | ||
rawGrades: proxyRestApi("/v1/rest/grades/raw"), | ||
aggregateGrades: proxyRestApi("/v1/rest/grades/aggregate"), | ||
gradesOptions: proxyRestApi("/v1/rest/grades/options"), | ||
instructor: proxyRestApi("/v1/rest/instructors", { pathArg: "courseId" }), | ||
instructors: proxyRestApi("/v1/rest/instructors"), | ||
allInstructors: proxyRestApi("/v1/rest/instructors/all"), | ||
larc: proxyRestApi("/v1/rest/larc"), | ||
websoc: proxyRestApi("/v1/rest/websoc", { argsTransform: geTransform }), | ||
depts: proxyRestApi("/v1/rest/websoc/depts"), | ||
terms: proxyRestApi("/v1/rest/websoc/terms"), | ||
week: proxyRestApi("/v1/rest/week"), | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"A JavaScript Date object returned as a string." | ||
scalar Date | ||
"A JavaScript object." | ||
scalar JSON | ||
|
||
type Query { | ||
_empty: String | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
"An object that includes important dates for a specified quarter." | ||
type QuarterDates { | ||
"When instruction begins for the given quarter." | ||
instructionStart: Date! | ||
"When instruction ends for the given quarter." | ||
instructionEnd: Date! | ||
"When finals begin for the given quarter." | ||
finalsStart: Date! | ||
"When finals end for the given quarter." | ||
finalsEnd: Date! | ||
} | ||
|
||
extend type Query { | ||
"Get important dates for a quarter." | ||
calendar(year: String!, quarter: Quarter!): QuarterDates! | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
"An object that represents a course." | ||
type Course { | ||
"The course ID." | ||
id: String! | ||
"The department code that the course belongs to." | ||
department: String! | ||
"The course number of the course." | ||
courseNumber: String! | ||
"The numeric part of the course number." | ||
courseNumeric: Int! | ||
"The school that the course belongs to." | ||
school: String! | ||
"The title of the course." | ||
title: String! | ||
"The level of the course." | ||
courseLevel: String! | ||
"The minimum number of units that can be earned by taking the course." | ||
minUnits: Float! | ||
"The maximum number of units that can be earned by taking the course." | ||
maxUnits: Float! | ||
"The course description." | ||
description: String! | ||
"The name of the department that the course belongs to." | ||
departmentName: String! | ||
"The UCINetIDs of all instructors who have taught this course in the past." | ||
instructorHistory: [String!]! | ||
"The prerequisite tree object for the course." | ||
prerequisiteTree: JSON! | ||
"The list of prerequisites for the course." | ||
prerequisiteList: [String!]! | ||
"The catalogue's prerequisite text for the course." | ||
prerequisiteText: String! | ||
"The courses for which this course is a prerequisite." | ||
prerequisiteFor: [String!]! | ||
"The repeat policy for this course." | ||
repeatability: String! | ||
"The grading option(s) available for this course." | ||
gradingOption: String! | ||
"The course(s) with which this course is concurrent." | ||
concurrent: String! | ||
"The course(s) that are the same as this course." | ||
sameAs: String! | ||
"The enrollment restriction(s) placed on this course." | ||
restriction: String! | ||
"The course(s) with which this course overlaps." | ||
overlap: String! | ||
"The corequisites for this course." | ||
corequisites: String! | ||
"The list of GE categories that this course fulfills." | ||
geList: [String!]! | ||
"The catalogue's GE text for this course." | ||
geText: String! | ||
"The list of terms in which this course was offered." | ||
terms: [String!]! | ||
} | ||
|
||
extend type Query { | ||
"Get the course with the given ID." | ||
course(courseId: String!): Course! | ||
"Get courses that match the given constraints." | ||
courses( | ||
"The department the courses are in." | ||
department: String | ||
"The course number of the courses." | ||
courseNumber: String | ||
"The numeric part of the course number." | ||
courseNumeric: Int | ||
"A substring of the courses' titles." | ||
titleContains: String | ||
"The level of the courses." | ||
courseLevel: Division | ||
"The minimum units of the courses." | ||
minUnits: Float | ||
"The maximum units of the courses." | ||
maxUnits: Float | ||
"A substring of the courses' descriptions." | ||
descriptionContains: String | ||
"A comma-separated list of all instructors that may have taught these courses." | ||
taughtByInstructors: String | ||
"The GE category that the courses fulfill." | ||
geCategory: GE | ||
"A comma-separated list of all terms that the courses may have been taught in." | ||
taughtInTerms: String | ||
): [Course!]! | ||
"Get all courses." | ||
allCourses: [Course!]! | ||
} |
Oops, something went wrong.