-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathloader.js
556 lines (529 loc) · 21.8 KB
/
loader.js
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
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
const xml2js = require('xml2js')
const pg = require('pg')
const fs = require('fs')
const crypto = require('crypto')
const hash = crypto.createHash
const db = require('./db')
const { type } = require('os')
const knexfile = require('./knexfile')
// For debugging purposes
var inspect = require('eyes').inspector({maxLength: false})
var importFile = process.argv[2]
var students
var depts
var courses
var uuids
var employees = new Map
var instructors = new Map
var employees_in_db
var instructors_in_db
var students_in_db
var uuids_in_db
var sfuids_present = []
var updates = {
updated: 0,
reactivated: 0,
inserted: 0,
removed: 0,
groupsadded: 0
}
// Load the Grouper_Loader_Groups config from json file
// The file must contain an array of JSON objects, each specified as such:
// { view: "grouper_academic_plans_v",
// loader: "plans",
// hasSemester: false
// }
// The 'hasTerm' property is optional. If present and true, it indicates that a 'semester' should be
// included in the WHERE clause to limit the results to the current semester
var rawdata = fs.readFileSync('grouper-loader.json');
var grouperLoaders = JSON.parse(rawdata);
console.log('Loaded '+ grouperLoaders.length + ' GrouperLoader definitions')
// Suck in the import file
var stripPrefix = xml2js.processors.stripPrefix
var parser = new xml2js.Parser( {
explicitRoot: false,
trim: true,
explicitArray: false,
// Force tags to lowercase
normalizeTags: true,
attrNameProcessors: [stripPrefix],
tagNameProcessors: [stripPrefix]
})
fs.readFile(importFile, 'utf8', async function(err, data) {
if (data.startsWith('uuid')) {
// Process UUID import
uuids_in_db = await loadUuids();
uuids = data.toString().split('\n')
await processUuidImport();
await db.queue.onIdle()
console.log('New Users added: ' + updates.inserted)
// at the end of the UUID import, also process the GrouperLoader groups.
groups_in_db = await loadGrouperLoaderGroups();
await processGrouperLoaderGroups();
console.log('New LoaderGroups added: ' + updates.groupsadded)
}
else if (data.startsWith('DEPT')) {
// process DEPT import file
}
else {
// process XML file import
console.log("Loading XML extract from " + importFile)
parser.parseString(data.replace(/&/g,'&'), async function (err, extract) {
if (err !== null) {
console.log("Error parsing input. Can't continue. " + err);
}
else {
var timestamp = extract.$.Timestamp
// TODO: Compare timestamp against last processed one in DB and abort if older/same
console.log("Extract time stamp: " + timestamp)
if (extract.hasOwnProperty('student')) {
students = extract.student
students_in_db = await loadFromDb('SIMS')
await processStudentImport()
}
else if (extract.hasOwnProperty('department')) {
depts = extract.department
employees_in_db = await loadFromDb('HAP')
await processEmployeeImport()
}
else if (extract.hasOwnProperty('course')) {
courses = extract.course
//inspect(extract.course)
instructors_in_db = await (loadFromDb('SIMSINSTRUCT'))
await processInstructorImport()
}
else {
console.log("Extracted data unrecognized")
}
}
await db.queue.onIdle()
// There has to be a better way, but the last DB action counter doesn't get updated until after we get here, so one of these counters may be off by one
console.log('Done')
console.log('Users with updates: ' + updates.updated)
console.log('Users reactivated: ' + updates.reactivated)
console.log('New Users added: ' + updates.inserted)
console.log('Users removed from feed: ' + updates.removed)
});
}
});
// Clean up some data fields in the student object.
// This primarily just converts certain single-element values that could be arrays
// into arrays to ensure consistency
//
function normalizeStudent(student) {
if ( typeof student.reginfo === 'undefined' ) { return }
if ( typeof student.reginfo.affiliation !== 'undefined' && !Array.isArray(student.reginfo.affiliation)) {
student.reginfo.affiliation = [student.reginfo.affiliation]
}
if (typeof student.reginfo.program !== 'undefined' && !Array.isArray(student.reginfo.program)) {
student.reginfo.program = [student.reginfo.program]
}
if (typeof student.reginfo.course !== 'undefined' && !Array.isArray(student.reginfo.course)) {
student.reginfo.course = [student.reginfo.course]
}
}
// Generate an MD5 hash of the JSONified student object and store it in the object
function genhash(person) {
person.hash = hash('md5').update(JSON.stringify(person)).digest('base64')
}
/* After the import file has been loaded, process it
* For each student:
* - Normalize the JS object produced
* - Generate a hash based on the JSON version of the object
* - Save the list of all SFUIDs seen
* - Update or add changed/new students in DB
* - Mark, as inactive, students who are no longer in the SoR feed
*/
async function processStudentImport() {
students.forEach((student) => {
normalizeStudent(student)
genhash(student)
sfuids_present.push(student.sfuid)
})
console.log("Active students loaded from XML: " + sfuids_present.length)
students.forEach(async (student) => {
let isUpdate = students_in_db.has(student.sfuid)
if (!isUpdate || students_in_db.get(student.sfuid) !== student.hash) {
await updateDbForPerson(student, 'SIMS', isUpdate)
}
})
if (students_in_db.size > 0) {
for (const sfuid of students_in_db.keys()) {
//console.log("present: " + sfuids_present.includes(sfuid))
if (!sfuids_present.includes(sfuid)) {
// Student is no longer in SoR feed. Set to inactive
try {
var rows = await db.updateSorObject({sfuid: sfuid, source: 'SIMS'},{status:'inactive'})
if (rows.length) {
updates.removed++
console.log(sfuid + " removed from REG feed. Setting to inactive")
}
else { console.log("what the..?")}
} catch(err) {
console.log("Error updating status for student: " + sfuid)
console.log(err)
}
}
}
}
}
// Process the Employee import data. The import comes to us as an array of departments,
// each with an array of employees. We need an array of employees each with an array of
// jobs/departments. Do the conversion and then compare the resulting employee
// objects against the DB
async function processEmployeeImport() {
let persons;
depts.forEach((dept) => {
let empls = dept.employees
let role = 'employee'
if (empls.hasOwnProperty('applicant')) {
persons = empls.applicant
role = 'applicant'
}
else if (empls.hasOwnProperty('emp')) {
persons = empls.emp
}
else {
console.log("Unrecognized department employee type")
inspect(empls)
return
}
if (!Array.isArray(persons)) {
persons = [persons]
}
persons.forEach((person) => {
//console.log("Processing " + person.sfuid)
// According to Amaint, these are the documented Status flags an employee could have in a job:
// A - Active?
// L - ? treat as active
// P - ? treat as active
// W - ? treat as active
// U - ? treat as active
// Q - Retired
// R - Retired
// T - Terminated?
//
// We will default to status == inactive but set to active if any job isn't in the 'T' state
person.role = role
person.status = 'inactive'
if (typeof person.job !== 'undefined') {
if (!Array.isArray(person.job)) {
person.job = [person.job]
}
person.job.forEach((job) => {
if ( typeof dept.deptcode !== 'undefined') {
job.deptcode = dept.deptcode
}
if ( typeof dept.deptname !== 'undefined') {
job.deptname = dept.deptname
}
if (job.status !== 'T') {
person.status = 'active'
}
})
}
if (!employees.has(person.sfuid)) {
employees.set(person.sfuid,person)
}
else {
//console.log("Adding job to " + person.sfuid)
let newperson = employees.get(person.sfuid)
newperson.job.push(...person.job)
if (newperson.status !== 'active') {
newperson.status = person.status
}
employees.set(person.sfuid,newperson)
//inspect(newperson)
}
})
})
employees.forEach(async (person) => {
genhash(person)
let isUpdate = employees_in_db.has(person.sfuid)
if (!isUpdate || employees_in_db.get(person.sfuid) !== person.hash) {
await updateDbForPerson(person, 'HAP', isUpdate)
}
})
if (employees_in_db.size > 0) {
for (const sfuid of employees_in_db.keys()) {
//console.log("present: " + sfuids_present.includes(sfuid))
if (!employees.has(sfuid)) {
// Employee is no longer in SoR feed. Set to inactive
try {
var rows = await db.updateSorObject({sfuid: sfuid, source: 'HAP'},{status:'inactive'})
if (rows.length) {
updates.removed++
console.log(sfuid + " removed from HAP feed. Setting to inactive")
}
else { console.log("what the..?")}
} catch(err) {
console.log("Error updating status for employee: " + sfuid)
console.log(err)
}
}
}
}
}
// Process the Instructors import data. The import comes to us as an array of courses,
// each with an array of sections with an array of instructors.
// We need an array of instructors each with an array of sections.
// Do the conversion and then compare the resulting instructor objects against the DB
async function processInstructorImport() {
courses.forEach((course) => {
if ( typeof course.classsections !== 'undefined'
&& typeof course.classsections.associated !== 'undefined') {
let assocs = course.classsections.associated
if (!Array.isArray(assocs)) {
assocs = [assocs]
}
assocs.forEach((assoc) => {
let sections = assoc.component
if (typeof sections === 'undefined') {
// placeholder course - no defined sections or instructors yet
return
}
if (!Array.isArray(sections)) {
sections = [sections]
}
sections.forEach((section) => {
if (typeof section.instructor !== 'undefined') {
let instructs = section.instructor
if (!Array.isArray(instructs)) {
instructs = [instructs]
}
instructs.forEach((instruct) => {
// Sanity check on the SFUID field
if (instruct.id.length < 5 || instruct.id < 9999) {
return
}
let instructor = {}
instructor.sfuid = instruct.id
instructor.sections =
[
{
term: course.term,
name: course.crsename,
num: course.crsenum,
title: course.crsetitle,
code: section.$.code,
section: section.sect,
type: section.classtype,
status: section.classstat,
rolecode: instruct.rolecode
}
]
if (instructors.has(instructor.sfuid)) {
let newinstructor = instructors.get(instructor.sfuid)
newinstructor.sections.push(...instructor.sections)
instructors.set(instructor.sfuid,newinstructor)
}
else {
instructors.set(instructor.sfuid,instructor)
}
})
}
})
})
}
})
instructors.forEach(async (person) => {
genhash(person)
let isUpdate = instructors_in_db.has(person.sfuid)
if (!isUpdate || instructors_in_db.get(person.sfuid) !== person.hash) {
await updateDbForPerson(person, 'SIMSINSTRUCT', isUpdate)
}
})
if (instructors_in_db.size > 0) {
for (const sfuid of instructors_in_db.keys()) {
//console.log("present: " + sfuids_present.includes(sfuid))
if (!instructors.has(sfuid)) {
// Employee is no longer in SoR feed. Set to inactive
try {
var rows = await db.updateSorObject({sfuid: sfuid, source: 'SIMSINSTRUCT'},{status:'inactive'})
if (rows.length) {
updates.removed++
console.log(sfuid + " removed from Instructor feed. Setting to inactive")
}
else { console.log("what the..?")}
} catch(err) {
console.log("Error updating status for employee: " + sfuid)
console.log(err)
}
}
}
}
}
async function processUuidImport() {
uuids.filter(v => ! v.includes('external_idNo')).forEach(async (line) => {
let fields = line.split(/\s+/,2)
try {
if (typeof fields === 'undefined' || fields[0] == null || fields[0].length == 0 || fields[1] == null || fields[1].length == 0 ) {
console.log("Skipping null entry: " + line);
}
// Check if a record already exists
else if (! uuids_in_db.includes(fields[0])) {
// Nope. Add one
await db.addUuid({uuid: fields[0], sfuid: fields[1]});
updates.inserted++;
}
} catch(err) {
console.log("Error processing UUID entry for " + line);
console.log(err);
}
});
}
// For each object in the Grouper_loader.json file, do a DB query to get the
// current list of groups in that loader's view. Add any missing groups to the grouper_loader_groups table
// { view: "grouper_academic_plans_v",
// loader: "plans",
// hasSemester: false
// }
async function processGrouperLoaderGroups() {
for (const job of grouperLoaders) {
viewgroups = new Array
var tmpgroups
if (job.hasSemester) {
tmpgroups = await db.getGrouperView(job.view,{semester: currentTermCode()})
viewgroups.push(...tmpgroups)
tmpgroups = await db.getGrouperView(job.view,{semester: nextTermCode()})
viewgroups.push(...tmpgroups)
} else {
tmpgroups = await db.getGrouperView(job.view)
viewgroups.push(...tmpgroups)
}
viewgroups.forEach((vgroup) => {
if (!groups_in_db.has(job.loader)) {
groups_in_db.set(job.loader, new Array)
}
if (!groups_in_db.get(job.loader).includes(vgroup)) {
await db.addGrouperLoaderGroup({group_name: vgroup, loader: job.loader})
updates.groupsadded++
}
})
}
}
// Load the contents of the grouper_loader table into a hash of arrays (one array per loader view)
async function loadGrouperLoaderGroups() {
let groups_in_db = new Map
try {
var rows = await db.getGrouperLoaderGroups();
if (rows != null) {
rows.forEach((row) => {
if (!groups_in_db.has(row.loader)) {
var groupArray = new Array
groups_in_db.set(row.loader,groupArray)
}
groups_in_db.get(row.loader).push(row.group_name)
})
}
} catch(err) {
console.log("Error loading LoaderGroups from DB")
console.log(err)
throw new Error("Something went badly wrong!");
}
return groups_in_db
}
async function loadFromDb(source) {
var users_in_db = new Map
try {
var rows = await db.getSorObjects(['sfuid','hash'],{status:'active',source: source})
if (rows != null) {
rows.forEach((row) => {
users_in_db.set(row.sfuid,row.hash)
})
console.log("Active users loaded from DB for " + source + ": " + users_in_db.size)
}
} catch(err) {
console.log("Error loading users from DB. Can't continue!")
console.log(err)
throw new Error("Something went badly wrong!");
}
return users_in_db;
}
async function loadUuids() {
let users_in_db
try {
var rows = await db.getUuid();
if (rows != null) {
users_in_db = Array.from(rows, row => row.uuid)
}
} catch(err) {
console.log("Error loading UUIDs from DB")
console.log(err)
throw new Error("Something went badly wrong!");
}
return users_in_db
}
async function updateDbForPerson(person,source,isUpdate) {
var rows
var update_type = "updated"
try {
if (isUpdate) {
// Update an existing active person's record
rows = await db.updateSorObject(
{sfuid: person.sfuid, source: source},
{
hash: person.hash,
lastname: person.lastname,
firstnames: person.firstnames,
userdata: JSON.stringify(person)
})
console.log("Updated '" + source + "' record for: " + person.sfuid)
}
else {
// Check whether the person exists in the DB at all yet
rows = await db.getSorObjects('id',{sfuid: person.sfuid, source: source})
if ( rows.length > 0) {
update_type = "reactivated"
// Person is in the DB but inactive. Update
rows = await db.updateSorObject({sfuid: person.sfuid, source: source},{
status: 'active',
hash: person.hash,
lastname: person.lastname,
firstnames: person.firstnames,
userdata: JSON.stringify(person)
})
// TODO: If there are any other actions to kick off when a person re-appears, do it here
console.log("Reactivated '" + source + "' record for: " + person.sfuid)
}
else {
// Person not in DB. Insert
update_type = "inserted"
rows = await db.addSorObject({
sfuid: person.sfuid,
status: 'active',
hash: person.hash,
lastname: person.lastname,
firstnames: person.firstnames,
source: source,
userdata: JSON.stringify(person)
})
console.log("Added '" + source + "' record for: " + person.sfuid)
}
}
if (rows.length > 0) {
updates[update_type]++
//console.log(update_type + " = " + updates[update_type])
if ((updates[update_type] % 1000) == 0) {
console.log(update_type + " " + updates[update_type] + " users")
}
}
} catch(err) {
console.log("Error processing user: " + person.sfuid)
console.log(err.message)
}
}
var currentTermCode = function() {
var date = new Date();
var month = date.getMonth();
var centuryDigit = '1'; // I'll be long-dead before this is an issue
var yearDigits = date.getFullYear().toString().substr(-2);
var termDigit = month < 4 ? '1' : month >= 8 ? '7' : '4'
return centuryDigit + yearDigits + termDigit;
};
var nextTermCode = function() {
var curterm = currentTermCode();
var offset = curterm.charAt(3) === '7' ? 4 : 3;
var nextTerm = parseInt(curterm)+offset;
return nextTerm.toString();
};