Skip to content

Commit fb93afb

Browse files
Sanskar MishraSanskar Mishra
Sanskar Mishra
authored and
Sanskar Mishra
committed
added rest and graphql endpoints for study rooms
1 parent 9a46938 commit fb93afb

File tree

11 files changed

+272
-0
lines changed

11 files changed

+272
-0
lines changed

CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Thank you for your interest in contributing to Anteater API!
4545
```
4646

4747
If the checksum does not match, the database dump may be corrupt or have been tampered with. You should delete it immediately and try downloading it again.
48+
n
4849

4950
5. Start the local Postgres database.
5051

apps/api/src/graphql/resolvers/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { coursesResolvers } from "$graphql/resolvers/courses";
33
import { enrollmentHistoryResolvers } from "$graphql/resolvers/enrollment-history";
44
import { gradesResolvers } from "$graphql/resolvers/grades";
55
import { instructorsResolvers } from "$graphql/resolvers/instructors";
6+
import { studyRoomsResolvers } from "$graphql/resolvers/study-rooms";
67
import { websocResolvers } from "$graphql/resolvers/websoc";
78
import { weekResolvers } from "$graphql/resolvers/week";
89
import { mergeResolvers } from "@graphql-tools/merge";
@@ -15,4 +16,5 @@ export const resolvers = mergeResolvers([
1516
instructorsResolvers,
1617
websocResolvers,
1718
weekResolvers,
19+
studyRoomsResolvers,
1820
]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { GraphQLContext } from "$graphql/graphql-context";
2+
import { studyRoomsQuerySchema } from "$schema";
3+
import { StudyRoomsService } from "$services";
4+
import { GraphQLError } from "graphql/error";
5+
6+
export const studyRoomsResolvers = {
7+
Query: {
8+
studyRoom: async (_: unknown, { id }: { id: string }, { db }: GraphQLContext) => {
9+
const service = new StudyRoomsService(db);
10+
const res = await service.getStudyRoomById(id);
11+
if (!res)
12+
throw new GraphQLError(`Study room '${id}' not found`, {
13+
extensions: { code: "NOT_FOUND" },
14+
});
15+
return res;
16+
},
17+
studyRooms: async (_: unknown, args: { query: unknown }, { db }: GraphQLContext) => {
18+
const service = new StudyRoomsService(db);
19+
const validatedQuery = studyRoomsQuerySchema.parse(args.query); // Validate and parse the input
20+
return await service.getStudyRooms(validatedQuery);
21+
},
22+
allStudyRooms: async (_: unknown, __: unknown, { db }: GraphQLContext) => {
23+
const service = new StudyRoomsService(db);
24+
return await service.getAllStudyRooms();
25+
},
26+
},
27+
};

apps/api/src/graphql/schema/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { enrollmentHistorySchema } from "./enrollment-history";
55
import { enums } from "./enums";
66
import { gradesSchema } from "./grades";
77
import { instructorsSchema } from "./instructors";
8+
import { studyRoomsGraphQLSchema } from "./study-rooms";
89
import { websocSchema } from "./websoc";
910
import { weekSchema } from "./week";
1011

@@ -36,4 +37,5 @@ export const typeDefs = mergeTypeDefs([
3637
instructorsSchema,
3738
websocSchema,
3839
weekSchema,
40+
studyRoomsGraphQLSchema,
3941
]);
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const studyRoomsGraphQLSchema = `#graphql
2+
type Slot {
3+
studyRoomId: String!
4+
start: String!
5+
end: String!
6+
isAvailable: Boolean!
7+
}
8+
9+
type StudyRoom {
10+
id: String!
11+
name: String!
12+
capacity: Int!
13+
location: String!
14+
description: String
15+
directions: String
16+
techEnhanced: Boolean!
17+
slots: [Slot!]!
18+
}
19+
20+
input StudyRoomsQuery {
21+
location: String
22+
capacityMin: Int
23+
capacityMax: Int
24+
isTechEnhanced: Boolean
25+
}
26+
27+
extend type Query {
28+
studyRoom(id: String!): StudyRoom!
29+
studyRooms(query: StudyRoomsQuery!): [StudyRoom!]!
30+
allStudyRooms: [StudyRoom!]!
31+
}
32+
`;

apps/api/src/rest/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { enrollmentHistoryRouter } from "./routes/enrollment-history";
77
import { gradesRouter } from "./routes/grades";
88
import { instructorsRouter } from "./routes/instructors";
99
import { pingRouter } from "./routes/ping";
10+
import { studyRoomsRouter } from "./routes/study-rooms";
1011
import { websocRouter } from "./routes/websoc";
1112
import { weekRouter } from "./routes/week";
1213

@@ -20,5 +21,6 @@ restRouter.route("/instructors", instructorsRouter);
2021
restRouter.route("/ping", pingRouter);
2122
restRouter.route("/websoc", websocRouter);
2223
restRouter.route("/week", weekRouter);
24+
restRouter.route("/study-rooms", studyRoomsRouter);
2325

2426
export { restRouter };
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { defaultHook } from "$hooks";
2+
import { productionCache } from "$middleware";
3+
import {
4+
errorSchema,
5+
responseSchema,
6+
studyRoomSchema,
7+
studyRoomsPathSchema,
8+
studyRoomsQuerySchema,
9+
} from "$schema";
10+
import { StudyRoomsService } from "$services";
11+
import type { Bindings } from "$types/bindings";
12+
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
13+
import { database } from "@packages/db";
14+
15+
const studyRoomsRouter = new OpenAPIHono<{ Bindings: Bindings }>({ defaultHook });
16+
17+
const allStudyRoomsRoute = createRoute({
18+
summary: "List all study rooms",
19+
operationId: "allStudyRooms",
20+
tags: ["Study Rooms"],
21+
method: "get",
22+
path: "/all",
23+
description: "Retrieves all available study rooms.",
24+
responses: {
25+
200: {
26+
content: {
27+
"application/json": { schema: responseSchema(studyRoomSchema.array()) },
28+
},
29+
description: "Successful operation",
30+
},
31+
500: {
32+
content: { "application/json": { schema: errorSchema } },
33+
description: "Server error occurred",
34+
},
35+
},
36+
});
37+
38+
const studyRoomByIdRoute = createRoute({
39+
summary: "Retrieve a study room",
40+
operationId: "studyRoomById",
41+
tags: ["Study Rooms"],
42+
method: "get",
43+
path: "/{id}",
44+
request: { params: studyRoomsPathSchema },
45+
description: "Retrieves a study room by its ID.",
46+
responses: {
47+
200: {
48+
content: { "application/json": { schema: responseSchema(studyRoomSchema) } },
49+
description: "Successful operation",
50+
},
51+
404: {
52+
content: { "application/json": { schema: errorSchema } },
53+
description: "Study room not found",
54+
},
55+
422: {
56+
content: { "application/json": { schema: errorSchema } },
57+
description: "Parameters failed validation",
58+
},
59+
500: {
60+
content: { "application/json": { schema: errorSchema } },
61+
description: "Server error occurred",
62+
},
63+
},
64+
});
65+
66+
const studyRoomsByFiltersRoute = createRoute({
67+
summary: "Filter study rooms",
68+
operationId: "studyRoomsByFilters",
69+
tags: ["Study Rooms"],
70+
method: "get",
71+
path: "/",
72+
request: { query: studyRoomsQuerySchema },
73+
description: "Retrieves study rooms matching the given filters.",
74+
responses: {
75+
200: {
76+
content: {
77+
"application/json": { schema: responseSchema(studyRoomSchema.array()) },
78+
},
79+
description: "Successful operation",
80+
},
81+
422: {
82+
content: { "application/json": { schema: errorSchema } },
83+
description: "Parameters failed validation",
84+
},
85+
500: {
86+
content: { "application/json": { schema: errorSchema } },
87+
description: "Server error occurred",
88+
},
89+
},
90+
});
91+
92+
studyRoomsRouter.get(
93+
"*",
94+
productionCache({ cacheName: "study-room-api", cacheControl: "max-age=86400" }),
95+
);
96+
97+
studyRoomsRouter.openapi(allStudyRoomsRoute, async (c) => {
98+
const service = new StudyRoomsService(database(c.env.DB.connectionString));
99+
return c.json(
100+
{
101+
ok: true,
102+
data: studyRoomSchema.array().parse(await service.getAllStudyRooms()),
103+
},
104+
200,
105+
);
106+
});
107+
108+
studyRoomsRouter.openapi(studyRoomByIdRoute, async (c) => {
109+
const { id } = c.req.valid("param");
110+
const service = new StudyRoomsService(database(c.env.DB.connectionString));
111+
const res = await service.getStudyRoomById(id);
112+
return res
113+
? c.json({ ok: true, data: studyRoomSchema.parse(res) }, 200)
114+
: c.json({ ok: false, message: `Study room ${id} not found` }, 404);
115+
});
116+
117+
studyRoomsRouter.openapi(studyRoomsByFiltersRoute, async (c) => {
118+
const query = c.req.valid("query");
119+
const service = new StudyRoomsService(database(c.env.DB.connectionString));
120+
return c.json(
121+
{
122+
ok: true,
123+
data: studyRoomSchema.array().parse(await service.getStudyRooms(query)),
124+
},
125+
200,
126+
);
127+
});
128+
129+
export { studyRoomsRouter };

apps/api/src/schema/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from "./grades";
66
export * from "./instructors.ts";
77
export * from "./websoc";
88
export * from "./week";
9+
export * from "./study-rooms";

apps/api/src/schema/study-rooms.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { z } from "@hono/zod-openapi";
2+
3+
export const slotSchema = z.object({
4+
studyRoomId: z.string(),
5+
start: z.string().openapi({ format: "date-time" }),
6+
end: z.string().openapi({ format: "date-time" }),
7+
isAvailable: z.boolean(),
8+
});
9+
10+
export const studyRoomSchema = z.object({
11+
id: z.string(),
12+
name: z.string(),
13+
capacity: z.number().int(),
14+
location: z.string(),
15+
description: z.string().optional(),
16+
directions: z.string().optional(),
17+
techEnhanced: z.boolean(),
18+
slots: z.array(slotSchema).optional(),
19+
});
20+
21+
export const studyRoomsPathSchema = z.object({
22+
id: z.string(),
23+
});
24+
25+
export const studyRoomsQuerySchema = z
26+
.object({
27+
location: z.string().optional(),
28+
capacityMin: z.coerce.number().int().optional(), // Coerce to number
29+
capacityMax: z.coerce.number().int().optional(), // Coerce to number
30+
isTechEnhanced: z.coerce.boolean().optional(), // Coerce to boolean
31+
})
32+
.refine((data) => Object.keys(data).length > 0, {
33+
message: "At least one filter must be provided.",
34+
});

apps/api/src/services/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from "./grades";
55
export * from "./instructors";
66
export * from "./websoc";
77
export * from "./week";
8+
export * from "./study-rooms";

apps/api/src/services/study-rooms.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { database } from "@packages/db";
2+
import { and, eq, gte, lte } from "@packages/db/drizzle";
3+
import { studyRoom } from "@packages/db/schema";
4+
5+
type StudyRoomsServiceInput = {
6+
location?: string;
7+
capacityMin?: number;
8+
capacityMax?: number;
9+
isTechEnhanced?: boolean;
10+
};
11+
12+
export class StudyRoomsService {
13+
constructor(private readonly db: ReturnType<typeof database>) {}
14+
15+
async getAllStudyRooms() {
16+
return this.db
17+
.select()
18+
.from(studyRoom)
19+
.then((rows) => rows);
20+
}
21+
22+
async getStudyRoomById(id: string) {
23+
const [room] = await this.db.select().from(studyRoom).where(eq(studyRoom.id, id));
24+
return room || null;
25+
}
26+
27+
async getStudyRooms(input: StudyRoomsServiceInput) {
28+
const conditions = [];
29+
if (input.location) conditions.push(eq(studyRoom.location, input.location));
30+
if (input.capacityMin) conditions.push(gte(studyRoom.capacity, input.capacityMin));
31+
if (input.capacityMax) conditions.push(lte(studyRoom.capacity, input.capacityMax));
32+
if (input.isTechEnhanced !== undefined)
33+
conditions.push(eq(studyRoom.techEnhanced, input.isTechEnhanced));
34+
35+
return this.db
36+
.select()
37+
.from(studyRoom)
38+
.where(and(...conditions))
39+
.then((rows) => rows);
40+
}
41+
}

0 commit comments

Comments
 (0)