diff --git a/apps/api/src/graphql/resolvers/programs.ts b/apps/api/src/graphql/resolvers/programs.ts index 759581e..476cad2 100644 --- a/apps/api/src/graphql/resolvers/programs.ts +++ b/apps/api/src/graphql/resolvers/programs.ts @@ -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"; @@ -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" }, diff --git a/apps/api/src/graphql/schema/programs.ts b/apps/api/src/graphql/schema/programs.ts index 690faef..319f953 100644 --- a/apps/api/src/graphql/schema/programs.ts +++ b/apps/api/src/graphql/schema/programs.ts @@ -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! diff --git a/apps/api/src/rest/routes/programs.ts b/apps/api/src/rest/routes/programs.ts index 6ab4fdd..d899501 100644 --- a/apps/api/src/rest/routes/programs.ts +++ b/apps/api/src/rest/routes/programs.ts @@ -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"; @@ -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: { @@ -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: { @@ -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: { @@ -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); }); diff --git a/apps/api/src/schema/programs.ts b/apps/api/src/schema/programs.ts index 27fe2cc..659d7d2 100644 --- a/apps/api/src/schema/programs.ts +++ b/apps/api/src/schema/programs.ts @@ -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", diff --git a/apps/api/src/services/programs.ts b/apps/api/src/services/programs.ts index 13dd45a..6323e24 100644 --- a/apps/api/src/services/programs.ts +++ b/apps/api/src/services/programs.ts @@ -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"; @@ -11,7 +14,7 @@ import type { z } from "zod"; export class ProgramsService { constructor(private readonly db: ReturnType) {} - async getMajors() { + async getMajors(query: z.infer) { const majorSpecialization = this.db.$with("major_specialization").as( this.db .select({ @@ -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) { 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) { 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) {