Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add way to filter programs endpoints by majorid and id #97

Merged
merged 9 commits into from
Jan 28, 2025
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!
waterkimchi marked this conversation as resolved.
Show resolved Hide resolved
}

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) : sql`true`)
andrew-wang0 marked this conversation as resolved.
Show resolved Hide resolved
.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) : sql`true`);
andrew-wang0 marked this conversation as resolved.
Show resolved Hide resolved
}

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) : sql`true`);
andrew-wang0 marked this conversation as resolved.
Show resolved Hide resolved
}

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