From 70addf532bd4ad4ded010b640ed7fa1f52814713 Mon Sep 17 00:00:00 2001 From: Christopher Kwong Date: Tue, 30 Apr 2024 22:53:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(study-rooms):=20=E2=9C=A8=20implement=20st?= =?UTF-8?q?udy=20room=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/bronya.config.ts | 13 +- apps/api/src/global.d.ts | 5 + apps/api/src/lib/utils.ts | 15 +- apps/api/src/routes/v1/graphql/resolvers.ts | 2 + .../v1/graphql/schema/studyRooms.graphql | 46 ++++++ .../routes/v1/rest/studyRooms/+endpoint.ts | 30 ++++ apps/api/src/routes/v1/rest/studyRooms/lib.ts | 60 +++++++ .../src/routes/v1/rest/studyRooms/schema.ts | 13 ++ .../routes/v1/rest/studyRooms/{id}/+config.ts | 11 ++ .../v1/rest/studyRooms/{id}/+endpoint.ts | 41 +++++ .../routes/v1/rest/studyRooms/{id}/schema.ts | 12 ++ .../rest-api/reference/study-rooms.md | 153 ++++++++++++++++++ apps/docs/sidebars.js | 1 + libs/uc-irvine-lib/src/spaces/index.ts | 12 +- packages/types/index.ts | 2 +- packages/types/types/studyroom.ts | 6 + tools/study-room-scraper/src/index.ts | 14 +- .../src/study-room-scraper.ts | 64 ++++---- 18 files changed, 446 insertions(+), 54 deletions(-) create mode 100644 apps/api/src/routes/v1/graphql/schema/studyRooms.graphql create mode 100644 apps/api/src/routes/v1/rest/studyRooms/+endpoint.ts create mode 100644 apps/api/src/routes/v1/rest/studyRooms/lib.ts create mode 100644 apps/api/src/routes/v1/rest/studyRooms/schema.ts create mode 100644 apps/api/src/routes/v1/rest/studyRooms/{id}/+config.ts create mode 100644 apps/api/src/routes/v1/rest/studyRooms/{id}/+endpoint.ts create mode 100644 apps/api/src/routes/v1/rest/studyRooms/{id}/schema.ts create mode 100644 apps/docs/docs/developers-guide/rest-api/reference/study-rooms.md diff --git a/apps/api/bronya.config.ts b/apps/api/bronya.config.ts index 6059ef5c..860b780f 100644 --- a/apps/api/bronya.config.ts +++ b/apps/api/bronya.config.ts @@ -18,7 +18,7 @@ import { App, Stack, Duration } from "aws-cdk-lib/core"; import { config } from "dotenv"; import type { BuildOptions } from "esbuild"; -import { normalizeCourse } from "./src/lib/utils"; +import { normalizeCourse, normalizeStudyRoom } from "./src/lib/utils"; const prisma = new PrismaClient(); @@ -121,6 +121,10 @@ export const esbuildOptions: BuildOptions = { path: args.path, namespace, })); + build.onResolve({ filter: /virtual:studyRooms/ }, (args) => ({ + path: args.path, + namespace, + })); build.onLoad({ filter: /virtual:courses/, namespace }, async () => ({ contents: `export const courses = ${JSON.stringify( Object.fromEntries( @@ -133,6 +137,13 @@ export const esbuildOptions: BuildOptions = { Object.fromEntries((await prisma.instructor.findMany()).map((x) => [x.ucinetid, x])), )}`, })); + build.onLoad({ filter: /virtual:studyRooms/, namespace }, async () => ({ + contents: `export const studyRooms = ${JSON.stringify( + Object.fromEntries( + (await prisma.studyRoom.findMany()).map(normalizeStudyRoom).map((x) => [x.id, x]), + ), + )}`, + })); }, }, ], diff --git a/apps/api/src/global.d.ts b/apps/api/src/global.d.ts index ff85b418..462254b5 100644 --- a/apps/api/src/global.d.ts +++ b/apps/api/src/global.d.ts @@ -19,3 +19,8 @@ declare module "virtual:instructors" { // eslint-disable-next-line @typescript-eslint/consistent-type-imports declare const instructors: Record; } + +declare module "virtual:studyRooms" { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + declare const studyRooms: Record; +} diff --git a/apps/api/src/lib/utils.ts b/apps/api/src/lib/utils.ts index 4315f83e..ad99526c 100644 --- a/apps/api/src/lib/utils.ts +++ b/apps/api/src/lib/utils.ts @@ -1,4 +1,4 @@ -import type { Course as PrismaCourse } from "@libs/db"; +import type { Course as PrismaCourse, StudyRoom as PrismaStudyRoom } from "@libs/db"; import type { Course, CourseLevel, @@ -6,6 +6,7 @@ import type { GECategory, InstructorPreview, PrerequisiteTree, + StudyRoom, } from "@peterportal-api/types"; const days = ["Su", "M", "Tu", "W", "Th", "F", "Sa"]; @@ -82,3 +83,15 @@ export function normalizeCourse(course: PrismaCourse): Course { terms: course.terms, }; } + +export function normalizeStudyRoom(room: PrismaStudyRoom): StudyRoom { + return { + id: room.id, + name: room.name, + capacity: room.capacity, + location: room.location, + description: room.description, + directions: room.directions, + techEnhanced: room.techEnhanced, + }; +} diff --git a/apps/api/src/routes/v1/graphql/resolvers.ts b/apps/api/src/routes/v1/graphql/resolvers.ts index 52e56058..9d60629a 100644 --- a/apps/api/src/routes/v1/graphql/resolvers.ts +++ b/apps/api/src/routes/v1/graphql/resolvers.ts @@ -23,6 +23,8 @@ export const resolvers: ApolloServerOptions["resolvers"] = { instructors: proxyRestApi("/v1/rest/instructors"), allInstructors: proxyRestApi("/v1/rest/instructors/all"), larc: proxyRestApi("/v1/rest/larc"), + studyRooms: proxyRestApi("/v1/rest/studyrooms"), + allStudyRooms: proxyRestApi("/v1/rest/studyrooms/all"), websoc: proxyRestApi("/v1/rest/websoc", { argsTransform: geTransform }), depts: proxyRestApi("/v1/rest/websoc/depts"), terms: proxyRestApi("/v1/rest/websoc/terms"), diff --git a/apps/api/src/routes/v1/graphql/schema/studyRooms.graphql b/apps/api/src/routes/v1/graphql/schema/studyRooms.graphql new file mode 100644 index 00000000..260a0a77 --- /dev/null +++ b/apps/api/src/routes/v1/graphql/schema/studyRooms.graphql @@ -0,0 +1,46 @@ +type TimeSlot { + "Date of the time slot (YYYY-MM-DD)." + date: String! + "Start time of the time slot (HH:MM)." + start: String! + "End time of the time slot (HH:MM)." + end: String! + "If the time slot is booked." + booked: Boolean! +} + +type StudyRoom { + "ID of study room used by spaces.lib." + id: ID! + "Name of the study room and its room number." + name: String! + "Number of chairs in the study room." + capacity: Int! + "Name of study location." + location: String! + "Description of the study room." + description: String + "Directions to the study room." + directions: String + "Time slots for the study room." + timeSlots: [TimeSlot]! + "If the study room has TV or other tech enhancements." + techEnhanced: Boolean +} + +type StudyLocation { + "ID of the study location using shortened name of the location." + id: ID! + "Location ID of the study location used by space.lib." + lid: String! + "Name of the study location." + name: String! + "Rooms in the study location." + rooms: [StudyRoom!]! +} + +extend type Query { + "Fetch all study rooms." + allStudyRooms(start: String!, end: String!): [StudyLocation!]! + studyRooms(location: String!, start: String!, end: String!): StudyLocation! +} diff --git a/apps/api/src/routes/v1/rest/studyRooms/+endpoint.ts b/apps/api/src/routes/v1/rest/studyRooms/+endpoint.ts new file mode 100644 index 00000000..9c166602 --- /dev/null +++ b/apps/api/src/routes/v1/rest/studyRooms/+endpoint.ts @@ -0,0 +1,30 @@ +import { createHandler } from "@libs/lambda"; +import { studyLocations } from "libs/uc-irvine-lib/src/spaces"; +import { ZodError } from "zod"; + +import { aggreagteStudyRooms } from "./lib"; +import { Query, QuerySchema } from "./schema"; + +export const GET = createHandler(async (event, context, res) => { + const headers = event.headers; + const query = event.queryStringParameters; + const requestId = context.awsRequestId; + let parsedQuery: Query; + try { + parsedQuery = QuerySchema.parse(query); + if (!studyLocations[parsedQuery.location]) { + return res.createErrorResult(404, `Location ${parsedQuery.location} not found`, requestId); + } + return res.createOKResult( + await aggreagteStudyRooms(parsedQuery.location, parsedQuery.start, parsedQuery.end), + headers, + requestId, + ); + } catch (e) { + if (e instanceof ZodError) { + const messages = e.issues.map((issue) => issue.message); + return res.createErrorResult(400, messages.join("; "), requestId); + } + return res.createErrorResult(400, e, requestId); + } +}); diff --git a/apps/api/src/routes/v1/rest/studyRooms/lib.ts b/apps/api/src/routes/v1/rest/studyRooms/lib.ts new file mode 100644 index 00000000..871ff699 --- /dev/null +++ b/apps/api/src/routes/v1/rest/studyRooms/lib.ts @@ -0,0 +1,60 @@ +import { studyLocations } from "libs/uc-irvine-lib/src/spaces"; +import { getStudySpaces } from "libs/uc-irvine-lib/src/spaces"; +import { TimeSlot, StudyLocation } from "packages/types"; +import { studyRooms } from "virtual:studyRooms"; + +/** + * Data structure of time slots returned by libs.spaces. + */ +type Slot = { + start: string; + end: string; + itemId: number; + checkSum: string; + className: string; +}; + +/** + * Map time slots to a more readable format. + */ +export function parseTimeSlots(slots: Slot[]): { [id: string]: TimeSlot[] } { + const timeSlots: { [id: string]: TimeSlot[] } = {}; + slots.forEach((slot) => { + const roomId = slot.itemId.toString(); + const [date, start] = slot.start.split(" "); + const [_, end] = slot.end.split(" "); + const timeSlot: TimeSlot = { + date, + start, + end, + booked: !!slot.className && slot.className === "s-lc-eq-checkout", + }; + if (!timeSlots[roomId]) { + timeSlots[roomId] = [timeSlot]; + } else { + timeSlots[roomId].push(timeSlot); + } + }); + return timeSlots; +} + +/** + * Aggregate study rooms and their time slots into a StudyLocation object. + */ +export async function aggreagteStudyRooms( + locationId: string, + start: string, + end: string, +): Promise { + const spaces = await getStudySpaces(studyLocations[locationId].lid, start, end); + const timeSlotsMap = parseTimeSlots(spaces.slots); + return { + id: locationId, + ...studyLocations[locationId], + rooms: Object.entries(timeSlotsMap) + .filter(([id, _]) => studyRooms[id]) + .map(([id, timeSlots]) => { + return { ...studyRooms[id], timeSlots }; + }), + }; +} diff --git a/apps/api/src/routes/v1/rest/studyRooms/schema.ts b/apps/api/src/routes/v1/rest/studyRooms/schema.ts new file mode 100644 index 00000000..a17a58a8 --- /dev/null +++ b/apps/api/src/routes/v1/rest/studyRooms/schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const QuerySchema = z.object({ + location: z.string({ required_error: 'Parameter "location" not provided' }), + start: z + .string({ required_error: 'Parameter "start" not provided' }) + .regex(/^\d{4}-\d{2}-\d{2}$/, { message: "Start date must be in YYYY-MM-DD format" }), + end: z + .string({ required_error: 'Parameter "end" not provided' }) + .regex(/^\d{4}-\d{2}-\d{2}$/, { message: "End date must be in YYYY-MM-DD format" }), +}); + +export type Query = z.infer; diff --git a/apps/api/src/routes/v1/rest/studyRooms/{id}/+config.ts b/apps/api/src/routes/v1/rest/studyRooms/{id}/+config.ts new file mode 100644 index 00000000..68864478 --- /dev/null +++ b/apps/api/src/routes/v1/rest/studyRooms/{id}/+config.ts @@ -0,0 +1,11 @@ +import type { 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/studyRooms/{id}/+endpoint.ts b/apps/api/src/routes/v1/rest/studyRooms/{id}/+endpoint.ts new file mode 100644 index 00000000..2a69b965 --- /dev/null +++ b/apps/api/src/routes/v1/rest/studyRooms/{id}/+endpoint.ts @@ -0,0 +1,41 @@ +import { createHandler } from "@libs/lambda"; +import { studyLocations } from "libs/uc-irvine-lib/src/spaces"; +import { ZodError } from "zod"; + +import { aggreagteStudyRooms } from "../lib"; + +import { Query, QuerySchema } from "./schema"; + +export const GET = createHandler(async (event, context, res) => { + const headers = event.headers; + const query = event.queryStringParameters; + const requestId = context.awsRequestId; + const { id } = event.pathParameters ?? {}; + let parsedQuery: Query; + try { + switch (id) { + case null: + case undefined: + return res.createErrorResult(400, "Location not provided", requestId); + case "all": + parsedQuery = QuerySchema.parse(query); + return res.createOKResult( + await Promise.all( + Object.keys(studyLocations).map(async (locationId) => { + return aggreagteStudyRooms(locationId, parsedQuery.start, parsedQuery.end); + }), + ), + headers, + requestId, + ); + default: + return res.createErrorResult(400, "Invalid endpoint", requestId); + } + } catch (e) { + if (e instanceof ZodError) { + const messages = e.issues.map((issue) => issue.message); + return res.createErrorResult(400, messages.join("; "), requestId); + } + return res.createErrorResult(400, e, requestId); + } +}); diff --git a/apps/api/src/routes/v1/rest/studyRooms/{id}/schema.ts b/apps/api/src/routes/v1/rest/studyRooms/{id}/schema.ts new file mode 100644 index 00000000..edc6a372 --- /dev/null +++ b/apps/api/src/routes/v1/rest/studyRooms/{id}/schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const QuerySchema = z.object({ + start: z + .string({ required_error: 'Parameter "start" not provided' }) + .regex(/^\d{4}-\d{2}-\d{2}$/, { message: "Start date must be in YYYY-MM-DD format" }), + end: z + .string({ required_error: 'Parameter "end" not provided' }) + .regex(/^\d{4}-\d{2}-\d{2}$/, { message: "End date must be in YYYY-MM-DD format" }), +}); + +export type Query = z.infer; diff --git a/apps/docs/docs/developers-guide/rest-api/reference/study-rooms.md b/apps/docs/docs/developers-guide/rest-api/reference/study-rooms.md new file mode 100644 index 00000000..407be640 --- /dev/null +++ b/apps/docs/docs/developers-guide/rest-api/reference/study-rooms.md @@ -0,0 +1,153 @@ +--- +pagination_prev: null +pagination_next: null +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Study Rooms + +The study rooms endpoint allows users to get information and availability of study rooms that can be reserved at UCI libraries. + +## Query parameters + +#### `location` string Required + +The location of the study rooms to query. Five locations are available to query: + +| location | name | +| -------- | -------------------------- | +| Langson | Langson Library | +| Gateway | Gateway Study Center | +| Science | Science Library | +| MRC | Multimedia Resource Center | +| GML | Grunigen Medical Library | + +#### `start` string Required + +The start date of time slots to query. YYYY-MM-DD format. + +#### `end` string Required + +The end date of time slots to query. YYYY-MM-DD format. + +### Code sample + + + + +```bash +curl "https://api-next.peterportal.org/v1/rest/studyRooms?location=Science&start=2024-04-26&end=2024-04-30"" +``` + + + + +### Response + + + + +```json +{ + "id": "Science", + "name": "Science Library", + "lid": "6580", + "rooms": [ + { + "id": "44667", + "name": "Science 371", + "capacity": 8, + "location": "Science Library", + "description": "This Collaborative Technology Work Space is located on the upper level of the 2nd Floor Grand Reading Room. Access via the stairway halfway through the Grand Reading Room. Digital display available. Bring your own laptop.", + "directions": "Access via the elevators or stairway, on the upper level of the Grand Reading Room.", + "techEnhanced": true, + "timeSlots": [ + { + "date": "2024-04-27", + "start": "13:00:00", + "end": "13:30:00", + "booked": false + } + "..." + ] + } + "..." + ] +} +``` + + + + +```typescript +// https://github.com/icssc/peterportal-api-next/blob/main/packages/types/types/studyRoom +type StudyLocation = { + id: string; + lid: string; + name: string; + rooms: { + id: string; + name: string; + capacity: number; + location: string; + description?: string; + directions?: string; + timeSlots?: { + date: string; + start: string; + end: string; + booked: boolean; + }[]; + techEnhanced?: boolean; + }[]; +}; +``` + + + + +## Get all study rooms + +### Code sample + + + + +```bash +curl "https://api-next.peterportal.org/v1/rest/studyRooms/all?start=2024-04-26&end=2024-04-30" +``` + + + + +### Response + + + + +```json +[ + { + "id": "Langson", + "...": "..." + }, + { + "id": "Gateway", + "...": "..." + }, + "..." +] +``` + + + + +```typescript +// https://github.com/icssc/peterportal-api-next/blob/main/packages/types/types/studyRoom +type StudyLocations = StudyLocation[]; +``` + + + diff --git a/apps/docs/sidebars.js b/apps/docs/sidebars.js index c8f126a6..56f6b20d 100644 --- a/apps/docs/sidebars.js +++ b/apps/docs/sidebars.js @@ -26,6 +26,7 @@ const sidebars = { "developers-guide/rest-api/reference/grades", "developers-guide/rest-api/reference/instructors", "developers-guide/rest-api/reference/larc", + "developers-guide/rest-api/reference/study-rooms", "developers-guide/rest-api/reference/websoc", "developers-guide/rest-api/reference/week", ], diff --git a/libs/uc-irvine-lib/src/spaces/index.ts b/libs/uc-irvine-lib/src/spaces/index.ts index 63ff8389..4015d109 100644 --- a/libs/uc-irvine-lib/src/spaces/index.ts +++ b/libs/uc-irvine-lib/src/spaces/index.ts @@ -7,12 +7,12 @@ const LIB_SPACE_AVAILABILITY_URL = "https://spaces.lib.uci.edu/spaces/availabili * Shortened libary names mapped to their IDs used by spaces.lib.uci.edu * See https://www.lib.uci.edu/ for shortened names **/ -export const studyLocationIds: { [id: string]: { name: string; lid: string } } = { - langson: { name: "Langson Library", lid: "6539" }, - gateway: { name: "Gateway Study Center", lid: "6579" }, - science: { name: "Science Library", lid: "6580" }, - mrc: { name: "Multimedia Resources Center", lid: "6581" }, - gml: { name: "Grunigen Medical Library", lid: "12189" }, +export const studyLocations: { [id: string]: { name: string; lid: string } } = { + Langson: { name: "Langson Library", lid: "6539" }, + Gateway: { name: "Gateway Study Center", lid: "6579" }, + Science: { name: "Science Library", lid: "6580" }, + MRC: { name: "Multimedia Resources Center", lid: "6581" }, + GML: { name: "Grunigen Medical Library", lid: "12189" }, }; /** diff --git a/packages/types/index.ts b/packages/types/index.ts index e100e410..3381aa88 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -6,6 +6,6 @@ export * from "./types/grades"; export * from "./types/instructor"; export * from "./types/larc"; export * from "./types/response"; -export * from "./types/studyroom"; +export * from "./types/studyRoom"; export * from "./types/websoc"; export * from "./types/week"; diff --git a/packages/types/types/studyroom.ts b/packages/types/types/studyroom.ts index 5642fd45..6ca2181e 100644 --- a/packages/types/types/studyroom.ts +++ b/packages/types/types/studyroom.ts @@ -79,3 +79,9 @@ export type StudyLocation = { */ rooms: StudyRoom[]; }; + +/** + * The type of the payload returned on a successful response from querying + * ``/v1/rest/studyRooms/all``. + */ +export type StudyLocations = StudyLocation[]; diff --git a/tools/study-room-scraper/src/index.ts b/tools/study-room-scraper/src/index.ts index 30eedb04..c368871f 100644 --- a/tools/study-room-scraper/src/index.ts +++ b/tools/study-room-scraper/src/index.ts @@ -15,23 +15,15 @@ async function main() { name: location.name, rooms: { create: location.rooms.map((room) => ({ - id: room.id, - name: room.name, - capacity: room.capacity, - location: room.location, - description: room.description, - directions: room.directions, - techEnhanced: room.techEnhanced, + ...room, })), }, }, }); }); await prisma.$transaction([ - prisma.studyRoom.deleteMany({ - where: { studyLocationId: { in: Object.keys(studyLocations) } }, - }), - prisma.studyLocation.deleteMany({ where: { id: { in: Object.keys(studyLocations) } } }), + prisma.studyRoom.deleteMany({}), + prisma.studyLocation.deleteMany({}), ...studyLocationInfo, ]); } diff --git a/tools/study-room-scraper/src/study-room-scraper.ts b/tools/study-room-scraper/src/study-room-scraper.ts index adf2132b..ded5818c 100644 --- a/tools/study-room-scraper/src/study-room-scraper.ts +++ b/tools/study-room-scraper/src/study-room-scraper.ts @@ -1,7 +1,7 @@ import type { StudyRoom, StudyLocation } from "@peterportal-api/types"; import { load } from "cheerio"; import fetch from "cross-fetch"; -import { studyLocationIds, getStudySpaces } from "libs/uc-irvine-lib/src/spaces"; +import { studyLocations, getStudySpaces } from "libs/uc-irvine-lib/src/spaces"; import * as winston from "winston"; const ROOM_SPACE_URL = "https://spaces.lib.uci.edu/space"; @@ -51,40 +51,33 @@ async function getRoomInfo(RoomId: string): Promise { const directionsHeader = $(".s-lc-section-directions"); const directionsText = directionsHeader.find("p").text().trim(); if (directionsText) { - room.directions = directionsText; + room.directions = directionsText.trim(); + if (!room.directions.endsWith(".")) { + room.directions += "."; + } } const descriptionHeader = $(".s-lc-section-description"); let descriptionText = ""; - - if (RoomId === "116383") { + if (room.location === "Grunigen Medical Library") { // Specific processing for the Grunigen Library case - descriptionText = descriptionHeader - .text() - .trim() - .replace(/^Description\s*/i, "") - .replace(/\s*\n+\s*/g, " ") - .replace(/Room Uses:/g, "Room Uses: ") - .replace(/Meetings/g, "Meetings,") - .replace(/Study groups/g, "Study groups,") - .replace( - /Video conferencing for users with a zoom or skype account/g, - "Video conferencing for users with a zoom or skype account.", - ) - .replace( - /Open to UCI faculty, staff and students with current UCIMC badge\./g, - "Open to UCI faculty, staff, and students with current UCIMC badge.", - ) - .replace( - /All meeting attendees must have their UCIMC badge to access the Study Room/g, - "All meeting attendees must have their UCIMC badge to access the Study Room.", - ) - .replace(/Power Available/g, "Power Available.") - .replace(/\s{2,}/g, " ") - .replace(/,\s*\./g, "."); + descriptionHeader.find("p").each(function () { + let paraText = $(this).text().trim(); + if (paraText.includes("\n")) { + paraText = paraText.replaceAll("\n", ", "); + if (!paraText.endsWith(":")) { + paraText += ". "; + } + } + descriptionText += paraText + " "; + }); + descriptionText = descriptionText.replace(/\s{2,}/g, " ").trim(); // Remove extra spaces + descriptionText = descriptionText.replace(/\s+,/g, ","); // Remove spaces before commas + descriptionText = descriptionText.replace(/\.\s*\./g, "."); // Remove extra periods + descriptionText = descriptionText.replace(".,", "."); // Remove commas after periods } else { // General processing for other rooms - const descriptionParts = []; + const descriptionParts: string[] = []; let combinedDescription = ""; descriptionHeader.contents().each((_, content) => { @@ -123,11 +116,14 @@ async function getRoomInfo(RoomId: string): Promise { // description ends with a single period combinedDescription = combinedDescription.replace(/\.\s*$/, "."); - descriptionText = combinedDescription; + descriptionText = combinedDescription.trim(); } if (descriptionText) { room.description = descriptionText; + if (!room.description.endsWith(".")) { + room.description += "."; + } } logger.info(`Scraped Room ${RoomId}`, { room }); @@ -151,12 +147,12 @@ export async function scrapeStudyLocations(): Promise { month: "2-digit", day: "2-digit", }); - const studyLocations: StudyLocations = {}; + const studyLocationsMap: StudyLocations = {}; const rids: Set = new Set(); - for (const lib in studyLocationIds) { + for (const lib in studyLocations) { const studyLocation: StudyLocation = { id: lib, - lid: studyLocationIds[lib].lid, + lid: studyLocations[lib].lid, name: lib, rooms: [], }; @@ -168,7 +164,7 @@ export async function scrapeStudyLocations(): Promise { studyLocation.rooms.push(await getRoomInfo(room.itemId)); rids.add(room.itemId); } - studyLocations[`${studyLocation.id}`] = studyLocation; + studyLocationsMap[`${studyLocation.id}`] = studyLocation; } - return studyLocations; + return studyLocationsMap; }