-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsql.ts
519 lines (486 loc) · 16.1 KB
/
sql.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
import { Connection, createConnection } from 'mysql2';
import { Professor, ProfessorUpdate } from './Professor';
import { getProfessorByName, getAllProfessor } from '../scraper/scraper';
import 'dotenv/config';
let connection: Connection;
/**
* Internal use function to execute a single SQL command as prepared statements with error catching
* @param cmd Single quotation marks for one-line commands. Template string for multi-line commands
* @param placeholder Optional parameter used for SQL commands that require function parameters
* @returns Promise containing SQL query result
*/
async function execute(cmd: string, placeholder?: string[]): Promise<any> {
const data = await new Promise((resolve) =>
connection.execute(cmd, placeholder, (err: any, result: any) => {
if (err?.code === 'ER_ACCESS_DENIED_ERROR') {
console.log(
'Error: Please check that the .env file exists, matches the template in .env-template, and is in the same relative file location as .env-template'
);
process.kill(process.pid, 'SIGTERM');
} else if (err) {
console.log(err);
process.kill(process.pid, 'SIGTERM');
}
resolve(result);
})
);
return data;
}
/* --- rateMyProfessorDB FUNCTIONS --- */
/**
* Adds a new professor entry into `rateMyProfessorDB` using a professor's full name
* @param broncoDirectName Professor's BroncoDirect name
*/
export async function addProf(broncoDirectName: string): Promise<void> {
try {
const data = await getProfessorByName(broncoDirectName.toLowerCase());
if (data && Object.keys(data).length > 0) {
await addProfGraphQL({
profName: broncoDirectName.toLowerCase(),
firstName: data?.firstName,
lastName: data?.lastName,
avgDifficulty: data?.avgDifficulty,
avgRating: data?.avgRating,
numRatings: data?.numRatings,
wouldTakeAgainPercent: data?.wouldTakeAgainPercent,
id: data?.id,
legacyId: data?.legacyId,
});
} else {
console.error(
`Professor ${broncoDirectName} not found in GraphQL query.`
);
}
} catch (err) {
console.error(err);
}
}
/**
* Private function - use `addProf(broncoDirectName)` to query data.
*
* Adds a new professor entry into `rateMyProfessorDB` using data from GraphQL.
* @param {Professor} newProfessor See professor interface (/api/Professor.d.ts).
*/
async function addProfGraphQL({
profName,
firstName,
lastName,
avgDifficulty,
avgRating,
numRatings,
wouldTakeAgainPercent,
id,
legacyId,
}: Professor): Promise<void> {
try {
// Check if data already exists in db
const result = await profSearch(profName);
if (!result || Object.keys(result).length === 0) {
void execute(
`INSERT INTO rateMyProfessorDB (
profName,
firstName,
lastName,
avgDifficulty,
avgRating,
numRatings,
wouldTakeAgainPercent,
id,
legacyId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
profName.toLowerCase(),
firstName,
lastName,
avgDifficulty.toFixed(1),
avgRating.toFixed(1),
numRatings.toString(),
wouldTakeAgainPercent.toFixed(2),
id,
legacyId.toString(),
]
);
console.log(
`[SUCCESS] Professor ${profName} has been added to the database.`
);
} else {
console.error(`Professor ${profName} already exists in the database.`);
}
} catch (err) {
console.error(err);
}
}
/**
* Updates existing professor data in the SQL database.
* @param {Professor} newProfessor See professor interface (/api/Professor.d.ts).
*/
export async function updateProf({
profName,
avgDifficulty,
avgRating,
numRatings,
wouldTakeAgainPercent,
}: ProfessorUpdate): Promise<void> {
try {
void execute(
`UPDATE rateMyProfessorDB SET
avgDifficulty = ?,
avgRating = ?,
numRatings = ?,
wouldTakeAgainPercent = ?,
timeAdded = CURRENT_TIMESTAMP
WHERE profName = ?`,
[
avgDifficulty.toFixed(1),
avgRating.toFixed(1),
numRatings.toString(),
wouldTakeAgainPercent.toFixed(2),
profName,
]
);
console.log(`[SUCCESS] Professor ${profName} has been updated.`);
} catch (err) {
console.error(err);
}
}
/**
* Checks if a professor's data in the db is more than 3 months old.
* Current expiration date is 3 months, or 7884000 seconds.
* @param broncoDirectName BroncoDirect name.
* @returns {Promise<boolean>} True if prof data is 3mo+ old, false otherwise.
*/
export async function checkExpiredProfData(
broncoDirectName: string
): Promise<boolean> {
const name = broncoDirectName.toLowerCase();
const result = await execute(
'SELECT * FROM `rateMyProfessorDB` WHERE `profName` = ? AND TIMESTAMPDIFF(SECOND, `timeAdded`, NOW()) >= 7884000',
[name]
);
return Object.keys(result).length !== 0;
}
/**
* Finds a professor in the SQL Database given their BroncoDirect name.
* @param {string} broncoDirectName BroncoDirect name.
* @returns {Promise<Professor>} JSON of professor data.
* Value can be extracted by awaiting function call within an async function.
*/
export async function profSearch(broncoDirectName: string): Promise<Professor> {
const [result] = await execute(
'SELECT * FROM `rateMyProfessorDB` WHERE `profName` = ?',
[broncoDirectName]
);
if (result) {
result.avgDifficulty = parseFloat(result.avgDifficulty);
result.avgRating = parseFloat(result.avgRating);
result.wouldTakeAgainPercent = parseFloat(result.wouldTakeAgainPercent);
}
return result;
}
/* --- professorDB FUNCTIONS --- */
/**
* Adds a professor to the professorDB table in SQL database.
* @param bdFirst BroncoDirect Professor's first name
* @param bdLast BroncoDirect Professor's last name
*/
export function addProfName(bdFirst: string, bdLast: string): void {
const fullName = bdFirst + ' ' + bdLast;
void execute('INSERT INTO professorDB (broncoDirectName) VALUES (?)', [
fullName,
]);
}
/**
* Checks if a professor's name exists in the `professorDB` database (meaning they are a valid professor).
* @param broncoDirectName name to be checked.
* @returns SQL Query result based on BroncoDirect name.
*/
export async function checkProfName(
broncoDirectName: string
): Promise<object[]> {
const result = await execute(
'SELECT * FROM `professorDB` WHERE `broncoDirectName` = ?',
[broncoDirectName]
);
return result;
}
/**
* Checks if `professorDB` already has prof names in it.
* @returns Boolean indicating whether database has data in it
*/
async function checkProfDatabaseExist(): Promise<boolean> {
const result = await execute(`SELECT COUNT(*) FROM professorDB`);
const resultAmount = Object.values(result[0])[0] as number;
return resultAmount > 0;
}
/**
* Gets all professor names in `professorDB` in alphabetical order.
* @returns SQL Query result based on professor names.
*/
export async function getProfNames(): Promise<object[]> {
return await execute(
'SELECT `broncoDirectName` FROM `professorDB` ORDER BY `broncoDirectName` ASC'
);
}
/* --- 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
*/
export async function createCourse(
courseName: string,
courseNumber: string,
preReqs: string,
coReqs: string
): Promise<void> {
try {
// Check if course already exists in the database
const result = await getCourse(courseNumber);
if (!result || Object.keys(result).length === 0) {
await execute(
`INSERT INTO Curriculum (
courseName,
courseNumber,
preReqs,
coReqs
) VALUES (?, ?, ?, ?)`,
[courseName, courseNumber, preReqs, coReqs]
);
console.log(
`[SUCCESS] Course ${courseNumber} - ${courseName} has been added to the Curriculum.`
);
} else {
console.error(
`Course ${courseNumber} - ${courseName} already exists in the Curriculum.`
);
}
} catch (err) {
console.error(err);
}
}
interface CurriculumCourse {
id: string;
courseName: string;
courseNumber: string;
preReqs: string;
coReqs: 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
*/
export async function updateCourse(
courseId: string,
updatedCourse: Partial<CurriculumCourse>
): Promise<void> {
try {
const result = await getCourseById(courseId);
if (result && Object.keys(result).length > 0) {
const mergedCourse = { ...result, ...updatedCourse };
await execute(
`UPDATE Curriculum SET
courseName = ?,
courseNumber = ?,
preReqs = ?,
coReqs = ?,
WHERE id = ?`,
[
mergedCourse.courseName,
mergedCourse.courseNumber,
mergedCourse.preReqs,
mergedCourse.coReqs,
courseId,
]
);
console.log(`[SUCCESS] Course ${courseId} has been updated.`);
} else {
console.error(`Course ${courseId} not found in the Curriculum.`);
}
} catch (err) {
console.error(err);
}
}
/**
* Removes an existing course by taking in the id of the target course, and handles errors for invalid inputs.
* @param courseId The id of the target course to be deleted
*/
export async function deleteCourse(courseId: string): Promise<void> {
try {
const result = await getCourseById(courseId);
if (result && Object.keys(result).length > 0) {
await execute(`DELETE FROM Curriculum WHERE id = ?`, [courseId]);
console.log(
`[SUCCESS] Course ${courseId} has been deleted from the Curriculum.`
);
} else {
console.error(`Course ${courseId} not found in the Curriculum.`);
}
} catch (err) {
console.error(err);
}
}
/**
* Retrieves information for a specific course based on the course number.
* @param courseNumber The course number of the target course
* @returns A Promise that resolves to the course information
*/
export async function getCourse(
courseNumber: string
): Promise<CurriculumCourse> {
const [result] = await execute(
`SELECT * FROM Curriculum WHERE courseNumber = ?`,
[courseNumber]
);
return result;
}
/**
* Retrieves information for a specific course based on the course id.
* @param courseId The id of the target course
* @returns A Promise that resolves to the course information
*/
export async function getCourseById(
courseId: string
): Promise<CurriculumCourse> {
const [result] = await execute(`SELECT * FROM Curriculum WHERE id = ?`, [
courseId,
]);
return result;
}
/* --- MySQL FUNCTIONS --- */
/**
* Checks for active SQL connection
* @returns Boolean indicating whether connection is active
*/
export async function checkSQLConnection(): Promise<boolean> {
// makes sure connection has a ping command
if (!connection?.ping) {
return false;
}
try {
connection.ping();
return true;
} catch (err) {
return false;
}
}
/**
* Initializes mySQL in the backend by creating a connection to the mySQL server. See comments within the function for download instructions on mySQL by ctrl (windows) / option (mac) clicking the function name.
*/
export async function initializeMySQL(): Promise<void> {
// Initializing mySQL requires a local mySQL server download from https://dev.mysql.com/doc/refman/8.0/en/installing.html.
// Download based on the OS you have and select the recommended developer bundle in the executable.
// IMPORTANT NOTE: InnoDB is not required to run mySQL. Simply click next until you can create a username and password.
// When running locally, .env should have the following variables:
// SQL_PORT=3306
// HOST=localhost
// DB_USER='user that you created'
// PASSWORD='password that you created'
// When initializing, there is an option to start the mySQL server instance when your PC starts.
// If left unchecked, the mySQL server can be started by opening 'mySQL Installer - Community' and selecting the option to reconfigure the mySQL Server.
// User and password necessary for mySQL operations and connections are also initialized through the mySQL server setup.
// Create a .env file using .env-template as a template to create mySQL connection.
const mySQLConfig: {
host: string;
port: number;
user: string;
password: string;
database?: string;
} = {
host: process.env.HOST ?? 'localhost',
port: process.env.SQL_PORT ? parseInt(process.env.SQL_PORT) : 3306,
user: process.env.DB_USER ?? 'root',
password: process.env.PASSWORD ?? 'password',
database: 'broncoDirectMeDB',
};
connection = createConnection(mySQLConfig);
connection.connect((err: Error | null) => {
if (err) {
delete mySQLConfig.database;
connection = createConnection(mySQLConfig);
void execute('CREATE DATABASE IF NOT EXISTS `broncoDirectMeDB`');
console.log(
"Database created. Rerun the code to use the mySQL functions. NOTE: The current nodemon process must be killed with ctrl+c (windows) or cmd+c (mac) and restarted completely by running 'npm run api'."
);
// mySQL looks for a database on connection.
// New installations of mySQL don't have a database created, so it's created here to prevent errors when running mySQL commands
}
});
void execute(`
CREATE TABLE IF NOT EXISTS rateMyProfessorDB (
profID int NOT NULL PRIMARY KEY AUTO_INCREMENT,
profName varchar(255),
firstName varchar(255),
lastName varchar(255),
avgDifficulty decimal(2, 1),
avgRating decimal(2, 1),
numRatings int,
wouldTakeAgainPercent decimal(5, 2),
timeAdded timestamp DEFAULT CURRENT_TIMESTAMP,
id varchar(255),
legacyId int
)
`);
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)
)
`);
void execute(`CREATE TABLE IF NOT EXISTS professorDB (
profID int NOT NULL PRIMARY KEY AUTO_INCREMENT,
broncoDirectName varchar(255)
)`);
void execute(`CREATE TABLE IF NOT EXISTS votesDB (
id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
userID int,
voteType boolean
)`);
void execute(`
CREATE TABLE IF NOT EXISTS usersDB (
userID int NOT NULL PRIMARY KEY AUTO_INCREMENT,
userEmail varchar(255)
)`);
// If professorDB is empty, call graphQL `getAllProfessor` function to get array of all rmp professors (cpp)
// Professors not currently in RMP may not be included in this scraping
if (!(await checkProfDatabaseExist())) {
await getAllProfessor().then((result) =>
result.forEach((val, index) => {
addProfName(val.firstName, val.lastName);
console.log(`Added ${val.firstName + ' ' + val.lastName} - `, index);
})
);
}
console.log('MySQL server successfully started!');
// const sampleProf: Professor = {
// profName: 'Poppy Gloria',
// firstName: 'Poppy',
// lastName: 'Gloria',
// avgDifficulty: 4.2,
// avgRating: 3.4,
// numRatings: 12,
// wouldTakeAgainPercent: 85.545,
// id: 'abc123',
// legacyId: 123456,
// };
// const sampleProfArray: string[] = [
// 'Thanh Nguyen',
// 'Ben Steichen',
// 'Steven Camacho',
// 'Li Ge',
// ];
// console.log('\n[BRONCODIRECT] Testing Professor Functions.\n-----\n');
// console.log('[BRONCODIRECT] Add Poppy Gloria to table');
// await addProfGraphQL(sampleProf);
// const result = await profSearch('Poppy Gloria');
// console.log(result);
// await addProf(sampleProfArray[0]);
// const result = await profSearch('Thanh Nguyen');
// console.log(result);
// const result = await profSearch('Ben Steichen');
// console.log(result);
}