Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/CourseInfo.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface CourseInfo {
id: string;
name: string;
units: string;
prerequisites: string;
corequisites: string;
}
51 changes: 51 additions & 0 deletions api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
getProfNames,
initializeMySQL,
checkSQLConnection,
searchCourses,
getCourse,
} from './sql';

const app = express();
Expand Down Expand Up @@ -182,6 +184,55 @@ app.get('/vote', (req, res) => {
return res.status(400).send(result);
});

// Unified endpoint for searching courses or listing all
app.get('/courses', async (req, res) => {
let key: string | undefined;

// Check if req.query.key is a string and assign it to key
// If not, key remains undefined
if (typeof req.query.key === 'string') {
key = req.query.key;
} else if (!req.query.key) {
key = undefined;
} else {
// Handle the case where key is not a string.
// Returning an error response
return res
.status(400)
.send('Invalid query parameter: key must be a string');
}
try {
// If the key is empty, consider it as a request for all courses
if (!key) {
// Assuming searchCourses function can handle empty keys to return all courses
const results = await searchCourses();
return res.json(results);
}
// If key is provided, search for courses based on the key
const results = await searchCourses(key);

res.json(results);
} catch (error) {
console.error(error);
res.status(500).send('An error occurred while fetching the courses');
}
});

// Endpoint for searching a course by an exact course number
app.get('/courses/:courseNumber', async (req, res) => {
const { courseNumber } = req.params;
try {
const result = await getCourse(courseNumber);
if (!result) {
return res.status(404).send('Course not found');
}
res.json(result);
} catch (error) {
console.error(error);
res.status(500).send('An error occurred while searching for the course');
}
});

app.listen(process.env.PORT ?? 3000);
void initializeMySQL();

Expand Down
130 changes: 113 additions & 17 deletions api/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Connection, createConnection } from 'mysql2';
import { Professor, ProfessorUpdate } from './Professor';
import { getProfessorByName, getAllProfessor } from '../scraper/scraper';
import 'dotenv/config';
import { CourseInfo } from './CourseInfo';

let connection: Connection;

Expand Down Expand Up @@ -237,17 +238,70 @@ export async function getProfNames(): Promise<object[]> {
/* --- Curriculum FUNCTIONS --- */

/**
* Adds a new course to the 'Curriculum' table with all the necessary course details as parameters.
* @param courseName The name of the course
* @param courseNumber The number of the course
* @param preReqs The prerequisites of the course
* @param coReqs The corequeuistes of the course
* Retrieves the total count of courses currently stored in the Curriculum table.
* @returns {Promise<number>} A promise that resolves to the number of courses.
*/
async function getCoursesCount(): Promise<number> {
const result = await execute(`SELECT COUNT(*) FROM Curriculum`);
const resultAmount = Object.values(result[0])[0] as number;
return resultAmount;
}

/**
* Fetches the latest courses from a specified URL and updates the local database.
* If the number of courses fetched matches the count in the database, the update is skipped.
* This function aims to prevent redundant updates on every process restart.
*/
async function fetchAndUpdateCourses(): Promise<void> {
const year = '2023-2024';
const url = `https://raw.githubusercontent.com/blu3eee/cpp-courses/main/parsed/courses_${year}.json`;

try {
const response = await fetch(url);
// Check if the request was successful
if (!response.ok) {
throw new Error(`Error fetching courses: ${response.statusText}`);
}

// Parse the JSON body of the response
const courses: Record<string, CourseInfo> = await response.json(); // Assuming this is an array of course objects
// Return if the courses amount is the same
// this logic can be further implemented,
// the current logic is just a temp guard to not check and create courses everytime the process restart on save (development env)
if (Object.values(courses).length === (await getCoursesCount())) {
return;
}
for (const course of Object.values(courses)) {
// Adapt based on your actual course object structure
// Assuming createCourse is a function you have for inserting course data into the database
await createCourse(
course.name,
course.id,
course.prerequisites,
course.corequisites,
course.units
);
}
} catch (error) {
console.error('Error fetching or updating courses:', error);
}
}

/**
* Inserts a new course into the Curriculum table.
* If the course already exists (determined by courseNumber), the insertion is skipped.
* @param {string} courseName - The name of the course.
* @param {string} courseNumber - The unique number of the course.
* @param {string} preReqs - The prerequisites of the course.
* @param {string} coReqs - The corequisites of the course.
* @param {string} units - The number of units the course is worth.
*/
export async function createCourse(
courseName: string,
courseNumber: string,
preReqs: string,
coReqs: string
coReqs: string,
units: string
): Promise<void> {
try {
// Check if course already exists in the database
Expand All @@ -258,19 +312,21 @@ export async function createCourse(
courseName,
courseNumber,
preReqs,
coReqs
) VALUES (?, ?, ?, ?)`,
[courseName, courseNumber, preReqs, coReqs]
coReqs,
units
) VALUES (?, ?, ?, ?, ?)`,
[courseName, courseNumber, preReqs, coReqs, units]
);
console.log(
`[SUCCESS] Course ${courseNumber} - ${courseName} has been added to the Curriculum.`
);
} else {
console.error(
`Course ${courseNumber} - ${courseName} already exists in the Curriculum.`
);
// console.error(
// `Course ${courseNumber} - ${courseName} already exists in the Curriculum.`
// );
}
} catch (err) {
console.error(`Error inserting a course to table Curriculum`);
console.error(err);
}
}
Expand All @@ -281,12 +337,14 @@ interface CurriculumCourse {
courseNumber: string;
preReqs: string;
coReqs: string;
units: string;
}

/**
* Updates an existing course by taking in the id of the target course, along with any course details that you intend to modify the target with.
* @param courseId The id of the target course
* @param updatedCourse An object containing the course details to be updated
* Updates the details of an existing course in the Curriculum table.
* If the course does not exist, an error is logged.
* @param {string} courseId - The ID of the course to update.
* @param {Partial<CurriculumCourse>} updatedCourse - An object containing the course details to be updated.
*/
export async function updateCourse(
courseId: string,
Expand All @@ -302,12 +360,14 @@ export async function updateCourse(
courseNumber = ?,
preReqs = ?,
coReqs = ?,
units = ?
WHERE id = ?`,
[
mergedCourse.courseName,
mergedCourse.courseNumber,
mergedCourse.preReqs,
mergedCourse.coReqs,
mergedCourse.units, // Convert units to string
courseId,
]
);
Expand Down Expand Up @@ -369,6 +429,37 @@ export async function getCourseById(
return result;
}

/**
* Searches the Curriculum for courses matching the given key phrase in either
* the course name or the course number.
* @param keyPhrase The phrase to search for.
* @returns A promise that resolves to an array of courses matching the search criteria.
*/
export async function searchCourses(keyPhrase?: string): Promise<any[]> {
// Escape the keyPhrase to prevent SQL injection
const escapedKeyPhrase = (keyPhrase ?? '')
.replace(/%/g, '\\%')
.replace(/_/g, '\\_');

// Use the LIKE operator with wildcard (%) for partial matches
// Concatenate both courseNumber and courseName with an OR for a broader search
const query = `
SELECT * FROM Curriculum
WHERE courseNumber LIKE ? OR courseName LIKE ?
`;

// Add wildcards to the search phrase for partial matching
const searchPattern = `%${escapedKeyPhrase}%`;

try {
const results = await execute(query, [searchPattern, searchPattern]);
return results;
} catch (error) {
console.error('Error searching for courses:', error);
throw error; // Re-throw the error for further handling, if necessary
}
}

/* --- MySQL FUNCTIONS --- */

/**
Expand Down Expand Up @@ -449,13 +540,16 @@ export async function initializeMySQL(): Promise<void> {
legacyId int
)
`);
// uncomment the line below to create the new Curriculum table on your first run
// void execute(`DROP table Curriculum`);
void execute(`
CREATE TABLE IF NOT EXISTS Curriculum (
id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
courseName varchar(255),
courseNumber varchar(255),
preReqs varchar(255),
coReqs varchar(255)
preReqs TEXT,
coReqs TEXT,
units varchar(255)
)
`);
void execute(`CREATE TABLE IF NOT EXISTS professorDB (
Expand Down Expand Up @@ -484,6 +578,8 @@ export async function initializeMySQL(): Promise<void> {
);
}

await fetchAndUpdateCourses();

console.log('MySQL server successfully started!');

// const sampleProf: Professor = {
Expand Down
65 changes: 36 additions & 29 deletions test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,37 +54,44 @@ describe('[Professor] 3 test cases:', function () {
});
});

describe('[Search] 3 test cases', function () {
it('Empty array', async function () {
const nameSend = {};
const res = await request(server).post('/search').send(nameSend);
describe('[Courses] 4 tests cases', function () {
// Test fetching all courses
it('GET /courses should return all courses', async function () {
const res = await request(server).get('/courses');
expect(res).to.have.status(200);
expect(res.body).to.be.an('array');
});

expect(res.body)
.to.be.an('object')
.that.includes({ err: 'please provide a json with key of count' });
// Test filtering courses with a specific key
it('GET /courses?key=computer should filter courses based on the key', async function () {
const key = 'computer'; // Use an actual key that you expect to filter by
const res = await request(server).get(`/courses?key=${key}`);
expect(res).to.have.status(200);
expect(res.body).to.be.an('array');
});
it('No count property', async function () {
const nameSend = {
test: 'val',
};
const res = await request(server).post('/search').send(nameSend);
expect(res.body)
.to.be.an('object')
.that.includes({ err: 'must specify the amount of professors needed' }) ||
expect(res.body)
.to.be.an('object')
.that.includes({ err: 'please specify a number' });

// Test fetching a course by its number
it('GET /courses/:courseNumber should return the course with the specific number', async function () {
const courseNumber = 'CS 1300'; // Use an actual course number from your database
const res = await request(server).get(`/courses/${courseNumber}`);
expect(res).to.have.status(200);
expect(res.body).to.be.an('object');
expect(res.body).to.include.keys([
'id',
'courseName',
'courseNumber',
'units',
'preReqs',
'coReqs',
]);
// Assert more specific properties of the course, such as matching the course number
expect(res.body.courseNumber).to.equal(courseNumber);
});
it('correct ', async function () {
const nameSend = {
count: 1,
};
const res = await request(server).post('/search').send(nameSend);
const keys = ['profs'];
expect(res.body).to.be.an('object').to.have.all.keys(keys);
expect(res.body.profs).to.be.a('array');
res.body.profs.forEach((element: any) => {
expect(element).to.be.a('number');
});

// Test fetching a course with an invalid course number
it('GET /courses/:courseNumber with an invalid number should return 404', async function () {
const invalidCourseNumber = 'INVALID123';
const res = await request(server).get(`/courses/${invalidCourseNumber}`);
expect(res).to.have.status(404);
});
});