Skip to content

Commit

Permalink
Merge pull request #767 from sandboxnu/minor-endpoints
Browse files Browse the repository at this point in the history
minor endpoints folder, types, and missing data
  • Loading branch information
Suraj-Ram authored Dec 8, 2024
2 parents f20fcb2 + 902b80a commit 5884459
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MajorModule } from "./major/major.module";
import { EmailModule } from "./email/email.module";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { MetaModule } from "./meta/meta.module";
import { MinorModule } from "./minor/minor.module";

@Module({
imports: [
Expand All @@ -29,6 +30,7 @@ import { MetaModule } from "./meta/meta.module";
AuthModule,
PlanModule,
MajorModule,
MinorModule,
EmailModule,
MetaModule,
],
Expand Down
125 changes: 125 additions & 0 deletions packages/api/src/minor/minor-collator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Minor } from "@graduate/common";
// year minor name minor data
const MINORS: Record<string, Record<string, Minor>> = {};
const MINOR_YEARS = new Set<string>();

const rootDir = "./src/minor/minors";

interface YearData {
year: string;
}

interface YearCollegeData {
year: string;
college: string;
}

interface YearCollegeMinorData {
year: string;
college: string;
minor: string;
}

async function fileExists(
fs: typeof import("fs/promises"),
path: string
): Promise<boolean> {
return await fs.access(path, fs.constants.F_OK).then(
() => true,
() => false
);
}

// TODO: this code is quick and dirty but works. this should be replaced with some dry-er code later.
/**
* Iterates over the ./minors directory, collecting minors and adding them to
* the exported MINORS and MINOR_YEARS object/set respectively. It prioritizes
* parsed.commit.json files over parsed.initial.json files because _.commit._
* files have been human-reviewed and _.initial._ files are raw scraper output.
*/
async function collateMinors() {
// TODO: determine why these needed to be runtime imports (normal import statements didn't work here).
const fs = await import("fs/promises");
const path = await import("path");
const years = (
await fs.readdir(path.resolve(rootDir), {
withFileTypes: true,
})
)
.filter((dirent) => dirent.isDirectory())
.map(
(dirent): YearData => ({
year: dirent.name,
})
);

const colleges = (
await Promise.all(
years.map(async ({ year }) => {
const colleges = await fs.readdir(path.join(rootDir, year), {
withFileTypes: true,
});
return colleges
.filter((dirent) => dirent.isDirectory())
.map(
(college): YearCollegeData => ({
year: year,
college: college.name,
})
);
})
)
).flat();

const minors = (
await Promise.all(
colleges.map(async ({ year, college }) => {
const minors = await fs.readdir(path.join(rootDir, year, college), {
withFileTypes: true,
});
return minors
.filter((dirent) => dirent.isDirectory())
.map(
(minor): YearCollegeMinorData => ({
year: year,
college: college,
minor: minor.name,
})
);
})
)
).flat();

years.forEach(({ year }) => {
MINOR_YEARS.add(year);
MINORS[year] = {};
});

const done = await Promise.all(
minors.map(async ({ year, college, minor }) => {
const basePath = path.join(rootDir, year, college, minor);
const commitFile = path.join(basePath, "parsed.commit.json");
const initialFile = path.join(basePath, "parsed.initial.json");

if (await fileExists(fs, commitFile)) {
const fileData = JSON.parse(
(await fs.readFile(commitFile)).toString()
) as Minor;
MINORS[year][fileData.name] = fileData;
} else if (await fileExists(fs, initialFile)) {
const fileData = JSON.parse(
(await fs.readFile(initialFile)).toString()
) as Minor;
if (MINORS[year]) MINORS[year][fileData.name] = fileData;
}
})
);

console.log(
`Successfully loaded ${done.length} minors across ${MINOR_YEARS.size} years!`
);
}

collateMinors();

export { MINORS, MINOR_YEARS };
32 changes: 32 additions & 0 deletions packages/api/src/minor/minor.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GetSupportedMinorsResponse, Minor } from "@graduate/common";
import { MinorService } from "./minor.service";
import {
Controller,
Get,
NotFoundException,
Param,
ParseIntPipe,
} from "@nestjs/common";

@Controller("minors")
export class MinorController {
constructor(private readonly minorService: MinorService) {}
@Get("/:catalogYear/:minorName")
getMinor(
@Param("catalogYear", ParseIntPipe) catalogYear: number,
@Param("minorName") minorName: string
): Minor {
const minor = this.minorService.findByMinorAndYear(minorName, catalogYear);
if (!minor) {
throw new NotFoundException();
}

return minor;
}

@Get("supportedMinors")
getSupportedMinors(): GetSupportedMinorsResponse {
const supportedMinors = this.minorService.getSupportedMinors();
return { supportedMinors };
}
}
10 changes: 10 additions & 0 deletions packages/api/src/minor/minor.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { MinorService } from "./minor.service";
import { MinorController } from "./minor.controller";

@Module({
controllers: [MinorController],
providers: [MinorService],
exports: [MinorService],
})
export class MinorModule {}
35 changes: 35 additions & 0 deletions packages/api/src/minor/minor.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Injectable, Logger } from "@nestjs/common";
import { formatServiceCtx } from "src/utils";
import { MINOR_YEARS, MINORS } from "./minor-collator";
import { Minor, SupportedMinors } from "@graduate/common";

@Injectable()
export class MinorService {
private readonly logger: Logger = new Logger();
findByMinorAndYear(minorName: string, catalogYear: number): Minor | null {
if (!MINOR_YEARS.has(String(catalogYear))) {
this.logger.debug(
{ message: "Minor year not found", catalogYear },
MinorService.formatMinorServiceCtx("findByMinorAndYear")
);
return null;
}

if (!MINORS[catalogYear][minorName]) {
this.logger.debug(
{ message: "Minor within year not found", minorName, catalogYear },
MinorService.formatMinorServiceCtx("findByMinorAndYear")
);
return null;
}
return MINORS[catalogYear][minorName];
}

getSupportedMinors(): SupportedMinors {
return MINORS;
}

private static formatMinorServiceCtx(methodName: string): string {
return formatServiceCtx(MinorService.name, methodName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "Mathematics",
"totalCreditsRequired": 24,
"yearVersion": 2022,
"requirementSections": [
{
"type": "SECTION",
"title": "Required Courses",
"requirements": [
{
"type": "COURSE",
"classId": 1341,
"subject": "MATH"
},
{
"type": "COURSE",
"classId": 1341,
"subject": "MATH"
}
]
},
{
"type": "SECTION",
"title": "Intermediate-Level Courses",
"requirements": [
{
"type": "COURSE",
"classId": 2321,
"subject": "MATH"
},
{
"type": "COURSE",
"classId": 2341,
"subject": "MATH"
},
{
"type": "COURSE",
"classId": 2331,
"subject": "MATH"
}
],
"minRequirementCount": 2
},
{
"type": "RANGE",
"subject": "MATH",
"idRangeStart": 3001,
"idRangeEnd": 4699,
"exceptions": [
{
"subject": "MATH",
"classId": 4000,
"type": "COURSE"
}
]
}
]
}
4 changes: 4 additions & 0 deletions packages/common/src/api-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ScheduleCourse2,
MetaInfo,
Maybe,
SupportedMinors,
} from "./types";

/** Types our API responds with. */
Expand Down Expand Up @@ -54,6 +55,9 @@ export class GetSupportedMajorsResponse {
// { year => { majorName => {concentrations, minRequiredConcentrations} }}
supportedMajors: SupportedMajors;
}
export class GetSupportedMinorsResponse {
supportedMinors: SupportedMinors;
}

export class GetMetaInfoResponse implements MetaInfo {
commit: Maybe<string>;
Expand Down
30 changes: 30 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,34 @@ export interface Major2 {
metadata?: MajorMetadata;
}

/**
* A Minor, containing all the requirements.
*
* @param name The name of the minor.
* @param requirementSections A list of the sections of requirements.
* @param totalCreditsRequired Total credits required to graduate with this minor.
* @param yearVersion The catalog version year of this minor.
* @param metadata Metadata for the minor.
*/

export interface Minor {
name: string;
requirementSections: Section[];
totalCreditsRequired: number;
yearVersion: number;
metadata?: MinorMetaData;
}
/**
* Metadata for a minor.
*
* @param verified Whether the major has been manually verified.
* @param lastEdited The last time the major was edited MM/DD/YYYY.
*/
export interface MinorMetaData {
verified: boolean;
lastEdited: string;
}

/**
* Metadata for a major.
*
Expand Down Expand Up @@ -473,9 +501,11 @@ export type SupportedConcentrations = {

// { majorName => { concentration, minRequiredConcentrations, verified} }
export type SupportedMajorsForYear = Record<string, SupportedConcentrations>;
export type SupportedMinorsForYear = Record<string, Minor>;

// { year => supported majors }
export type SupportedMajors = Record<string, SupportedMajorsForYear>;
export type SupportedMinors = Record<string, SupportedMinorsForYear>;

/**
* Types for a some result from an algorithim. Currently used for the result of
Expand Down

0 comments on commit 5884459

Please sign in to comment.