From 3aec27cb491a445d3988446c527d8331b0ec3d06 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:10:58 -0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(grades-updater):=20=F0=9F=90=9B=20updat?= =?UTF-8?q?e=20paths=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/grades-updater/src/lib.ts | 2 +- tools/grades-updater/src/sanitize-data.ts | 8 ++++---- tools/grades-updater/src/upload-data.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/grades-updater/src/lib.ts b/tools/grades-updater/src/lib.ts index caaa602e..f23718e5 100644 --- a/tools/grades-updater/src/lib.ts +++ b/tools/grades-updater/src/lib.ts @@ -53,7 +53,7 @@ function createLogger(): Logger { const transports: Transport[] = [ new winston.transports.Console(), new winston.transports.File({ - filename: `${__dirname}/logs/${Date.now()}.log`, + filename: `${__dirname}/../logs/${Date.now()}.log`, }), ]; return winston.createLogger({ diff --git a/tools/grades-updater/src/sanitize-data.ts b/tools/grades-updater/src/sanitize-data.ts index cc9278aa..ea2abb56 100644 --- a/tools/grades-updater/src/sanitize-data.ts +++ b/tools/grades-updater/src/sanitize-data.ts @@ -187,7 +187,7 @@ function createParser(filePath: string): Parser { async function processFile(filePath: string): Promise { const courseParser: Parser = createParser(filePath); const outputFilePath: string = resolve( - `${__dirname}/outputData/${basename(filePath, ".csv")}.output.csv`, + `${__dirname}/../outputData/${basename(filePath, ".csv")}.output.csv`, ); const stream: fs.WriteStream = fs.createWriteStream(outputFilePath, { flags: "a", @@ -221,13 +221,13 @@ async function processFile(filePath: string): Promise { * The entry point of this program. */ async function sanitizeData(): Promise { - if (!fs.existsSync(`${__dirname}/inputData`) || !fs.existsSync(`${__dirname}/outputData`)) { + if (!fs.existsSync(`${__dirname}/../inputData`) || !fs.existsSync(`${__dirname}/../outputData`)) { throw new Error("Please create /inputData and /outputData first"); } await Promise.all( fs - .readdirSync(resolve(`${__dirname}/inputData`)) - .map((file: string) => processFile(resolve(`${__dirname}/inputData/${file}`))), + .readdirSync(resolve(`${__dirname}/../inputData`)) + .map((file: string) => processFile(resolve(`${__dirname}/../inputData/${file}`))), ); } diff --git a/tools/grades-updater/src/upload-data.ts b/tools/grades-updater/src/upload-data.ts index b64d8f05..b1042dde 100644 --- a/tools/grades-updater/src/upload-data.ts +++ b/tools/grades-updater/src/upload-data.ts @@ -153,13 +153,13 @@ async function processData(sections: Section[]): Promise { * The entry point of this program. */ async function uploadData(): Promise { - if (!fs.existsSync(`${__dirname}/outputData`)) { + if (!fs.existsSync(`${__dirname}/../outputData`)) { throw new Error("Please create /outputData first"); } const paths = fs - .readdirSync(resolve(`${__dirname}/outputData`)) - .map((file: string) => resolve(`${__dirname}/outputData/${file}`)); + .readdirSync(resolve(`${__dirname}/../outputData`)) + .map((file: string) => resolve(`${__dirname}/../outputData/${file}`)); for (const path of paths) { logger.info(`Started processing ${path}`); const sections = await processFile(path); From 01ed84d0ae2c8cff87a1ed9781925b924d3b6a9a Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:43:01 -0800 Subject: [PATCH 2/5] =?UTF-8?q?ci:=20=F0=9F=91=B7=20stop=20renovate=20from?= =?UTF-8?q?=20upgrading=20to=20node20=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b29b49e..ef39f858 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ }, "packageManager": "pnpm@8.9.0", "engines": { - "node": "18", + "node": "^18", "pnpm": "8" } } From dd71f67f77ef39e55a2b40d1d1488e18a4b2b47b Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:48:13 -0800 Subject: [PATCH 3/5] =?UTF-8?q?ci:=20=F0=9F=91=B7=20try=20again=20[skip=20?= =?UTF-8?q?ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- renovate.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ef39f858..7b29b49e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ }, "packageManager": "pnpm@8.9.0", "engines": { - "node": "^18", + "node": "18", "pnpm": "8" } } diff --git a/renovate.json b/renovate.json index 572ec840..de2875f7 100644 --- a/renovate.json +++ b/renovate.json @@ -31,6 +31,11 @@ "matchPackageNames": ["constructs"], "groupName": "Constructs", "allowedVersions": "10.2.69" + }, + { + "matchPackageNames": ["node"], + "groupName": "Node.js", + "allowedVersions": "<19.0.0" } ], "pin": { "commitMessageSuffix": "[skip ci]" } From 17e4c3387fa4991a1f543c5969944bfd29bb3e7a Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:56:03 -0800 Subject: [PATCH 4/5] =?UTF-8?q?ci:=20=F0=9F=91=B7=20pin=20docusaurus=20to?= =?UTF-8?q?=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renovate.json b/renovate.json index de2875f7..05bee269 100644 --- a/renovate.json +++ b/renovate.json @@ -17,6 +17,11 @@ "labels": ["type: dependency"], "nvm": { "enabled": false }, "packageRules": [ + { + "matchPackagePatterns": ["@docusaurus/*"], + "groupName": "Docusaurus v2", + "allowedVersions": "<3.0.0" + }, { "matchPackagePatterns": ["react(?:-router)?"], "groupName": "Docusaurus v2: React dependencies", From f0bf098542fa27bb8ad62927aafd98970191bdab Mon Sep 17 00:00:00 2001 From: Aponia Date: Tue, 14 Nov 2023 19:01:50 -0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E2=9C=A8=20create=20handler=20help?= =?UTF-8?q?er=20function=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/bronya.config.ts | 11 +-- apps/api/src/routes/v1/graphql/+config.ts | 6 +- .../src/routes/v1/rest/calendar/+endpoint.ts | 42 +++++----- .../src/routes/v1/rest/courses/+endpoint.ts | 33 +++----- .../routes/v1/rest/courses/{id}/+endpoint.ts | 32 +++----- .../routes/v1/rest/grades/{id}/+endpoint.ts | 50 +++++------- .../routes/v1/rest/instructors/+endpoint.ts | 34 +++----- .../v1/rest/instructors/{id}/+endpoint.ts | 32 +++----- apps/api/src/routes/v1/rest/larc/+endpoint.ts | 17 ++-- .../src/routes/v1/rest/websoc/+endpoint.ts | 43 ++++------ .../routes/v1/rest/websoc/{id}/+endpoint.ts | 39 +++------ apps/api/src/routes/v1/rest/week/+endpoint.ts | 42 ++++------ libs/lambda/src/handler.ts | 79 +++++++++++++++++++ libs/lambda/src/index.ts | 2 + libs/lambda/src/request.ts | 7 ++ 15 files changed, 229 insertions(+), 240 deletions(-) create mode 100644 libs/lambda/src/handler.ts create mode 100644 libs/lambda/src/request.ts diff --git a/apps/api/bronya.config.ts b/apps/api/bronya.config.ts index d53e8d87..07b7921c 100644 --- a/apps/api/bronya.config.ts +++ b/apps/api/bronya.config.ts @@ -4,7 +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 { logger } from "@libs/lambda"; +import { logger, warmingRequestBody } from "@libs/lambda"; import { LambdaIntegration, ResponseType } from "aws-cdk-lib/aws-apigateway"; import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; import { RuleTargetInput, Rule, Schedule } from "aws-cdk-lib/aws-events"; @@ -79,13 +79,6 @@ const prismaSchema = resolve(libsDbDirectory, "prisma", prismaSchemaFile); */ const prismaQueryEngineFile = "libquery_engine-linux-arm64-openssl-1.0.x.so.node"; -/** - * The body of a warming request. - * - * TODO: actually recognize warming requests in the route handlers. - */ -const warmingRequestBody = { body: "warming request" }; - /** * Shared ESBuild options. */ @@ -93,7 +86,7 @@ export const esbuildOptions: BuildOptions = { format: "esm", platform: "node", bundle: true, - minify: true, + // minify: true, banner: { js }, /** diff --git a/apps/api/src/routes/v1/graphql/+config.ts b/apps/api/src/routes/v1/graphql/+config.ts index 7a361f6b..b663757c 100644 --- a/apps/api/src/routes/v1/graphql/+config.ts +++ b/apps/api/src/routes/v1/graphql/+config.ts @@ -3,7 +3,7 @@ import { join, resolve } from "node:path"; import { ApiPropsOverride } from "@bronya.js/api-construct"; -import { esbuildOptions } from "../../../../bronya.config"; +import { esbuildOptions, constructs } from "../../../../bronya.config"; export const overrides: ApiPropsOverride = { esbuild: { @@ -27,4 +27,8 @@ export const overrides: ApiPropsOverride = { }, ], }, + constructs: { + functionPlugin: constructs.functionPlugin, + restApiProps: constructs.restApiProps, + }, }; diff --git a/apps/api/src/routes/v1/rest/calendar/+endpoint.ts b/apps/api/src/routes/v1/rest/calendar/+endpoint.ts index e4657b34..4340354d 100644 --- a/apps/api/src/routes/v1/rest/calendar/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/calendar/+endpoint.ts @@ -1,36 +1,26 @@ import { PrismaClient } from "@libs/db"; -import { createOKResult, createErrorResult } from "@libs/lambda"; +import { createHandler } from "@libs/lambda"; import { getTermDateData } from "@libs/uc-irvine-api/registrar"; import type { Quarter, QuarterDates } from "@peterportal-api/types"; -import type { APIGatewayProxyHandler } from "aws-lambda"; import { ZodError } from "zod"; import { QuerySchema } from "./schema"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const query = event.queryStringParameters; const requestId = context.awsRequestId; - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } - try { const where = QuerySchema.parse(query); - const res = await prisma.calendarTerm.findFirst({ + const result = await prisma.calendarTerm.findFirst({ where, select: { instructionStart: true, @@ -40,8 +30,8 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { }, }); - if (res) { - return createOKResult(res, headers, requestId); + if (result) { + return res.createOKResult(result, headers, requestId); } const termDateData = await getTermDateData( @@ -57,19 +47,23 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { }); if (!Object.keys(termDateData).length) { - return createErrorResult( + return res.createErrorResult( 400, `The requested term, ${where.year} ${where.quarter}, is currently unavailable.`, requestId, ); } - return createOKResult(termDateData[[where.year, where.quarter].join(" ")], headers, requestId); + return res.createOKResult( + termDateData[[where.year, where.quarter].join(" ")], + headers, + requestId, + ); } catch (error) { if (error instanceof ZodError) { const messages = error.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, error, requestId); + return res.createErrorResult(400, error, requestId); } -}; +}, onWarm); diff --git a/apps/api/src/routes/v1/rest/courses/+endpoint.ts b/apps/api/src/routes/v1/rest/courses/+endpoint.ts index 1e1d129c..52f949cb 100644 --- a/apps/api/src/routes/v1/rest/courses/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/courses/+endpoint.ts @@ -1,6 +1,5 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; -import type { APIGatewayProxyHandler } from "aws-lambda"; +import { createHandler } from "@libs/lambda"; import { ZodError } from "zod"; import { constructPrismaQuery, normalizeCourse } from "./lib"; @@ -8,39 +7,31 @@ import { QuerySchema } from "./schema"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const query = event.queryStringParameters; const requestId = context.awsRequestId; - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } - try { const parsedQuery = QuerySchema.parse(query); + // The query object being empty shouldn't return all courses, since there's /courses/all for that. if (!Object.keys(parsedQuery).length) { - return createErrorResult(400, "Course number not provided", requestId); + return res.createErrorResult(400, "Course number not provided", requestId); } const courses = await prisma.course.findMany({ where: constructPrismaQuery(parsedQuery) }); - return createOKResult(courses.map(normalizeCourse), headers, requestId); + return res.createOKResult(courses.map(normalizeCourse), headers, requestId); } catch (error) { if (error instanceof ZodError) { const messages = error.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, error, requestId); + return res.createErrorResult(400, error, requestId); } -}; +}, onWarm); 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 4598cf44..5bd6e3b5 100644 --- a/apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/courses/{id}/+endpoint.ts @@ -1,40 +1,30 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; -import type { APIGatewayProxyHandler } from "aws-lambda"; +import { createHandler } from "@libs/lambda"; import { normalizeCourse } from "../lib"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const requestId = context.awsRequestId; const params = event.pathParameters; - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } - if (params?.id == null) { - return createErrorResult(400, "Course number not provided", requestId); + return res.createErrorResult(400, "Course number not provided", requestId); } if (params?.id === "all") { const courses = await prisma.course.findMany(); - return createOKResult(courses.map(normalizeCourse), headers, requestId); + return res.createOKResult(courses.map(normalizeCourse), headers, requestId); } try { - return createOKResult( + return res.createOKResult( normalizeCourse( await prisma.course.findFirstOrThrow({ where: { id: decodeURIComponent(params.id) }, @@ -44,6 +34,6 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { requestId, ); } catch { - return createErrorResult(404, `Course ${params.id} not found`, requestId); + return res.createErrorResult(404, `Course ${params.id} not found`, requestId); } -}; +}, onWarm); diff --git a/apps/api/src/routes/v1/rest/grades/{id}/+endpoint.ts b/apps/api/src/routes/v1/rest/grades/{id}/+endpoint.ts index 556fd1a4..ba76cfea 100644 --- a/apps/api/src/routes/v1/rest/grades/{id}/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/grades/{id}/+endpoint.ts @@ -1,12 +1,11 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult, logger } from "@libs/lambda"; +import { logger, createHandler } from "@libs/lambda"; import type { AggregateGradesByCourse, AggregateGradesByOffering, GradesOptions, RawGrades, } from "@peterportal-api/types"; -import type { APIGatewayProxyHandler } from "aws-lambda"; import { ZodError } from "zod"; import { @@ -21,30 +20,21 @@ import { QuerySchema } from "./schema"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const { headers, pathParameters: params, queryStringParameters: query } = event; const { awsRequestId: requestId } = context; - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } - try { const parsedQuery = QuerySchema.parse(query); switch (params?.id) { case "raw": case "aggregate": { - const res = ( + const result = ( await prisma.gradesSection.findMany({ where: constructPrismaQuery(parsedQuery), include: { instructors: true }, @@ -52,14 +42,14 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { ).map(transformRow); switch (params.id) { case "raw": - return createOKResult(res, headers, requestId); + return res.createOKResult(result, headers, requestId); case "aggregate": - return createOKResult(aggregateGrades(res), headers, requestId); + return res.createOKResult(aggregateGrades(result), headers, requestId); } } break; case "options": { - const res = await prisma.gradesSection.findMany({ + const result = await prisma.gradesSection.findMany({ where: constructPrismaQuery(parsedQuery), select: { year: true, @@ -73,7 +63,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { const departments = new Set(); const courseNumbers = new Set(); const sectionCodes = new Set(); - res.forEach(({ year, department, courseNumber, sectionCode }) => { + result.forEach(({ year, department, courseNumber, sectionCode }) => { years.add(year); departments.add(department); courseNumbers.add(courseNumber); @@ -88,7 +78,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { }), sectionCodes: Array.from(sectionCodes).sort(), }; - return createOKResult( + return res.createOKResult( { ...ret, instructors: parsedQuery.instructor @@ -111,7 +101,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { ); } case "aggregateByCourse": { - return createOKResult( + return res.createOKResult( aggregateByCourse( ( await prisma.gradesSection.findMany({ @@ -125,7 +115,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { ); } case "aggregateByOffering": { - return createOKResult( + return res.createOKResult( aggregateByOffering( ( await prisma.gradesSection.findMany({ @@ -139,7 +129,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { ); } } - return createErrorResult( + return res.createErrorResult( 400, params?.id ? `Invalid operation ${params.id}` : "Operation name not provided", requestId, @@ -147,20 +137,20 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { } catch (e) { if (e instanceof ZodError) { const messages = e.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } if (e instanceof Error) { logger.error(e.message); // findMany failing due to too many placeholders if (e.message.includes("1390")) { - return createErrorResult( + return res.createErrorResult( 400, "Your query returned too many entries. Please refine your search.", requestId, ); } - return createErrorResult(400, e.message, requestId); + return res.createErrorResult(400, e.message, requestId); } - return createErrorResult(400, e, requestId); + return res.createErrorResult(400, e, requestId); } -}; +}, onWarm); diff --git a/apps/api/src/routes/v1/rest/instructors/+endpoint.ts b/apps/api/src/routes/v1/rest/instructors/+endpoint.ts index df946501..c1a9eb0c 100644 --- a/apps/api/src/routes/v1/rest/instructors/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/instructors/+endpoint.ts @@ -1,6 +1,5 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; -import type { APIGatewayProxyHandler } from "aws-lambda"; +import { createHandler } from "@libs/lambda"; import { ZodError } from "zod"; import { constructPrismaQuery } from "./lib"; @@ -8,35 +7,26 @@ import { QuerySchema } from "./schema"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const query = event.queryStringParameters; const requestId = context.awsRequestId; - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } - try { const parsedQuery = QuerySchema.parse(query); // The query object being empty shouldn't return all courses, since there's /courses/all for that. if (!Object.keys(parsedQuery).length) - return createErrorResult(400, "Instructor UCInetID not provided", requestId); + return res.createErrorResult(400, "Instructor UCInetID not provided", requestId); const instructors = await prisma.instructor.findMany({ where: constructPrismaQuery(parsedQuery), }); if (parsedQuery.taughtInTerms) { const terms = new Set(parsedQuery.taughtInTerms); - return createOKResult( + return res.createOKResult( instructors.filter( (instructor) => [...new Set(Object.values(instructor.courseHistory as Record))] @@ -47,12 +37,12 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { requestId, ); } - return createOKResult(instructors, headers, requestId); + return res.createOKResult(instructors, headers, requestId); } catch (error) { if (error instanceof ZodError) { const messages = error.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, error, requestId); + return res.createErrorResult(400, error, requestId); } -}; +}, onWarm); 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 371dcc04..2e9d489e 100644 --- a/apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/instructors/{id}/+endpoint.ts @@ -1,38 +1,28 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; -import type { APIGatewayProxyHandler } from "aws-lambda"; +import { createHandler } from "@libs/lambda"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const requestId = context.awsRequestId; const params = event.pathParameters; - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } - if (params?.id == null) { - return createErrorResult(400, "Instructor UCInetID not provided", requestId); + return res.createErrorResult(400, "Instructor UCInetID not provided", requestId); } try { if (params.id === "all") { const instructors = await prisma.instructor.findMany(); - return createOKResult(instructors, headers, requestId); + return res.createOKResult(instructors, headers, requestId); } - return createOKResult( + return res.createOKResult( await prisma.instructor.findFirstOrThrow({ where: { ucinetid: decodeURIComponent(params.id) }, }), @@ -40,6 +30,6 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { requestId, ); } catch { - return createErrorResult(404, `Instructor ${params.id} not found`, requestId); + return res.createErrorResult(404, `Instructor ${params.id} not found`, requestId); } -}; +}, onWarm); diff --git a/apps/api/src/routes/v1/rest/larc/+endpoint.ts b/apps/api/src/routes/v1/rest/larc/+endpoint.ts index 2e1d87d5..7b624927 100644 --- a/apps/api/src/routes/v1/rest/larc/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/larc/+endpoint.ts @@ -1,5 +1,4 @@ -import { createErrorResult, createOKResult } from "@libs/lambda"; -import type { APIGatewayProxyHandler } from "aws-lambda"; +import { createHandler } from "@libs/lambda"; import { load } from "cheerio"; import { fetch } from "cross-fetch"; import { ZodError } from "zod"; @@ -7,7 +6,7 @@ import { ZodError } from "zod"; import { fmtBldg, fmtDays, fmtTime, quarterToLarcSuffix } from "./lib"; import { QuerySchema } from "./schema"; -export const GET: APIGatewayProxyHandler = async (event, context) => { +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const requestId = context.awsRequestId; const query = event.queryStringParameters; @@ -16,14 +15,14 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { const { year, quarter } = QuerySchema.parse(query); // SS10wk does not have LARC sessions apparently - if (quarter === "Summer10wk") return createOKResult([], headers, requestId); + if (quarter === "Summer10wk") return res.createOKResult([], headers, requestId); // TODO: move this code to its own scraper, and rewrite this route to fetch // data from the DB. const html = await fetch( `https://enroll.larc.uci.edu/${year}${quarterToLarcSuffix(quarter)}`, - ).then((res) => res.text()); + ).then((response) => response.text()); const $ = load(html); @@ -63,13 +62,13 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { return { courseInfo: { ...match?.groups }, sections }; }); - return createOKResult(larcSections, headers, requestId); + return res.createOKResult(larcSections, headers, requestId); } catch (e) { if (e instanceof ZodError) { const messages = e.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, e, requestId); + return res.createErrorResult(400, e, requestId); } -}; +}); diff --git a/apps/api/src/routes/v1/rest/websoc/+endpoint.ts b/apps/api/src/routes/v1/rest/websoc/+endpoint.ts index 90f3c129..f6fa9358 100644 --- a/apps/api/src/routes/v1/rest/websoc/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/websoc/+endpoint.ts @@ -1,8 +1,7 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; +import { createHandler } from "@libs/lambda"; import type { WebsocAPIResponse } from "@libs/uc-irvine-api/websoc"; import { combineAndNormalizeResponses, notNull, sortResponse } from "@libs/websoc-utils"; -import type { APIGatewayProxyHandler } from "aws-lambda"; import { ZodError } from "zod"; import { APILambdaClient } from "./APILambdaClient"; @@ -14,29 +13,15 @@ const prisma = new PrismaClient(); // let connected = false const lambdaClient = await APILambdaClient.new(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const query = event.queryStringParameters; const requestId = context.awsRequestId; - // if (!connected) { - // lambdaClient = await APILambdaClient.new(); - // try { - // await prisma.$connect(); - // connected = true; - - // /** - // * TODO: handle warmer requests. - // */ - - // // if (request.isWarmerRequest) { - // // return createOKResult("Warmed", headers, requestId); - // // } - // } catch { - // // no-op - // } - // } - try { const parsedQuery = QuerySchema.parse(query); @@ -53,7 +38,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { * cacheOnly is set to false. */ if (websocSections.length > 900 && !parsedQuery.cacheOnly) { - return createErrorResult( + return res.createErrorResult( 400, "More than 900 sections matched your query. Please refine your search.", requestId, @@ -97,7 +82,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { const combinedResponses = combineAndNormalizeResponses(...responses); - return createOKResult(sortResponse(combinedResponses), headers, requestId); + return res.createOKResult(sortResponse(combinedResponses), headers, requestId); } const websocApiResponses = websocSections @@ -106,7 +91,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { const combinedResponses = combineAndNormalizeResponses(...websocApiResponses); - return createOKResult(sortResponse(combinedResponses), headers, requestId); + return res.createOKResult(sortResponse(combinedResponses), headers, requestId); } /** @@ -115,7 +100,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { * querying WebSoc. */ if (parsedQuery.cacheOnly) { - return createOKResult({ schools: [] }, headers, requestId); + return res.createOKResult({ schools: [] }, headers, requestId); } } @@ -125,12 +110,12 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { queries: normalizeQuery(parsedQuery), }); - return createOKResult(websocResults, headers, requestId); + return res.createOKResult(websocResults, headers, requestId); } catch (error) { if (error instanceof ZodError) { const messages = error.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, error, requestId); + return res.createErrorResult(400, error, requestId); } -}; +}, onWarm); diff --git a/apps/api/src/routes/v1/rest/websoc/{id}/+endpoint.ts b/apps/api/src/routes/v1/rest/websoc/{id}/+endpoint.ts index 4becf3f0..a017cb20 100644 --- a/apps/api/src/routes/v1/rest/websoc/{id}/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/websoc/{id}/+endpoint.ts @@ -1,6 +1,5 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; -import type { APIGatewayProxyHandler } from "aws-lambda"; +import { createHandler } from "@libs/lambda"; import { ZodError } from "zod"; import { APILambdaClient } from "../APILambdaClient"; @@ -12,29 +11,15 @@ const prisma = new PrismaClient(); // let connected = false const lambdaClient = await APILambdaClient.new(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; const requestId = context.awsRequestId; const params = event.pathParameters; - // if (!connected) { - // lambdaClient = await APILambdaClient.new(); - // try { - // await prisma.$connect(); - // connected = true; - - // /** - // * TODO: handle warmer requests. - // */ - - // // if (request.isWarmerRequest) { - // // return createOKResult("Warmed", headers, requestId); - // // } - // } catch { - // // no-op - // } - // } - try { switch (params?.id) { case "terms": { @@ -86,7 +71,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { ); }); - return createOKResult(webSocTerms, headers, requestId); + return res.createOKResult(webSocTerms, headers, requestId); } case "depts": { @@ -119,16 +104,16 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { return 0; }); - return createOKResult(webSocDepts, headers, requestId); + return res.createOKResult(webSocDepts, headers, requestId); } } } catch (error) { if (error instanceof ZodError) { const messages = error.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, error, requestId); + return res.createErrorResult(400, error, requestId); } - return createErrorResult(400, "Invalid endpoint", requestId); -}; + return res.createErrorResult(400, "Invalid endpoint", requestId); +}, onWarm); diff --git a/apps/api/src/routes/v1/rest/week/+endpoint.ts b/apps/api/src/routes/v1/rest/week/+endpoint.ts index 7f47dbb9..61738cee 100644 --- a/apps/api/src/routes/v1/rest/week/+endpoint.ts +++ b/apps/api/src/routes/v1/rest/week/+endpoint.ts @@ -1,7 +1,6 @@ import { PrismaClient } from "@libs/db"; -import { createErrorResult, createOKResult } from "@libs/lambda"; +import { createHandler } from "@libs/lambda"; import type { WeekData } from "@peterportal-api/types"; -import type { APIGatewayProxyHandler } from "aws-lambda"; import { ZodError } from "zod"; import { getQuarter, getWeek } from "./lib"; @@ -9,23 +8,14 @@ import { QuerySchema } from "./schema"; const prisma = new PrismaClient(); -export const GET: APIGatewayProxyHandler = async (event, context) => { +async function onWarm() { + await prisma.$connect(); +} + +export const GET = createHandler(async (event, context, res) => { const headers = event.headers; - const query = event.queryStringParameters; const requestId = context.awsRequestId; - - /** - * TODO: handle warmer requests. - */ - - // if (request.isWarmerRequest) { - // try { - // await prisma.$connect(); - // return createOKResult("Warmed", headers, requestId); - // } catch (e) { - // createErrorResult(500, e, requestId); - // } - // } + const query = event.queryStringParameters; try { const parsedQuery = QuerySchema.parse(query); @@ -50,7 +40,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { }); // handle case of school break if (!termsInProgress.length && !termsInFinals.length) - return createOKResult( + return res.createOKResult( { weeks: [-1], quarters: ["N/A"], @@ -64,7 +54,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { const [term] = termsInProgress; const weeks = [getWeek(date, term)]; const quarters = [getQuarter(term.year, term.quarter)]; - return createOKResult( + return res.createOKResult( { weeks, quarters, @@ -78,7 +68,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { if (!termsInProgress.length && termsInFinals.length === 1) { const [term] = termsInFinals; const quarters = [getQuarter(term.year, term.quarter)]; - return createOKResult( + return res.createOKResult( { weeks: [-1], quarters, @@ -102,7 +92,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { } else { display = `Week ${week1} • ${quarter1} | Week ${week2} • ${quarter2}`; } - return createOKResult( + return res.createOKResult( { weeks: [week1, week2], quarters: [quarter1, quarter2], @@ -120,7 +110,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { const quarters = [termInProgress, termInFinals].map(({ year, quarter }) => getQuarter(year, quarter), ); - return createOKResult( + return res.createOKResult( { weeks, quarters, @@ -131,7 +121,7 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { ); } // cases above should be exhaustive but you never know - return createErrorResult( + return res.createErrorResult( 500, "Something unexpected happened. Please try again later.", requestId, @@ -139,8 +129,8 @@ export const GET: APIGatewayProxyHandler = async (event, context) => { } catch (error) { if (error instanceof ZodError) { const messages = error.issues.map((issue) => issue.message); - return createErrorResult(400, messages.join("; "), requestId); + return res.createErrorResult(400, messages.join("; "), requestId); } - return createErrorResult(400, error, requestId); + return res.createErrorResult(400, error, requestId); } -}; +}, onWarm); diff --git a/libs/lambda/src/handler.ts b/libs/lambda/src/handler.ts new file mode 100644 index 00000000..254225f6 --- /dev/null +++ b/libs/lambda/src/handler.ts @@ -0,0 +1,79 @@ +import type { APIGatewayProxyEvent, Callback, Context, APIGatewayProxyResult } from "aws-lambda"; + +import { warmingRequestBody } from "./request"; +import { createOKResult, createErrorResult } from "./response"; + +/** + * `res` object like Express.js . + */ +export type ResponseHelpers = { + /** + * Create an OK response. + */ + createOKResult: typeof createOKResult; + + /** + * Create an error response. + */ + createErrorResult: typeof createErrorResult; + + /** + * Create an ok response and send it. + */ + ok: ( + payload: T, + requestHeaders: Record, + requestId: string, + ) => void; + + /** + * Create an error response and send it. + */ + error: (statusCode: number, e: unknown, requestId: string) => void; +}; + +export type ExtendedApiGatewayHandler = ( + event: APIGatewayProxyEvent, + context: Context, + res: ResponseHelpers, +) => void | APIGatewayProxyResult | Promise; + +/** + * Override the type from aws-lambda because it's bad. + */ +export type APIGatewayProxyHandler = ( + event: APIGatewayProxyEvent, + context: Context, + callback: Callback, +) => void | Promise; + +/** + * Creates a handler for API Gateway. + * + * Handles warming requests and provides utilities for formatting responses. + */ +export function createHandler( + handler: ExtendedApiGatewayHandler, + onWarm?: ExtendedApiGatewayHandler, +): APIGatewayProxyHandler { + return async function (event, context, callback) { + const res: ResponseHelpers = { + ok: (payload, headers, requestId) => { + callback(undefined, createOKResult(payload, headers, requestId)); + }, + error: (statusCode, e, requestId) => { + callback(undefined, createErrorResult(statusCode, e, requestId)); + }, + createOKResult, + createErrorResult, + }; + + if (event.body === JSON.stringify(warmingRequestBody)) { + return onWarm + ? onWarm(event, context, res) + : createOKResult("Successfully warmed!", event.headers, context.awsRequestId); + } + + return handler(event, context, res); + }; +} diff --git a/libs/lambda/src/index.ts b/libs/lambda/src/index.ts index 4747115a..13471763 100644 --- a/libs/lambda/src/index.ts +++ b/libs/lambda/src/index.ts @@ -2,3 +2,5 @@ export * from "./logger"; export * from "./compress"; export * from "./response"; export * from "./constants"; +export * from "./request"; +export * from "./handler"; diff --git a/libs/lambda/src/request.ts b/libs/lambda/src/request.ts new file mode 100644 index 00000000..a824b231 --- /dev/null +++ b/libs/lambda/src/request.ts @@ -0,0 +1,7 @@ +/** + * The body of a warming request. + * + * A warming request is periodically sent to ensure that the Lambda function is active. + * Ideally, it wouldn't trigger any expensive computations. + */ +export const warmingRequestBody = { body: "warming request" };