From e781a5fbf92d889999d8f276a78f9ec488199169 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sat, 9 Dec 2023 16:52:52 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20implement=20in-memory=20cac?= =?UTF-8?q?he=20for=20/courses|instructors/{id}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/bronya.config.ts | 39 ++++++++----- apps/api/src/global.d.ts | 14 ++++- apps/api/src/lib/utils.ts | 55 +++++++++++++++++++ .../src/routes/v1/rest/courses/+endpoint.ts | 4 +- apps/api/src/routes/v1/rest/courses/lib.ts | 55 +------------------ .../routes/v1/rest/courses/{id}/+config.ts | 11 ++++ .../routes/v1/rest/courses/{id}/+endpoint.ts | 32 ++--------- .../routes/v1/rest/instructors/+endpoint.ts | 3 - .../v1/rest/instructors/{id}/+config.ts | 11 ++++ .../v1/rest/instructors/{id}/+endpoint.ts | 32 +++-------- 10 files changed, 132 insertions(+), 124 deletions(-) create mode 100644 apps/api/src/routes/v1/rest/courses/{id}/+config.ts create mode 100644 apps/api/src/routes/v1/rest/instructors/{id}/+config.ts diff --git a/apps/api/bronya.config.ts b/apps/api/bronya.config.ts index 8eeb75c1..b2937b8d 100644 --- a/apps/api/bronya.config.ts +++ b/apps/api/bronya.config.ts @@ -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"; @@ -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. * @@ -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(), + )}`, + })); }, }, ], diff --git a/apps/api/src/global.d.ts b/apps/api/src/global.d.ts index 9b730474..b254f1a4 100644 --- a/apps/api/src/global.d.ts +++ b/apps/api/src/global.d.ts @@ -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; +} + +declare module "virtual:instructors" { + declare const instructors: Record; } diff --git a/apps/api/src/lib/utils.ts b/apps/api/src/lib/utils.ts index 8497192e..1e3274e6 100644 --- a/apps/api/src/lib/utils.ts +++ b/apps/api/src/lib/utils.ts @@ -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"]; /** @@ -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>).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[], + }; +} diff --git a/apps/api/src/routes/v1/rest/courses/+endpoint.ts b/apps/api/src/routes/v1/rest/courses/+endpoint.ts index 52f949cb..3dbd9e59 100644 --- a/apps/api/src/routes/v1/rest/courses/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/courses/+endpoint.ts @@ -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(); diff --git a/apps/api/src/routes/v1/rest/courses/lib.ts b/apps/api/src/routes/v1/rest/courses/lib.ts index 223011b2..aa13a485 100644 --- a/apps/api/src/routes/v1/rest/courses/lib.ts +++ b/apps/api/src/routes/v1/rest/courses/lib.ts @@ -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>).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. diff --git a/apps/api/src/routes/v1/rest/courses/{id}/+config.ts b/apps/api/src/routes/v1/rest/courses/{id}/+config.ts new file mode 100644 index 00000000..3dac3a61 --- /dev/null +++ b/apps/api/src/routes/v1/rest/courses/{id}/+config.ts @@ -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, + }, +}; diff --git a/apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts b/apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts index 5bd6e3b5..115a29b7 100644 --- a/apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts @@ -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; @@ -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); +}); diff --git a/apps/api/src/routes/v1/rest/instructors/+endpoint.ts b/apps/api/src/routes/v1/rest/instructors/+endpoint.ts index 5fce7f26..c1a9eb0c 100644 --- a/apps/api/src/routes/v1/rest/instructors/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/instructors/+endpoint.ts @@ -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() { diff --git a/apps/api/src/routes/v1/rest/instructors/{id}/+config.ts b/apps/api/src/routes/v1/rest/instructors/{id}/+config.ts new file mode 100644 index 00000000..3dac3a61 --- /dev/null +++ b/apps/api/src/routes/v1/rest/instructors/{id}/+config.ts @@ -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, + }, +}; diff --git a/apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts b/apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts index 2e9d489e..9c372c59 100644 --- a/apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts @@ -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; @@ -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); +});