Skip to content
This repository was archived by the owner on Aug 4, 2022. It is now read-only.
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**/venv

# build
www/dist
**/dist

# cache
**/__pycache
Expand Down
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node dist/api_v2/app.js
16 changes: 16 additions & 0 deletions api_v2/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// import "module-alias/register";
import * as express from "express";
import * as cors from "cors";
import * as routes from "./routes";
import { logger } from "../shared/logger";

const app = express();
const port = process.env.PORT || 3000;

// Add middleware
app.use(cors());
app.use(routes.courses);

app.listen(port, () => {
logger.info(`Server running on port ${port}`);
});
35 changes: 35 additions & 0 deletions api_v2/libs/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { logger } from "../../shared/logger";
import { DB_URI } from "../../shared/config";
import { MongoClient, Db } from "mongodb";

// The singleton MongoClient object
let client: MongoClient;
// The singleton DB object
let db: Db;

/** Connects to database. */
export async function connectToDb() {
try {
if (!client) {
client = new MongoClient(DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
}
if (!client.isConnected()) {
await client.connect();
}
} catch (e) {
logger.error("Failed to connect to database");
logger.error(e);
}
}

/** Returns a database instance. */
export async function getDbInstance() {
if (!db) {
await connectToDb();
db = client.db("db");
}
return db;
}
115 changes: 115 additions & 0 deletions api_v2/models/course.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { getDbInstance } from "../libs/db";

type CourseSnippet = {
course_code: string;
course_title: string;
postgrad: boolean;
};

type CourseDetail = {
course_code: string;
au: number;
course_title: string;
constraint: {
prerequisite: string[];
na_to: string[];
na_to_all: string[];
mutex: string;
};
as_ue?: boolean;
as_pe?: boolean;
pass_fail: boolean;
semesters: string[];
description: string;
last_update: Date;
postgrad: boolean;
};

export async function getAllCourseSnippet(): Promise<Array<CourseSnippet>> {
// Connect to Db and get collection
const db = await getDbInstance();
const collection = db.collection("courses");

// Query database
const result: Array<CourseSnippet> = await collection
.aggregate<CourseSnippet>([
{
$project: {
_id: 0,
course_code: 1,
course_title: 1,
postgrad: 1,
},
},
])
.toArray();

// Return result
return result;
}

export async function getCourseDetailByCourseCode(
courseCode: string
): Promise<CourseDetail | null> {
// Connect to Db and get collection
const db = await getDbInstance();
const collection = db.collection("courses");

// Pre-process course code
courseCode = courseCode.toUpperCase();

// Query database
const result = await collection.findOne<CourseDetail | null>(
{
course_code: courseCode,
},
{
projection: {
_id: 0,
},
}
);

return result;
}

/**
* Returns an array of course snippets in which course titles match the regex.
* Currently, only supports title matching; TODO: add description matching in the future.
* @param query regex
*/
export async function getCourseSnippetByRegex(
query: string
): Promise<Array<CourseSnippet>> {
if (!query || query.trim().length <= 1) {
return [];
}

// Connect to Db and get collection
const db = await getDbInstance();
const collection = db.collection("courses");

// Build regex
const keywords: string[] = query.trim().split(" ");
const regexPreBuild = keywords.map((val) => "(.*" + val + ".*)+").join("");
const regex = RegExp(regexPreBuild, "i");

// Search
const result = collection
.aggregate([
{
$match: { course_title: regex },
},
{
$project: {
_id: 0,
course_code: 1,
course_title: 1,
postgrad: 1,
},
},
])
.toArray();

return result;
}
17 changes: 17 additions & 0 deletions api_v2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "api_v2",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "ts-node-dev app.ts"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1"
},
"devDependencies": {
"@types/cors": "^2.8.7",
"@types/express": "^4.17.8"
}
}
35 changes: 35 additions & 0 deletions api_v2/routes/courses/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { logger } from "../../../shared/logger";
import { Router, Request, Response } from "express";
import {
getCourseDetailByCourseCode,
getAllCourseSnippet,
getCourseSnippetByRegex,
} from "../../models/course";

/** Router instance. */
const courses = Router();

courses.get("/course/all", async (req: Request, res: Response) => {
logger.info("Getting course list.");
// await connectToDb();
logger.info("Connected to DB.");
// const course = new Course();
const courseList = await getAllCourseSnippet();
return res.json(courseList);
});

courses.get("/course/search", async (req: Request, res: Response) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我感觉这里和上面的 /course/all 可以合并更符合REST的面向资源的思想

all API: /courses
query API: /course?q=xxx
getOne API: /courses/:courseID

const { query } = req.query;
logger.info(`Searching for ${query}`);
const searchResult = await getCourseSnippetByRegex(query as string);
return res.json(searchResult);
});

courses.get("/course/:course_code", async (req: Request, res: Response) => {
const courseCode = req.params.course_code.toUpperCase();
logger.info(`Getting course detail for ${courseCode}`);
const courseDetail = await getCourseDetailByCourseCode(courseCode);
return res.json(courseDetail);
});

export { courses };
3 changes: 3 additions & 0 deletions api_v2/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { courses } from "./courses";

export { courses };
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@
"private": true,
"license": "MIT",
"scripts": {
"run:scraper": "ts-node scraper/scraper.ts"
"postinstall": "tsc",
"build": "tsc",
"run:scraper": "ts-node scraper/scraper.ts",
"api:run": "node dist/api_v2/app.js",
"api:debug": "ts-node-dev api_v2/app.ts"
},
"_moduleAliases": {
"@root": "./"
},
"engines": {
"node": " >= 10.14.2"
},
"workspaces": [
"shared",
"www",
"scraper"
"scraper",
"api_v2"
],
"dependencies": {
"module-alias": "^2.2.2",
"ts-node": "^9.0.0",
"ts-node-dev": "^1.0.0",
"typescript": "^4.0.3"
}
}
29 changes: 29 additions & 0 deletions shared/logger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as winston from "winston";

const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
defaultMeta: { service: "user-service" },
transports: [
//
// - Write to all logs with level `info` and below to `combined.log`
// - Write all logs error (and below) to `error.log`.
//
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});

//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}

export { logger };
3 changes: 2 additions & 1 deletion shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"mongodb": "^3.6.0",
"mongodb-memory-server": "^6.6.7",
"ts-node-dev": "^1.0.0-pre.61",
"typescript": "^4.0.2"
"typescript": "^4.0.2",
"winston": "^3.3.3"
}
}
9 changes: 5 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"exclude": ["functions", "**/*.test.ts"],
"exclude": ["functions", "scraper", "**/*.test.ts"],
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
Expand All @@ -9,8 +9,9 @@
"strict": true,
"target": "es2017",
"baseUrl": "./",
"outDir": "dist",
"paths": {
"@root/*": ["./*"]
},
},
}
}
}
}
Loading