Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

Commit

Permalink
feat!: 💥 ✨ normalize section meeting and final times (WIP) (#68)
Browse files Browse the repository at this point in the history
Co-authored-by: Aponia <[email protected]>
  • Loading branch information
ecxyzzy and ap0nia authored Sep 4, 2023
1 parent 6d95ea5 commit cccc382
Show file tree
Hide file tree
Showing 19 changed files with 1,181 additions and 324 deletions.
8 changes: 7 additions & 1 deletion apps/api/v1/graphql/src/graphql/enum.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ enum CancelledCourses {
Include
Only
}
"The set of valid options for filtering based on enrollment status."
"The set of valid enrollment statuses."
enum EnrollmentStatus {
OPEN
Waitl
FULL
NewOnly
}
"The set of valid final exam statuses for a section."
enum WebsocSectionFinalExamStatus {
NO_FINAL
TBA_FINAL
SCHEDULED_FINAL
}
50 changes: 43 additions & 7 deletions apps/api/v1/graphql/src/graphql/websoc.graphql
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
"The meeting time for a section."
"A type that represents the hour and minute parts of a time."
type HourMinute {
"The hour (0-23)."
hour: Int!
"The minute (0-59)."
minute: Int!
}
"The meeting information for a section."
type WebsocSectionMeeting {
"What day(s) the section meets on (e.g. ``MWF``)."
days: String!
"What time the section meets at."
time: String!
"The building(s) the section meets in."
"""
Whether the meeting time is TBA.
If this field is `false`, then `days`, `startTime`, and `endTime`
are **guaranteed** to be non-null; otherwise, they are **guaranteed** to be null.
"""
timeIsTBA: Boolean!
"The classroom(s) the section meets in."
bldg: [String!]!
"What day(s) the section meets on (e.g. ``MWF``)."
days: String
"The time at which the section begins."
startTime: HourMinute
"The time at which the section concludes."
endTime: HourMinute
}
"The enrollment statistics for a section."
type WebsocSectionEnrollment {
Expand All @@ -18,6 +34,26 @@ type WebsocSectionEnrollment {
"""
sectionEnrolled: String!
}
"The final exam data for a section."
type WebsocSectionFinalExam {
"""
The status of the exam.
If this field is `SCHEDULED_FINAL`, then all other fields are
**guaranteed** to be non-null; otherwise, they are **guaranteed** to be null.
"""
examStatus: WebsocSectionFinalExamStatus!
"The month in which the final exam takes place."
month: Int
"The day of the month in which the final exam takes place."
day: Int
"When the final exam starts."
startTime: HourMinute
"When the final exam ends."
endTime: HourMinute
"Where the final exam takes place."
bldg: String
}
"A WebSoc section object."
type WebsocSection {
"The section code."
Expand All @@ -33,7 +69,7 @@ type WebsocSection {
"The meeting time(s) of this section."
meetings: [WebsocSectionMeeting!]!
"The date and time of the final exam for this section."
finalExam: String!
finalExam: WebsocSectionFinalExam!
"The maximum capacity of this section."
maxCapacity: String!
"""
Expand Down
1 change: 1 addition & 0 deletions apps/api/v1/rest/websoc/ant.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const outDir = resolve(cwd, "./dist");

const config: AntConfigStub = {
esbuild: {
external: process.env.NODE_ENV === "development" ? [] : ["@services/websoc-proxy"],
plugins: [
cleanCopy({ cwd, outDir, prismaClientDir, prismaSchema }),
selectDelete(env.NODE_ENV, outDir),
Expand Down
2 changes: 2 additions & 0 deletions apps/api/v1/rest/websoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"dependencies": {
"@aws-sdk/client-lambda": "3.398.0",
"@libs/db": "workspace:*",
"@libs/websoc-utils": "workspace:*",
"ant-stack": "workspace:*",
"zod": "3.22.2"
},
"devDependencies": {
"@libs/build-tools": "workspace:*",
"@services/websoc-proxy": "workspace:*",
"peterportal-api-next-types": "workspace:*"
}
}
72 changes: 72 additions & 0 deletions apps/api/v1/rest/websoc/src/APILambdaClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { InvokeCommand, LambdaClient, LambdaClientConfig } from "@aws-sdk/client-lambda";
import type { WebsocAPIResponse } from "@libs/websoc-api-next";
import { zeroUUID } from "ant-stack";
import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";
import type { Department, TermData } from "peterportal-api-next-types";

/**
* A {@link `LambdaClient`} wrapper for the API to interface with the WebSoc proxy service.
*
* This class behaves slightly differently depending on the environment.
* In the development environment, instead of talking to the proxy service in AWS, it takes the code
* of the proxy service and executes it directly.
*
* Since the import process is asynchronous, instances of this class have to be created using
* the static {@link `new`} method, rather than invoking the constructor. For the same reason,
* the constructor is private, and doesn't actually do anything other than return an empty instance.
*/
export class APILambdaClient {
private client!: LambdaClient;

private service?: (
event: APIGatewayProxyEvent,
context: Context,
) => Promise<APIGatewayProxyResult>;

private constructor() {}

static async new(configuration: LambdaClientConfig = {}) {
const client = new APILambdaClient();
client.client = new LambdaClient(configuration);
if (process.env.NODE_ENV === "development") {
const { handler } = await import("@services/websoc-proxy");
client.service = handler;
} else {
client.service = undefined;
}
return client;
}

private async invoke(body: Record<string, unknown>) {
if (this.service) {
const payload = await this.service(
{ body: JSON.stringify(body) } as APIGatewayProxyEvent,
{ awsRequestId: zeroUUID } as Context,
);
return JSON.parse(payload.body);
}
const res = await this.client.send(
new InvokeCommand({
FunctionName: "peterportal-api-next-services-prod-websoc-proxy-function",
Payload: new TextEncoder().encode(JSON.stringify({ body: JSON.stringify(body) })),
}),
);
const payload = JSON.parse(Buffer.from(res.Payload ?? []).toString());
return JSON.parse(payload.body);
}

async getDepts(body: Record<string, unknown>): Promise<Department[]> {
const invocationResponse = await this.invoke(body);
return invocationResponse.payload;
}

async getTerms(body: Record<string, unknown>): Promise<TermData[]> {
const invocationResponse = await this.invoke(body);
return invocationResponse.payload;
}

async getWebsoc(body: Record<string, unknown>): Promise<WebsocAPIResponse> {
const invocationResponse = await this.invoke(body);
return invocationResponse.payload;
}
}
74 changes: 37 additions & 37 deletions apps/api/v1/rest/websoc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,51 @@
import { LambdaClient } from "@aws-sdk/client-lambda";
import { PrismaClient } from "@libs/db";
import type { WebsocAPIResponse } from "@libs/websoc-api-next";
import { combineAndNormalizeResponses, notNull, sortResponse } from "@libs/websoc-utils";
import { createErrorResult, createOKResult, type InternalHandler } from "ant-stack";
import type { WebsocAPIResponse } from "peterportal-api-next-types";
import { ZodError } from "zod";

import {
combineResponses,
constructPrismaQuery,
normalizeQuery,
notNull,
PeterPortalApiLambdaClient,
sortResponse,
} from "./lib";
import { APILambdaClient } from "./APILambdaClient";
import { constructPrismaQuery, normalizeQuery } from "./lib";
import { QuerySchema } from "./schema";

let prisma: PrismaClient;

const lambda = new LambdaClient({});

const lambdaClient = new PeterPortalApiLambdaClient(lambda);

const quarterOrder = ["Winter", "Spring", "Summer1", "Summer10wk", "Summer2", "Fall"];

let prisma: PrismaClient;
let connected = false;
let lambdaClient: APILambdaClient;

export const GET: InternalHandler = async (request) => {
const { headers, params, query, requestId } = request;

prisma ??= new PrismaClient();
lambdaClient ??= await APILambdaClient.new();

if (request.isWarmerRequest) {
if (!connected) {
try {
await prisma.$connect();
return createOKResult("Warmed", headers, requestId);
} catch (error) {
createErrorResult(500, error, requestId);
connected = true;
if (request.isWarmerRequest) {
return createOKResult("Warmed", headers, requestId);
}
} catch {
// no-op
}
}

try {
switch (params?.id) {
case "terms": {
const [gradesTerms, webSocTerms] = await Promise.all([
prisma.gradesSection.findMany({
distinct: ["year", "quarter"],
select: {
year: true,
quarter: true,
},
orderBy: [{ year: "desc" }, { quarter: "desc" }],
}),
connected
? prisma.gradesSection.findMany({
distinct: ["year", "quarter"],
select: {
year: true,
quarter: true,
},
orderBy: [{ year: "desc" }, { quarter: "desc" }],
})
: [],
lambdaClient.getTerms({ function: "terms" }),
]);

Expand Down Expand Up @@ -92,12 +90,14 @@ export const GET: InternalHandler = async (request) => {

case "depts": {
const [gradesDepts, webSocDepts] = await Promise.all([
prisma.gradesSection.findMany({
distinct: ["department"],
select: {
department: true,
},
}),
connected
? prisma.gradesSection.findMany({
distinct: ["department"],
select: {
department: true,
},
})
: [],
lambdaClient.getDepts({ function: "depts" }),
]);

Expand Down Expand Up @@ -126,7 +126,7 @@ export const GET: InternalHandler = async (request) => {

const parsedQuery = QuerySchema.parse(query);

if (parsedQuery.cache) {
if (connected && parsedQuery.cache) {
const websocSections = await prisma.websocSection.findMany({
where: constructPrismaQuery(parsedQuery),
select: { department: true, courseNumber: true, data: true },
Expand Down Expand Up @@ -181,7 +181,7 @@ export const GET: InternalHandler = async (request) => {
.map((x) => x.data)
.filter(notNull) as WebsocAPIResponse[];

const combinedResponses = combineResponses(...responses);
const combinedResponses = combineAndNormalizeResponses(...responses);

return createOKResult(sortResponse(combinedResponses), headers, requestId);
}
Expand All @@ -190,7 +190,7 @@ export const GET: InternalHandler = async (request) => {
.map((x) => x.data)
.filter(notNull) as WebsocAPIResponse[];

const combinedResponses = combineResponses(...websocApiResponses);
const combinedResponses = combineAndNormalizeResponses(...websocApiResponses);

return createOKResult(sortResponse(combinedResponses), headers, requestId);
}
Expand Down
Loading

0 comments on commit cccc382

Please sign in to comment.