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,7 +1,10 @@
import type { GraphQLContext } from "$graphql/graphql-context";
import {
majorQuerySchema,
waterkimchi marked this conversation as resolved.
Show resolved Hide resolved
majorRequirementsQuerySchema,
minorQuerySchema,
minorRequirementsQuerySchema,
specializationQuerySchema,
specializationRequirementsQuerySchema,
} from "$schema";
import { ProgramsService } from "$services";
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 = majorQuerySchema.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 = minorQuerySchema.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 = specializationQuerySchema.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
10 changes: 7 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,14 @@ input ProgramRequirementsQuery {
programId: String!
}

input ProgramQuery {
id: String!
waterkimchi marked this conversation as resolved.
Show resolved Hide resolved
}

extend type Query {
majors: [MajorPreview!]!
minors: [MinorPreview!]!
specializations: [SpecializationPreview!]!
majors(query: ProgramQuery): [MajorPreview!]!
minors(query: ProgramQuery): [MinorPreview!]!
specializations(query: ProgramQuery): [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 @@ -2,14 +2,17 @@ import { defaultHook } from "$hooks";
import { productionCache } from "$middleware";
import {
errorSchema,
majorQuerySchema,
majorRequirementsQuerySchema,
majorRequirementsResponseSchema,
majorsResponseSchema,
minorQuerySchema,
minorRequirementsQuerySchema,
minorRequirementsResponseSchema,
minorsResponseSchema,
programRequirementSchema,
responseSchema,
specializationQuerySchema,
specializationRequirementsQuerySchema,
specializationRequirementsResponseSchema,
specializationsResponseSchema,
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: majorQuerySchema },
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: minorQuerySchema },
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: specializationQuerySchema },
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 majorQuerySchema = z.object({
id: z.string().optional().openapi({
description: "The ID of a single major to request, if provided",
example: "BA-163",
}),
});

export const minorQuerySchema = z.object({
id: z.string().optional().openapi({
description: "The ID of the specific minor being requested for",
waterkimchi marked this conversation as resolved.
Show resolved Hide resolved
example: "49A",
}),
});

export const specializationQuerySchema = 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,6 +1,9 @@
import type {
majorQuerySchema,
majorRequirementsQuerySchema,
minorQuerySchema,
minorRequirementsQuerySchema,
specializationQuerySchema,
specializationRequirementsQuerySchema,
} from "$schema";
import type { database } from "@packages/db";
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 majorQuerySchema>) {
waterkimchi marked this conversation as resolved.
Show resolved Hide resolved
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 minorQuerySchema>) {
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 specializationQuerySchema>) {
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
Loading