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

Commit

Permalink
feat: ✨ implement in-memory cache for /courses|instructors/{id}
Browse files Browse the repository at this point in the history
  • Loading branch information
ecxyzzy committed Dec 10, 2023
1 parent ff1f32a commit e781a5f
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 124 deletions.
39 changes: 25 additions & 14 deletions apps/api/bronya.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
import { Api, type ApiConstructProps } from "@bronya.js/api-construct";
import { createApiCliPlugins } from "@bronya.js/api-construct/plugins/cli";
import { isCdk } from "@bronya.js/core";
import { PrismaClient } from "@libs/db";
import { logger, warmingRequestBody } from "@libs/lambda";
import { LambdaIntegration, ResponseType } from "aws-cdk-lib/aws-apigateway";
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
Expand All @@ -16,6 +17,10 @@ import { App, Stack, Duration } from "aws-cdk-lib/core";
import { config } from "dotenv";
import type { BuildOptions } from "esbuild";

import { normalizeCourse } from "./src/lib/utils";

const prisma = new PrismaClient();

/**
* Whether we're executing in CDK.
*
Expand Down Expand Up @@ -107,20 +112,26 @@ export const esbuildOptions: BuildOptions = {
{
name: "in-memory-cache",
setup: (build) => {
build.onResolve({ filter: /INSTRUCTORS/ }, (args) => {
return {
path: args.path,
namespace,
};
});

build.onLoad({ filter: /INSTRUCTORS/, namespace }, async () => {
const instructors = ["johndoe", "janedoe", "johndoe2"]; // await prisma.instructors(...)

return {
contents: `export const instructors = ${JSON.stringify(instructors)}`,
};
});
build.onResolve({ filter: /virtual:courses/ }, (args) => ({
path: args.path,
namespace,
}));
build.onResolve({ filter: /virtual:instructors/ }, (args) => ({
path: args.path,
namespace,
}));
build.onLoad({ filter: /virtual:courses/, namespace }, async () => ({
contents: `export const courses = ${JSON.stringify(
Object.fromEntries(
(await prisma.course.findMany()).map(normalizeCourse).map((x) => [x.id, x]),
),
)}`,
}));
build.onLoad({ filter: /virtual:instructors/, namespace }, async () => ({
contents: `export const instructors = ${JSON.stringify(
await prisma.instructor.findMany(),
)}`,
}));
},
},
],
Expand Down
14 changes: 12 additions & 2 deletions apps/api/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
declare module "INSTRUCTORS" {
declare const instructors: string[];
// These are the type declarations for the virtual modules
// used by the `/courses/{id}` and `/instructors/{id}` routes.
// The reason we have to use inline imports here is because with `import` statements, TypeScript
// treats this as a normal module rather than an ambient module, and so the module declarations
// don't actually work when consumed by other TypeScript source files.

declare module "virtual:courses" {
declare const courses: Record<string, import("@peterportal-api/types").Course>;
}

declare module "virtual:instructors" {
declare const instructors: Record<string, import("@peterportal-api/types").Instructor>;
}
55 changes: 55 additions & 0 deletions apps/api/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Course as PrismaCourse, CourseLevel as PrismaCourseLevel } from "@libs/db";
import { Course, CourseLevel, GE, GECategory, PrerequisiteTree } from "@peterportal-api/types";

const days = ["Su", "M", "Tu", "W", "Th", "F", "Sa"];

/**
Expand Down Expand Up @@ -35,3 +38,55 @@ export const flattenDayStringsAndSplit = (value: TransformInput): TransformOutpu
),
)
: undefined;

export function normalizeCourse(course: PrismaCourse): Course {
let courseLevel: CourseLevel;
switch (course.courseLevel as PrismaCourseLevel) {
case "LowerDiv":
courseLevel = "Lower Division (1-99)";
break;
case "UpperDiv":
courseLevel = "Upper Division (100-199)";
break;
case "Graduate":
courseLevel = "Graduate/Professional Only (200+)";
break;
}
const geList = (course.geList as Array<Omit<GE, "ANY">>).map((x): GECategory => {
switch (x) {
case "GE-1A":
return "GE Ia: Lower Division Writing";
case "GE-1B":
return "GE Ib: Upper Division Writing";
case "GE-2":
return "GE II: Science and Technology";
case "GE-3":
return "GE III: Social & Behavioral Sciences";
case "GE-4":
return "GE IV: Arts and Humanities";
case "GE-5A":
return "GE Va: Quantitative Literacy";
case "GE-5B":
return "GE Vb: Formal Reasoning";
case "GE-6":
return "GE VI: Language Other Than English";
case "GE-7":
return "GE VII: Multicultural Studies";
case "GE-8":
return "GE VIII: International/Global Issues";
// this branch should never happen
default:
throw new Error();
}
});
return {
...course,
courseLevel,
instructorHistory: course.instructorHistory as unknown as string[],
prerequisiteTree: course.prerequisiteTree as unknown as PrerequisiteTree,
prerequisiteList: course.prerequisiteList as unknown as string[],
prerequisiteFor: course.prerequisiteFor as unknown as string[],
geList,
terms: course.terms as unknown as string[],
};
}
4 changes: 3 additions & 1 deletion apps/api/src/routes/v1/rest/courses/+endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { PrismaClient } from "@libs/db";
import { createHandler } from "@libs/lambda";
import { ZodError } from "zod";

import { constructPrismaQuery, normalizeCourse } from "./lib";
import { normalizeCourse } from "../../../../lib/utils";

import { constructPrismaQuery } from "./lib";
import { QuerySchema } from "./schema";

const prisma = new PrismaClient();
Expand Down
55 changes: 1 addition & 54 deletions apps/api/src/routes/v1/rest/courses/lib.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,7 @@
import { Course as PrismaCourse, CourseLevel as PrismaCourseLevel, Prisma } from "@libs/db";
import { Course, CourseLevel, GE, GECategory, PrerequisiteTree } from "@peterportal-api/types";
import { Prisma } from "@libs/db";

import { Query } from "./schema";

export function normalizeCourse(course: PrismaCourse): Course {
let courseLevel: CourseLevel;
switch (course.courseLevel as PrismaCourseLevel) {
case "LowerDiv":
courseLevel = "Lower Division (1-99)";
break;
case "UpperDiv":
courseLevel = "Upper Division (100-199)";
break;
case "Graduate":
courseLevel = "Graduate/Professional Only (200+)";
break;
}
const geList = (course.geList as Array<Omit<GE, "ANY">>).map((x): GECategory => {
switch (x) {
case "GE-1A":
return "GE Ia: Lower Division Writing";
case "GE-1B":
return "GE Ib: Upper Division Writing";
case "GE-2":
return "GE II: Science and Technology";
case "GE-3":
return "GE III: Social & Behavioral Sciences";
case "GE-4":
return "GE IV: Arts and Humanities";
case "GE-5A":
return "GE Va: Quantitative Literacy";
case "GE-5B":
return "GE Vb: Formal Reasoning";
case "GE-6":
return "GE VI: Language Other Than English";
case "GE-7":
return "GE VII: Multicultural Studies";
case "GE-8":
return "GE VIII: International/Global Issues";
// this branch should never happen
default:
throw new Error();
}
});
return {
...course,
courseLevel,
instructorHistory: course.instructorHistory as unknown as string[],
prerequisiteTree: course.prerequisiteTree as unknown as PrerequisiteTree,
prerequisiteList: course.prerequisiteList as unknown as string[],
prerequisiteFor: course.prerequisiteFor as unknown as string[],
geList,
terms: course.terms as unknown as string[],
};
}

/**
* Constructs a Prisma query for the given filter parameters.
* @param parsedQuery The query object parsed by Zod.
Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/routes/v1/rest/courses/{id}/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiPropsOverride } from "@bronya.js/api-construct";

import { esbuildOptions, constructs } from "../../../../../../bronya.config";

export const overrides: ApiPropsOverride = {
esbuild: esbuildOptions,
constructs: {
functionPlugin: constructs.functionPlugin,
restApiProps: constructs.restApiProps,
},
};
32 changes: 6 additions & 26 deletions apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { PrismaClient } from "@libs/db";
import { createHandler } from "@libs/lambda";

import { normalizeCourse } from "../lib";

const prisma = new PrismaClient();

async function onWarm() {
await prisma.$connect();
}
import { courses } from "virtual:courses";

export const GET = createHandler(async (event, context, res) => {
const headers = event.headers;
Expand All @@ -17,23 +9,11 @@ export const GET = createHandler(async (event, context, res) => {
if (params?.id == null) {
return res.createErrorResult(400, "Course number not provided", requestId);
}

if (params?.id === "all") {
const courses = await prisma.course.findMany();
return res.createOKResult(courses.map(normalizeCourse), headers, requestId);
return res.createOKResult(courses, headers, requestId);
}

try {
return res.createOKResult(
normalizeCourse(
await prisma.course.findFirstOrThrow({
where: { id: decodeURIComponent(params.id) },
}),
),
headers,
requestId,
);
} catch {
return res.createErrorResult(404, `Course ${params.id} not found`, requestId);
if (courses[decodeURIComponent(params.id)]) {
return res.createOKResult(courses[decodeURIComponent(params.id)], headers, requestId);
}
}, onWarm);
return res.createErrorResult(404, `Course ${params.id} not found`, requestId);
});
3 changes: 0 additions & 3 deletions apps/api/src/routes/v1/rest/instructors/+endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { PrismaClient } from "@libs/db";
import { createHandler } from "@libs/lambda";
import { instructors } from "INSTRUCTORS";
import { ZodError } from "zod";

import { constructPrismaQuery } from "./lib";
import { QuerySchema } from "./schema";

console.log({ instructors });

const prisma = new PrismaClient();

async function onWarm() {
Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/routes/v1/rest/instructors/{id}/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiPropsOverride } from "@bronya.js/api-construct";

import { esbuildOptions, constructs } from "../../../../../../bronya.config";

export const overrides: ApiPropsOverride = {
esbuild: esbuildOptions,
constructs: {
functionPlugin: constructs.functionPlugin,
restApiProps: constructs.restApiProps,
},
};
32 changes: 8 additions & 24 deletions apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { PrismaClient } from "@libs/db";
import { createHandler } from "@libs/lambda";

const prisma = new PrismaClient();

async function onWarm() {
await prisma.$connect();
}
import { instructors } from "virtual:instructors";

export const GET = createHandler(async (event, context, res) => {
const headers = event.headers;
Expand All @@ -15,21 +9,11 @@ export const GET = createHandler(async (event, context, res) => {
if (params?.id == null) {
return res.createErrorResult(400, "Instructor UCInetID not provided", requestId);
}

try {
if (params.id === "all") {
const instructors = await prisma.instructor.findMany();
return res.createOKResult(instructors, headers, requestId);
}

return res.createOKResult(
await prisma.instructor.findFirstOrThrow({
where: { ucinetid: decodeURIComponent(params.id) },
}),
headers,
requestId,
);
} catch {
return res.createErrorResult(404, `Instructor ${params.id} not found`, requestId);
if (params.id === "all") {
return res.createOKResult(instructors, headers, requestId);
}
if (instructors[decodeURIComponent(params.id)]) {
return res.createOKResult(instructors[decodeURIComponent(params.id)], headers, requestId);
}
}, onWarm);
return res.createErrorResult(404, `Instructor ${params.id} not found`, requestId);
});

0 comments on commit e781a5f

Please sign in to comment.