Skip to content

Commit

Permalink
feat(study-rooms): serve slots data (#75)
Browse files Browse the repository at this point in the history
## Description

Adds study room slots and availability to study room endpoint.

As previously implemented in the data pipeline, availability is
available in on the days between the day of the current time in UCI's
timezone (`America/Los_Angeles`) and exactly 3 days later, inclusive.
Data is updated every 2 minutes.

This PR simply exposes that data. We use ISO 8601 dates ~~(most
precisely, `z.string().datetime({ local: true })`) which are given with
the `Z` indicating UTC but should be interpreted as being in UCI's
timezone~~. We ask Postgres to interpret the `TIMESTAMP NO TIMEZONE` in
`America/Los_Angeles` and format as zoned ISO 8601, avoiding adding a JS
timezone library.

**Note**: The `slots` field on returned study rooms is now guaranteed to
exist. This is not a breaking change.

We use a materialized view to precompute the JSON structure of the
response. The refreshing of this view is relatively light and is done as
part of the study room and slot scraping process every 2 minutes.
However, it is not completely trivial; see below demonstrating that it
produces a significant speed improvement per request which will accrue
over time. **This design decision is a database schema change.** Since
we have this JSON structure beforehand, we need only `SELECT` with
conditions when serving a response.

## Related Issue

Feature update as requested in #46. ~~Also closes the inconsistency
raised in #74 since study room data was one of the offenders and is
within the jurisdiction of this PR.~~ It doesn't.

## Motivation and Context

>be me
>want study room slots
>no study room slots

mfw

## How Has This Been Tested?

Tested with Postman on a local deployment.

## Screenshots (if appropriate):

### Old screenshots (before timezone correction):

REST is functional:

![Screenshot_20250111_192159](https://github.com/user-attachments/assets/64d72053-79a4-4c39-93b6-5903e0a80e49)

GraphQL is functional:

![Screenshot_20250111_192242](https://github.com/user-attachments/assets/576fa5d9-f2cf-4dc1-ab3d-afd638568ca7)

### New screenshots (latest):


![Screenshot_20250114_145739](https://github.com/user-attachments/assets/30801e4f-d5c4-46cd-b543-e96d57dce5a9)


![Screenshot_20250114_150118](https://github.com/user-attachments/assets/9a4dde1b-c394-49e8-b481-9251e5a8dfa7)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)

## Checklist:

- [x] My code involves a change to the database schema.
- [ ] My code requires a change to the documentation.

---------

Co-authored-by: Andrew Wang <[email protected]>
  • Loading branch information
laggycomputer and andrew-wang0 authored Jan 15, 2025
1 parent 8f198a3 commit e1eccc4
Show file tree
Hide file tree
Showing 8 changed files with 3,363 additions and 7 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/graphql/schema/study-rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const studyRoomsGraphQLSchema = `#graphql
description: String
directions: String
techEnhanced: Boolean!
slots: [Slot]
slots: [Slot]!
}
input StudyRoomsQuery {
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/schema/study-rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { z } from "@hono/zod-openapi";

export const slotSchema = z.object({
studyRoomId: z.string(),
start: z.string().openapi({ format: "date-time" }),
end: z.string().openapi({ format: "date-time" }),
start: z.string().datetime({ offset: true }),
end: z.string().datetime({ offset: true }),
isAvailable: z.boolean(),
});

Expand All @@ -15,7 +15,7 @@ export const studyRoomSchema = z.object({
description: z.string().optional(),
directions: z.string().optional(),
techEnhanced: z.boolean(),
slots: z.array(slotSchema).optional(),
slots: z.array(slotSchema),
});

export const studyRoomsPathSchema = z.object({
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/services/study-rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { studyRoomsQuerySchema } from "$schema";
import type { z } from "@hono/zod-openapi";
import type { database } from "@packages/db";
import { and, eq, gte, lte } from "@packages/db/drizzle";
import { studyRoom } from "@packages/db/schema";
import { studyRoom, studyRoomView } from "@packages/db/schema";

type StudyRoomsServiceInput = z.infer<typeof studyRoomsQuerySchema>;

Expand All @@ -24,7 +24,7 @@ export class StudyRoomsService {

return this.db
.select()
.from(studyRoom)
.from(studyRoomView)
.where(conditions.length ? and(...conditions) : undefined);
}
}
3 changes: 2 additions & 1 deletion apps/data-pipeline/study-location-scraper/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { database } from "@packages/db";
import { lt } from "@packages/db/drizzle";
import { studyLocation, studyRoom, studyRoomSlot } from "@packages/db/schema";
import { studyLocation, studyRoom, studyRoomSlot, studyRoomView } from "@packages/db/schema";
import { conflictUpdateSetAllCols } from "@packages/db/utils";
import type { Cheerio, CheerioAPI } from "cheerio";
import { load } from "cheerio";
Expand Down Expand Up @@ -267,5 +267,6 @@ export async function doScrape(db: ReturnType<typeof database>) {
set: conflictUpdateSetAllCols(studyRoomSlot),
});
await tx.delete(studyRoomSlot).where(lt(studyRoomSlot.end, new Date()));
await tx.refreshMaterializedView(studyRoomView);
});
}
6 changes: 6 additions & 0 deletions packages/db/migrations/0008_study_rooms_materialized_view.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE MATERIALIZED VIEW "public"."study_room_view" AS (select "study_room"."id", "study_room"."name", "study_room"."capacity", "study_room"."location", "study_room"."description", "study_room"."directions", "study_room"."tech_enhanced", ARRAY_AGG(JSONB_BUILD_OBJECT(
'studyRoomId', "study_room_slot"."study_room_id",
'start', to_json("study_room_slot"."start" AT TIME ZONE 'America/Los_Angeles'),
'end', to_json("study_room_slot"."end" AT TIME ZONE 'America/Los_Angeles'),
'isAvailable', "study_room_slot"."is_available"
)) as "slots" from "study_room" left join "study_room_slot" on "study_room"."id" = "study_room_slot"."study_room_id" group by "study_room"."id");
Loading

0 comments on commit e1eccc4

Please sign in to comment.