Skip to content

Commit

Permalink
feat: add way to filter programs endpoints by majorid and id (#97)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->
Previously, the endpoints for programs returned a comprehensive list of
all `majors`, `minors`, and `specializations` without any filtering
options. With this update, we have added the ability to query specific
data by including optional parameters like `majorId` or `minorId`. This
enhancement allows users to request only the relevant `specializations`
associated with a particular `major` or `minor`, streamlining the data
retrieval process and improving the overall user experience.

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an
issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps
to reproduce -->
<!--- Please link to the issue here: -->
`/v2/rest/programs/specializations` should have some way to filter by
major id - #96

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
Currently, the endpoint returns a list of all available specializations,
which can be overwhelming and inefficient, especially for users seeking
information about a specific `major`. By implementing this fix, users
will be able to first retrieve the `major ID` through
`/programs/majors`, and then query
`/programs/specializations?majorID=BS-201` to get only the relevant
specializations for a particular major, such as `Computer Science`.

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
Tested using local environment.

## Screenshots (if appropriate):
REST:

Majors:
<img width="1064" alt="Screenshot 2025-01-27 at 21 25 04"
src="https://github.com/user-attachments/assets/4c0fa642-f0a3-42be-9922-3a9aecab04f4"
/>

Minors:
<img width="1070" alt="Screenshot 2025-01-27 at 21 25 17"
src="https://github.com/user-attachments/assets/9fbe6128-00a0-4608-ae8a-d78d436a9cd4"
/>

Specializations:
<img width="1242" alt="Screenshot 2025-01-27 at 21 25 34"
src="https://github.com/user-attachments/assets/8c6282fe-9bd7-4979-9dac-a25e606a079a"
/>

GraphQL:

Majors:
<img width="902" alt="Screenshot 2025-01-27 at 23 14 26"
src="https://github.com/user-attachments/assets/1f427cea-28be-4e91-809c-e6e74373643b"
/>


Minors:
<img width="977" alt="Screenshot 2025-01-27 at 22 07 38"
src="https://github.com/user-attachments/assets/d1a92e8b-5b2c-4beb-9772-feec67284c81"
/>

Specializations:
<img width="1202" alt="Screenshot 2025-01-27 at 22 09 06"
src="https://github.com/user-attachments/assets/58ab8cb2-444f-4ba9-bcbc-62709e851e9e"
/>

## 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:

<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->

- [ ] 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
waterkimchi and andrew-wang0 authored Jan 28, 2025
1 parent d9f9e26 commit 76144b7
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 17 deletions.
18 changes: 12 additions & 6 deletions apps/api/src/graphql/resolvers/programs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { GraphQLContext } from "$graphql/graphql-context";
import {
majorRequirementsQuerySchema,
majorsQuerySchema,
minorRequirementsQuerySchema,
minorsQuerySchema,
specializationRequirementsQuerySchema,
specializationsQuerySchema,
} from "$schema";
import { ProgramsService } from "$services";
import { GraphQLError } from "graphql/error";
Expand Down Expand Up @@ -39,27 +42,30 @@ export const programResolvers = {
});
return res;
},
majors: async (_: unknown, args: unknown, { db }: GraphQLContext) => {
majors: async (_: unknown, args: { query?: unknown }, { db }: GraphQLContext) => {
const parsedArgs = majorsQuerySchema.parse(args?.query);
const service = new ProgramsService(db);
const res = await service.getMajors();
const res = await service.getMajors(parsedArgs);
if (!res)
throw new GraphQLError("Major data not found", {
extensions: { code: "NOT_FOUND" },
});
return res;
},
minors: async (_: unknown, args: unknown, { db }: GraphQLContext) => {
minors: async (_: unknown, args: { query?: unknown }, { db }: GraphQLContext) => {
const parsedArgs = minorsQuerySchema.parse(args?.query);
const service = new ProgramsService(db);
const res = await service.getMinors();
const res = await service.getMinors(parsedArgs);
if (!res)
throw new GraphQLError("Minor data not found", {
extensions: { code: "NOT_FOUND" },
});
return res;
},
specializations: async (_: unknown, args: unknown, { db }: GraphQLContext) => {
specializations: async (_: unknown, args: { query?: unknown }, { db }: GraphQLContext) => {
const parsedArgs = specializationsQuerySchema.parse(args?.query);
const service = new ProgramsService(db);
const res = await service.getSpecializations();
const res = await service.getSpecializations(parsedArgs);
if (!res)
throw new GraphQLError("Specializations data not found", {
extensions: { code: "NOT_FOUND" },
Expand Down
18 changes: 15 additions & 3 deletions apps/api/src/graphql/schema/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,22 @@ input ProgramRequirementsQuery {
programId: String!
}
input MajorsQuery {
id: String!
}
input MinorsQuery {
id: String!
}
input SpecializationsQuery {
majorId: String!
}
extend type Query {
majors: [MajorPreview!]!
minors: [MinorPreview!]!
specializations: [SpecializationPreview!]!
majors(query: MajorsQuery): [MajorPreview!]!
minors(query: MinorsQuery): [MinorPreview!]!
specializations(query: SpecializationsQuery): [SpecializationPreview!]!
major(query: ProgramRequirementsQuery!): Program!
minor(query: ProgramRequirementsQuery!): Program!
specialization(query: ProgramRequirementsQuery!): Program!
Expand Down
15 changes: 12 additions & 3 deletions apps/api/src/rest/routes/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import {
errorSchema,
majorRequirementsQuerySchema,
majorRequirementsResponseSchema,
majorsQuerySchema,
majorsResponseSchema,
minorRequirementsQuerySchema,
minorRequirementsResponseSchema,
minorsQuerySchema,
minorsResponseSchema,
programRequirementSchema,
responseSchema,
specializationRequirementsQuerySchema,
specializationRequirementsResponseSchema,
specializationsQuerySchema,
specializationsResponseSchema,
} from "$schema";
import { ProgramsService } from "$services";
Expand All @@ -29,6 +32,7 @@ const majorsRoute = createRoute({
method: "get",
path: "/majors",
description: "List all available majors in UCI's current catalogue.",
request: { query: majorsQuerySchema },
responses: {
200: {
content: {
Expand All @@ -50,6 +54,7 @@ const minorsRoute = createRoute({
method: "get",
path: "/minors",
description: "List all available majors in UCI's current catalogue.",
request: { query: minorsQuerySchema },
responses: {
200: {
content: {
Expand All @@ -71,6 +76,7 @@ const specializationsRoute = createRoute({
method: "get",
path: "/specializations",
description: "List all available majors in UCI's current catalogue.",
request: { query: specializationsQuerySchema },
responses: {
200: {
content: {
Expand Down Expand Up @@ -183,20 +189,23 @@ programsRouter.get(
);

programsRouter.openapi(majorsRoute, async (c) => {
const query = c.req.valid("query");
const service = new ProgramsService(database(c.env.DB.connectionString));
const res = await service.getMajors();
const res = await service.getMajors(query);
return c.json({ ok: true, data: majorsResponseSchema.parse(res) }, 200);
});

programsRouter.openapi(minorsRoute, async (c) => {
const query = c.req.valid("query");
const service = new ProgramsService(database(c.env.DB.connectionString));
const res = await service.getMinors();
const res = await service.getMinors(query);
return c.json({ ok: true, data: minorsResponseSchema.parse(res) }, 200);
});

programsRouter.openapi(specializationsRoute, async (c) => {
const query = c.req.valid("query");
const service = new ProgramsService(database(c.env.DB.connectionString));
const res = await service.getSpecializations();
const res = await service.getSpecializations(query);
return c.json({ ok: true, data: specializationsResponseSchema.parse(res) }, 200);
});

Expand Down
21 changes: 21 additions & 0 deletions apps/api/src/schema/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ import { z } from "@hono/zod-openapi";

const programIdBase = z.string({ required_error: "programId is required" });

export const majorsQuerySchema = z.object({
id: z.string().optional().openapi({
description: "The ID of a single major to request, if provided",
example: "BA-163",
}),
});

export const minorsQuerySchema = z.object({
id: z.string().optional().openapi({
description: "The ID of a single minor to request, if provided",
example: "49A",
}),
});

export const specializationsQuerySchema = z.object({
majorId: z.string().optional().openapi({
description: "Only fetch specializations associated with the major with this ID, if provided",
example: "BS-201",
}),
});

export const majorRequirementsQuerySchema = z.object({
programId: programIdBase.openapi({
description: "A major ID to query requirements for",
Expand Down
16 changes: 11 additions & 5 deletions apps/api/src/services/programs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type {
majorRequirementsQuerySchema,
majorsQuerySchema,
minorRequirementsQuerySchema,
minorsQuerySchema,
specializationRequirementsQuerySchema,
specializationsQuerySchema,
} from "$schema";
import type { database } from "@packages/db";
import { eq, sql } from "@packages/db/drizzle";
Expand All @@ -11,7 +14,7 @@ import type { z } from "zod";
export class ProgramsService {
constructor(private readonly db: ReturnType<typeof database>) {}

async getMajors() {
async getMajors(query: z.infer<typeof majorsQuerySchema>) {
const majorSpecialization = this.db.$with("major_specialization").as(
this.db
.select({
Expand All @@ -34,27 +37,30 @@ export class ProgramsService {
division: degree.division,
})
.from(majorSpecialization)
.where(query.id ? eq(majorSpecialization.id, query.id) : undefined)
.innerJoin(major, eq(majorSpecialization.id, major.id))
.innerJoin(degree, eq(major.degreeId, degree.id));
}

async getMinors() {
async getMinors(query: z.infer<typeof minorsQuerySchema>) {
return this.db
.select({
id: minor.id,
name: minor.name,
})
.from(minor);
.from(minor)
.where(query.id ? eq(minor.id, query.id) : undefined);
}

async getSpecializations() {
async getSpecializations(query: z.infer<typeof specializationsQuerySchema>) {
return this.db
.select({
id: specialization.id,
majorId: specialization.majorId,
name: specialization.name,
})
.from(specialization);
.from(specialization)
.where(query.majorId ? eq(specialization.majorId, query.majorId) : undefined);
}

async getMajorRequirements(query: z.infer<typeof majorRequirementsQuerySchema>) {
Expand Down

0 comments on commit 76144b7

Please sign in to comment.