Skip to content

Commit 4da03bd

Browse files
committed
modify relevant endpoints
1 parent 5bc37f5 commit 4da03bd

File tree

3 files changed

+116
-54
lines changed

3 files changed

+116
-54
lines changed

scrapers/nus-v2/env.example.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"appKey": "",
33
"studentKey": "",
4+
"ttApiKey": "",
5+
"courseApiKey": "",
6+
"acadApiKey": "",
7+
"acadAppKey": "",
48
"baseUrl": "",
59
"apiConcurrency": 5,
610
"elasticConfig": null
7-
}
11+
}

scrapers/nus-v2/src/config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { ClientOptions } from '@elastic/elasticsearch';
55
export type Config = Readonly<{
66
appKey: string;
77
studentKey: string;
8+
ttApiKey: string;
9+
courseApiKey: string;
10+
acadApiKey: string;
11+
acadAppKey: string;
812

913
// Base URL for all API requests
1014
baseUrl: string;
@@ -39,12 +43,16 @@ const config: Config = {
3943
// From env
4044
appKey: env.appKey,
4145
studentKey: env.studentKey,
46+
ttApiKey: env.ttApiKey,
47+
courseApiKey: env.courseApiKey,
48+
acadApiKey: env.acadApiKey,
49+
acadAppKey: env.acadAppKey,
4250
elasticConfig: env.elasticConfig,
4351
baseUrl: addTrailingSlash(env.baseUrl),
4452
apiConcurrency: env.apiConcurrency || 5,
4553

4654
// Other config
47-
academicYear: '2025/2026',
55+
academicYear: '2024/2025',
4856
dataPath: path.resolve(__dirname, '../data'),
4957
};
5058

scrapers/nus-v2/src/services/nus-api.ts

Lines changed: 102 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ export interface INusApi {
8787
type ApiParams = {
8888
[key: string]: string;
8989
};
90+
type ApiHeaders = {
91+
[key: string]: string;
92+
};
9093

9194
// Error codes specified by the API. Note that these, like many other things
9295
// in the API, are not to be relied upon completely
@@ -95,13 +98,23 @@ const OKAY: STATUS_CODE = '00000';
9598
const AUTH_ERROR: STATUS_CODE = '10000';
9699
const RECORD_NOT_FOUND: STATUS_CODE = '10001';
97100

98-
// Authentication via tokens sent through headers
99-
const headers = {
100-
'X-APP-API': config.appKey,
101-
'X-STUDENT-API': config.studentKey,
101+
// Shared headers for all API requests
102+
const commonHeaders: ApiHeaders = {
102103
'Content-Type': 'application/json',
103104
};
104105

106+
// Authentication via tokens sent through headers
107+
const ttHeaders: ApiHeaders = {
108+
'X-API-KEY': config.ttApiKey,
109+
};
110+
const courseHeaders: ApiHeaders = {
111+
'X-API-KEY': config.courseApiKey,
112+
};
113+
const acadHeaders: ApiHeaders = {
114+
'X-API-KEY': config.acadApiKey,
115+
'X-APP-KEY': config.acadAppKey,
116+
};
117+
105118
/**
106119
* Map the error code from the API to the correct class
107120
*/
@@ -129,21 +142,32 @@ function mapErrorCode(code: string, msg: string) {
129142
* configuration such as authentication which all API calls should have,
130143
* as well as error handling.
131144
*/
132-
async function callApi<Data>(endpoint: string, params: ApiParams): Promise<Data> {
145+
async function callApi<Data>(
146+
endpoint: string,
147+
params: ApiParams,
148+
headers: ApiHeaders,
149+
): Promise<Data> {
133150
// 1. Construct request URL
134151
const url = new URL(endpoint, config.baseUrl);
152+
153+
// 2. Encode params in the query string for GET requests
154+
Object.entries(params).forEach(([key, value]) => {
155+
url.searchParams.append(key, value);
156+
});
157+
135158
let response;
136159

137160
try {
138-
// 2. All API requests use POST HTTP method with params encoded in JSON
139-
// in the body
140-
response = await axios.post(url.href, params, {
141-
transformRequest: [(data) => JSON.stringify(data)],
142-
// 3. Apply authentication using header
143-
headers,
161+
// 3. All API requests use GET HTTP method with params encoded in the query string.
162+
response = await axios.get(url.href, {
163+
// 4. Apply authentication using headers
164+
headers: {
165+
...commonHeaders,
166+
...headers,
167+
},
144168
});
145169
} catch (e) {
146-
// 4. Handle network / request level errors, eg. server returning non-200
170+
// 5. Handle network / request level errors, eg. server returning non-200
147171
// status code
148172
let message;
149173
if (e.response) {
@@ -188,8 +212,8 @@ class NusApi implements INusApi {
188212
/**
189213
* Wrapper around base callApi method that pushes the call into a queue
190214
*/
191-
callApi = async <T>(endpoint: string, params: ApiParams) =>
192-
this.queue.add(() => callApi<T>(endpoint, params));
215+
callApi = async <T>(endpoint: string, params: ApiParams, headers: ApiHeaders) =>
216+
this.queue.add(() => callApi<T>(endpoint, params, headers));
193217

194218
/**
195219
* Calls the modules endpoint
@@ -198,10 +222,14 @@ class NusApi implements INusApi {
198222
try {
199223
// DO NOT remove this await - the promise must settle so the catch
200224
// can handle the NotFoundError from the API
201-
return await this.callApi<ModuleInfo[]>('module', {
202-
term,
203-
...params,
204-
});
225+
return await this.callApi<ModuleInfo[]>(
226+
'CourseNUSMods',
227+
{
228+
term,
229+
...params,
230+
},
231+
courseHeaders,
232+
);
205233
} catch (e) {
206234
// The modules endpoint will return NotFound even for valid inputs
207235
// that just happen to have no records, so we ignore this error
@@ -215,18 +243,22 @@ class NusApi implements INusApi {
215243
};
216244

217245
getFaculty = async (): Promise<AcademicGrp[]> =>
218-
this.callApi('config/get-acadgroup', {
219-
eff_status: 'A',
220-
// % is a wildcard so this function returns everything
221-
acad_group: '%',
222-
});
246+
this.callApi(
247+
'edurec/config/v1/get-acadgroup',
248+
{
249+
eff_status: 'A',
250+
},
251+
acadHeaders,
252+
);
223253

224254
getDepartment = async (): Promise<AcademicOrg[]> =>
225-
this.callApi('config/get-acadorg', {
226-
eff_status: 'A',
227-
// % is a wildcard so this function returns everything
228-
acad_org: '%',
229-
});
255+
this.callApi(
256+
'edurec/config/v1/get-acadorg',
257+
{
258+
eff_status: 'A',
259+
},
260+
acadHeaders,
261+
);
230262

231263
getModuleInfo = async (term: string, moduleCode: ModuleCode): Promise<ModuleInfo> => {
232264
// Module info API takes in subject and catalog number separately, so we need
@@ -239,11 +271,15 @@ class NusApi implements INusApi {
239271

240272
// catalognbr = Catalog number
241273
const [subject, catalognbr] = parts;
242-
const modules = await this.callApi<ModuleInfo[]>('module', {
243-
term,
244-
subject,
245-
catalognbr,
246-
});
274+
const modules = await this.callApi<ModuleInfo[]>(
275+
'CourseNUSMods',
276+
{
277+
term,
278+
subject,
279+
catalognbr,
280+
},
281+
courseHeaders,
282+
);
247283

248284
if (modules.length === 0) throw new NotFoundError(`Module ${moduleCode} cannot be found`);
249285
return modules[0];
@@ -256,34 +292,44 @@ class NusApi implements INusApi {
256292
this.callModulesEndpoint(term, { acadorg: departmentCode });
257293

258294
getModuleTimetable = async (term: string, module: ModuleCode): Promise<TimetableLesson[]> =>
259-
this.callApi('classtt/withdate/published', {
260-
term,
261-
module,
262-
});
295+
this.callApi(
296+
'timetable/v1/published/class/withdate',
297+
{
298+
term,
299+
module,
300+
},
301+
ttHeaders,
302+
);
263303

264304
getDepartmentTimetables = async (
265305
term: string,
266306
departmentCode: string,
267307
): Promise<TimetableLesson[]> =>
268-
this.callApi('classtt/withdate/published', {
269-
term,
270-
deptfac: departmentCode,
271-
});
308+
this.callApi(
309+
'timetable/v1/published/class/withdate',
310+
{
311+
term,
312+
deptfac: departmentCode,
313+
},
314+
ttHeaders,
315+
);
272316

273317
getSemesterTimetables = async (
274318
term: string,
275319
lessonConsumer: (lesson: TimetableLesson) => void,
276320
): Promise<void> =>
277321
new Promise((resolve, reject) => {
278-
const endpoint = 'classtt/withdate/published';
322+
const endpoint = 'timetable/v1/published/class/withdate';
279323
const url = new URL(endpoint, config.baseUrl);
280-
const body = JSON.stringify({ term });
324+
url.searchParams.append('term', term);
281325

282326
oboe({
283327
url: url.href,
284-
headers,
285-
body,
286-
method: 'POST',
328+
headers: {
329+
...commonHeaders,
330+
...ttHeaders,
331+
},
332+
method: 'GET',
287333
})
288334
.node('data[*]', (lesson: TimetableLesson) => {
289335
// Consume and discard each lesson
@@ -298,7 +344,7 @@ class NusApi implements INusApi {
298344
resolve();
299345
} else {
300346
const error = mapErrorCode(code, msg);
301-
error.requestConfig = { url: url.href, data: body };
347+
error.requestConfig = { url: url.href };
302348
reject(error);
303349
}
304350
})
@@ -314,18 +360,22 @@ class NusApi implements INusApi {
314360
});
315361

316362
getModuleExam = async (term: string, module: ModuleCode): Promise<ModuleExam> => {
317-
const exams = await this.callApi<ModuleExam[]>('examtt/published', {
318-
term,
319-
module,
320-
});
363+
const exams = await this.callApi<ModuleExam[]>(
364+
'timetable/v1/published/exam',
365+
{
366+
term,
367+
module,
368+
},
369+
ttHeaders,
370+
);
321371

322372
if (exams.length === 0)
323373
throw new NotFoundError(`Exams for ${module} cannot be found, or the module has no exams`);
324374
return exams[0];
325375
};
326376

327377
getTermExams = async (term: string): Promise<ModuleExam[]> =>
328-
this.callApi('examtt/published', { term });
378+
this.callApi('timetable/v1/published/exam', { term }, ttHeaders);
329379
}
330380

331381
// Export as default a singleton instance to be used globally

0 commit comments

Comments
 (0)