itachi1706/CheesecakeUtilities

View on GitHub
firebase/functions/index.js

Summary

Maintainability
A
1 hr
Test Coverage
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');

// The Firebase Admin SDK to access the Firebase Realtime Database. 
const admin = require('firebase-admin');
admin.initializeApp();

// Vehicle Mileage Utility
// Calculate Statistics (non-training mileage only for now)
exports.calculateStatistics = functions.database.ref('/vehmileage/users/{userid}/records').onWrite(
    (snapshot, context) => {
        console.log("Function Version: 030620191912")
        console.log("Dep Versions listed below");
        console.log(process.versions)
        const records = snapshot.after.val();
        var stats = {totalMileage: 0}
        console.log('Processing User', context.params.userid);
        console.log('Calculating Total Mileage...');
        var total = calculateTotalMileage(records);
        stats.totalMileage = total;
        console.log('Total Mileage: ', total, ' km');
        console.log('Calculating Class Total Mileage...');
        // Only save those that has more than 0;
        var classM = calculateByClass(records);
        stats = Object.assign({}, stats, classM);
        console.log('Processing Mileage split by misc items (date, month, vehicle type)...');
        var misc = miscMileage(records);
        stats.timeRecords = misc.time;
        stats.vehicleTypes = misc.vehicles;
        stats.vehicleNumberRecords = misc.vehicleNumber;
        console.log('Finished Processing User. Saving to Firebase DB');
        console.log(stats);
        return setRecord(snapshot.after.ref.parent.child('statistics'), stats, context);
    }
)

function miscMileage(recordList) {
    var date = {};
    var month = {};
    var vehicle = {};
    var vehicleNum = {};
    Object.keys(recordList).forEach(key => {
        if (typeof recordList[key] === 'object') {
            if (recordList[key].trainingMileage == true) return;
            // Add Date Time mileage
            var timezoneOffset = 8 * 60 * 60 * 1000; // 8 Hours
            if (recordList[key].hasOwnProperty("timezone")) timezoneOffset = recordList[key].timezone; // Adjust date to timezone
            var rDate = new Date(parseInt(recordList[key].datetimeFrom, 10) + timezoneOffset); // Init with timezone difference
            rDate.setHours(0,0,0,0); // Add to date
            if (!date[rDate.getTime().toString()]) date[rDate.getTime().toString()] = 0.0;
            date[rDate.getTime().toString()] += parseFloat(recordList[key].totalMileage);
            rDate.setDate(1); // Add to month
            if (!month[rDate.getTime().toString()]) month[rDate.getTime().toString()] = 0.0;
            month[rDate.getTime().toString()] += parseFloat(recordList[key].totalMileage);

            // Add Vehicle filtered mileage
            if (!vehicle[recordList[key].vehicleId]) vehicle[recordList[key].vehicleId] = 0.0;
            if (!vehicleNum[recordList[key].vehicleNumber]) vehicleNum[recordList[key].vehicleNumber] = 0.0;
            vehicle[recordList[key].vehicleId] += parseFloat(recordList[key].totalMileage); // Vehicle Type
            vehicleNum[recordList[key].vehicleNumber] += parseFloat(recordList[key].totalMileage); // Vehicle Number
        }
    });
    var result = {time: {perDate: date, perMonth: month}, vehicles: vehicle, vehicleNumber: vehicleNum}
    return result;
}

function calculateTotalMileage(recordList) {
    var mileage = 0.0;
    Object.keys(recordList).forEach(key => {
        if (typeof recordList[key] === 'object') {
            if (recordList[key].trainingMileage == true) return;
            mileage += parseFloat(recordList[key].totalMileage);
        }
    });
    return mileage;
}

function calculateByClass(recordList) {
    var classMileage = {};
    Object.keys(recordList).forEach(key => {
        if (typeof recordList[key] === 'object') {
            if (recordList[key].trainingMileage == true) return;
            if (!classMileage[recordList[key].vehicleClass]) classMileage[recordList[key].vehicleClass] = 0.0;
            classMileage[recordList[key].vehicleClass] += parseFloat(recordList[key].totalMileage);
        }
    });
    return classMileage;
}

async function setRecord(ref, record, context) {
    const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
    appOptions.databaseAuthVariableOverride = context.auth;
    const app = admin.initializeApp(appOptions, 'app');

    const deleteApp = () => app.delete().catch(() => null); // Deleting the app is necessary for preventing concurrency leaks
    try {
        const res = await app.database().ref(ref).set(record);
        await deleteApp();
        return res;
    }
    catch (err) {
        await deleteApp();
        return await Promise.reject(err);
    }
}

// GPA Calculator Utility
// Calculate GPA/Score (Also need to make sure it does not activate twice lol)
exports.calculateGpa = functions.database.ref('/gpacalc/users/{userid}/{institutionid}').onWrite(
    async (snapshot, context) => {
        console.log("Async Function Version: 160120230058");
        console.log("Dep Versions listed below");
        console.log(process.versions);
        const checkToContinue = await admin.database().ref('/gpacalc/update/' + context.params.userid).once('value');
        if (checkToContinue.exists()) {
            console.log("Second execution of function, preventing further execution");
            await admin.database().ref('/gpacalc/update/' + context.params.userid).remove();
            return;
        }
        const records = snapshot.after.val();
        const dataSnapshot = await admin.database().ref('/gpacalc/scoring').once('value');
        var stats = records;
        var gradeTiers = dataSnapshot.val();
        console.log('Calculating User ', context.params.userid, ' Grade for Institution ', context.params.institutionid);
        if (typeof records === 'object') {
            console.log("Processing ", records.name, " (", records.shortName, ")");
            stats = processInstitution(records, gradeTiers[records.type]);
            await admin.database().ref('/gpacalc/update/' + context.params.userid).set(true); // Update state to prevent second execution from doing the calculation again
        }
        console.log('Finished Processing User. Saving to Firebase DB');
        console.log(stats);
        return setRecord(snapshot.after.ref, stats, context);
    }
)

function processInstitution(institution, gradeTier) {
    // Handle ordering
    var currentTime = Date.now();
    if (institution.startTimestamp == null) institution.startTimestamp = currentTime;
    if (institution.endTimestamp == null) institution.endTimestamp = -1;
    institution.order = (institution.endTimestamp == -1) ? institution.startTimestamp : institution.endTimestamp;

    // Process all the semesters first
    if (institution.semester == null) {
        institution.gpa = "Unknown"
        if (gradeTier.type == "gpa") institution.totalCredits = 0 // Total Credits is only gradable credits
        return institution; // Don't edit anything
    }
    var count = 0.0;
    var totalCredits = 0.0;
    Object.keys(institution.semester).forEach(key => {
        if (typeof institution.semester[key] === 'object') {
            console.log("Processing Semester: ", institution.semester[key].name)
            institution.semester[key] = processSemester(institution.semester[key], gradeTier);
            // Process count or semester
            if (institution.semester[key].gpa == "Unknown") return; // Do not try and add unknown lol
            if (gradeTier.type == "count") {
                // Add all the semesters up
                count += parseInt(institution.semester[key].gpa);
            } else {
                var sem_gpa = institution.semester[key].fullgpa * institution.semester[key].totalCredits;
                count += sem_gpa; 
                totalCredits += institution.semester[key].totalCredits;
            }
        }
    });
    console.log("Institution Count and Credits: ", count, " | ", totalCredits);
    if (gradeTier.type == "count") {
        institution.gpa = String(parseInt(count, 10));
    } else if (totalCredits != 0) {
        var gpa = count / totalCredits;
        institution.fullgpa = gpa;
        institution.totalCredits = parseInt(totalCredits);
        institution.gpa = toFixed(gpa, 4);
    } else {
        institution.gpa = "Unknown";
        institution.totalCredits = 0;
    }
    return institution;
}

function processSemester(semester, gradeTier) {
    // Handle ordering
    var currentTime = Date.now();
    if (semester.startTimestamp == null) semester.startTimestamp = currentTime;
    if (semester.endTimestamp == null) semester.endTimestamp = -1;
    semester.order = (semester.endTimestamp == -1) ? semester.startTimestamp : semester.endTimestamp;

    // Calculate the various modules
    if (semester.modules == null) {
        semester.gpa = "Unknown"
        if (gradeTier.type == "gpa") semester.totalCredits = 0 // Total Credits is only gradable credits
        return semester;
    }
    var count = 0.0;
    var totalCredits = 0.0;
    Object.keys(semester.modules).forEach(key => {
        if (typeof semester.modules[key] === 'object') {
            if (semester.modules[key].passFail == true) {
                console.log("Ignoring Pass/Fail Module [", semester.modules[key].courseCode, "] ", semester.modules[key].name);
                return;
            }
            if (semester.modules[key].gradeTier == -1) {
                console.log("Ignoring module [", semester.modules[key].courseCode, "] ", semester.modules[key].name, " without grades");
                return;
            }
            var grade = gradeTier.gradetier[semester.modules[key].gradeTier].value;
            var mod_gpa = 0;
            if (gradeTier.type == "count") {
                mod_gpa = grade; // Grade
            } else {
                mod_gpa = grade * semester.modules[key].credits; // GPA Calculation = (grade * credits) / total credits
            }
            console.log("Grade Received for [", semester.modules[key].courseCode, "] ", semester.modules[key].name, ": ", grade, " | Module Grade with Credits: ", mod_gpa);
            count += mod_gpa; 
            totalCredits += semester.modules[key].credits;
        }
    });
    console.log("Semester Count and Credits: ", count, " | ", totalCredits);
    if (gradeTier.type == "count") {
        // Is count so we return the count only
        semester.gpa = String(parseInt(count, 10));
    } else if (totalCredits != 0) {
        var gpa = count / totalCredits; 
        semester.fullgpa = gpa;
        semester.gpa = toFixed(gpa, 4);
        semester.totalCredits = parseInt(totalCredits);
    } else {
        semester.gpa = "Unknown";
        semester.totalCredits = 0;
    }
    return semester;
}

function toFixed(num, fixed) {
    var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?');
    return num.toString().match(re)[0];
}