From 0abafe05c8ab3a6304c3432f540e7567939bca31 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 29 Oct 2024 16:46:26 -0400 Subject: [PATCH 1/8] Add SQL and model definitions for Hubble class merge groups. --- .../models/hubble_class_merge_group.ts | 38 +++++++++++++++++++ .../sql/create_merge_groups_table.sql | 12 ++++++ 2 files changed, 50 insertions(+) create mode 100644 src/stories/hubbles_law/models/hubble_class_merge_group.ts create mode 100644 src/stories/hubbles_law/sql/create_merge_groups_table.sql diff --git a/src/stories/hubbles_law/models/hubble_class_merge_group.ts b/src/stories/hubbles_law/models/hubble_class_merge_group.ts new file mode 100644 index 0000000..a99bc83 --- /dev/null +++ b/src/stories/hubbles_law/models/hubble_class_merge_group.ts @@ -0,0 +1,38 @@ +import { Sequelize, DataTypes, Model, InferAttributes, InferCreationAttributes } from "sequelize"; +import { Class } from "../../../models"; + +export class HubbleClassMergeGroup extends Model, InferCreationAttributes> { + declare group_id: number; + declare class_id: number; + declare merge_order: number; +} + +export function initializeHubbleClassMergeGroupModel(sequelize: Sequelize) { + HubbleClassMergeGroup.init({ + group_id: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + autoIncrement: true, + primaryKey: true, + }, + class_id: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + references: { + model: Class, + key: "id", + } + }, + merge_order: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + } + }, { + sequelize, + indexes: [ + { + fields: ["class_id"], + }, + ] + }); +} diff --git a/src/stories/hubbles_law/sql/create_merge_groups_table.sql b/src/stories/hubbles_law/sql/create_merge_groups_table.sql new file mode 100644 index 0000000..42dac10 --- /dev/null +++ b/src/stories/hubbles_law/sql/create_merge_groups_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE HubbleClassMergeGroups( + group_id int(11) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + class_id int(11) UNSIGNED NOT NULL UNIQUE, + merge_order int(11) UNSIGNED NOT NULL, + + PRIMARY KEY(group_id), + INDEX(class_id), + FOREIGN KEY(class_id) + REFERENCES Classes(id) + ON UPDATE CASCADE + ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci PACK_KEYS=0; From 3f656a1c745e99cf04381c6464afdb37e5bc664a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 29 Oct 2024 16:56:46 -0400 Subject: [PATCH 2/8] Initialize class merge groups when setting up models. --- src/stories/hubbles_law/models/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stories/hubbles_law/models/index.ts b/src/stories/hubbles_law/models/index.ts index f6bd4a0..976c691 100644 --- a/src/stories/hubbles_law/models/index.ts +++ b/src/stories/hubbles_law/models/index.ts @@ -6,6 +6,7 @@ import { SyncMergedHubbleClasses, initializeSyncMergedHubbleClassesModel } from import { Sequelize } from "sequelize"; import { initializeHubbleStudentDataModel } from "./hubble_student_data"; import { initializeHubbleClassDataModel } from "./hubble_class_data"; +import { initializeHubbleClassMergeGroupModel } from "./hubble_class_merge_group"; export { Galaxy, @@ -23,4 +24,5 @@ export function initializeModels(db: Sequelize) { initializeSyncMergedHubbleClassesModel(db); initializeHubbleStudentDataModel(db); initializeHubbleClassDataModel(db); + initializeHubbleClassMergeGroupModel(db); } From fdf2b8f8aaa83c958831c564c39d34303b7b722f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 29 Oct 2024 17:34:31 -0400 Subject: [PATCH 3/8] Update class measurement functions to use new merge groups. --- src/stories/hubbles_law/database.ts | 76 +++++++++++++---------------- src/stories/hubbles_law/router.ts | 22 ++++++--- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/stories/hubbles_law/database.ts b/src/stories/hubbles_law/database.ts index f149d2a..e655d74 100644 --- a/src/stories/hubbles_law/database.ts +++ b/src/stories/hubbles_law/database.ts @@ -7,6 +7,7 @@ import { HubbleStudentData } from "./models/hubble_student_data"; import { HubbleClassData } from "./models/hubble_class_data"; import { IgnoreStudent } from "../../models/ignore_student"; import { logger } from "../../logger"; +import { HubbleClassMergeGroup } from "./models/hubble_class_merge_group"; const galaxyAttributes = ["id", "ra", "decl", "z", "type", "name", "element"]; @@ -207,7 +208,9 @@ export async function getStudentHubbleMeasurements(studentID: number): Promise { +async function getHubbleMeasurementsForStudentClass(studentID: number, classID: number, excludeWithNull: boolean = false): Promise { + + const classIDs = await getMergedIDsForClass(classID); const studentWhereConditions: WhereOptions = []; const classDataStudentIDs = await getClassDataIDsForStudent(studentID); @@ -317,6 +320,28 @@ async function getClassIDsForSyncClass(classID: number): Promise { return classIDs; } +async function getMergedIDsForClass(classID: number): Promise { + // TODO: Currently this uses two queries: + // The first to get the merge group (if there is one) + // Then a second to get all of the classes in the merge group + // Maybe we can just write some SQL to make these one query? + const mergeGroup = await HubbleClassMergeGroup.findOne({ + where: { + class_id: classID + } + }); + if (mergeGroup === null) { + return [classID]; + } + + const mergeEntries = await HubbleClassMergeGroup.findAll({ + where: { + group_id: mergeGroup.group_id, + } + }); + return mergeEntries.map(entry => entry.class_id); +} + export async function getClassDataIDsForStudent(studentID: number): Promise { const state = (await StoryState.findOne({ where: { student_id: studentID }, @@ -329,69 +354,38 @@ export async function getClassDataIDsForStudent(studentID: number): Promise { - const classIDs = await getClassIDsForSyncClass(classID); - return getHubbleMeasurementsForStudentClasses(studentID, classIDs, excludeWithNull); -} - -async function getHubbleMeasurementsForAsyncStudent(studentID: number, classID: number | null, excludeWithNull: boolean = false): Promise { - const classIDs = await getClassIDsForAsyncStudent(studentID, classID); - return getHubbleMeasurementsForStudentClasses(studentID, classIDs, excludeWithNull); -} - export async function getClassMeasurements(studentID: number, - classID: number | null, + classID: number, lastChecked: number | null = null, excludeWithNull: boolean = false, ): Promise { - const cls = classID !== null ? await findClassById(classID) : null; - const asyncClass = cls?.asynchronous ?? true; - let data: HubbleMeasurement[] | null; - if (classID === null || asyncClass) { - data = await getHubbleMeasurementsForAsyncStudent(studentID, classID, excludeWithNull); - } else { - data = await getHubbleMeasurementsForSyncStudent(studentID, classID, excludeWithNull); - } - if (data != null && lastChecked != null) { + let data = await getHubbleMeasurementsForStudentClass(studentID, classID, excludeWithNull); + if (data.length > 0 && lastChecked != null) { const lastModified = Math.max(...data.map(meas => meas.last_modified.getTime())); if (lastModified <= lastChecked) { - data = null; + data = []; } } - return data ?? []; + return data; } // The advantage of this over the function above is that it saves bandwidth, // since we aren't sending the data itself. // This is intended to be used with cases where we need to frequently check the number of measurements export async function getClassMeasurementCount(studentID: number, - classID: number | null, + classID: number, excludeWithNull: boolean = false, ): Promise { - const cls = classID !== null ? await findClassById(classID) : null; - const asyncClass = cls?.asynchronous ?? true; - let data: HubbleMeasurement[] | null; - if (classID === null || asyncClass) { - data = await getHubbleMeasurementsForAsyncStudent(studentID, classID, excludeWithNull); - } else { - data = await getHubbleMeasurementsForSyncStudent(studentID, classID, excludeWithNull); - } + const data = await getClassMeasurements(studentID, classID, null, excludeWithNull); return data?.length ?? 0; } // Similar to the function above, this is intended for cases where we need to frequently check // how many students have completed their measurements, such as the beginning of Stage 4 in the Hubble story export async function getStudentsWithCompleteMeasurementsCount(studentID: number, - classID: number | null, + classID: number, ): Promise { - const cls = classID !== null ? await findClassById(classID) : null; - const asyncClass = cls?.asynchronous ?? true; - let data: HubbleMeasurement[] | null; - if (classID === null || asyncClass) { - data = await getHubbleMeasurementsForAsyncStudent(studentID, classID, true); - } else { - data = await getHubbleMeasurementsForSyncStudent(studentID, classID, true); - } + const data = await getHubbleMeasurementsForStudentClass(studentID, classID, true); const counts: Record = {}; data?.forEach(measurement => { if (measurement.student_id in counts) { diff --git a/src/stories/hubbles_law/router.ts b/src/stories/hubbles_law/router.ts index 108b20c..e721ac0 100644 --- a/src/stories/hubbles_law/router.ts +++ b/src/stories/hubbles_law/router.ts @@ -44,7 +44,7 @@ import { import { Express, Router } from "express"; import { Sequelize } from "sequelize"; -import { findClassById, findStudentById } from "../../database"; +import { classForStudentStory, findClassById, findStudentById } from "../../database"; import { SyncMergedHubbleClasses, initializeModels } from "./models"; import { setUpHubbleAssociations } from "./associations"; @@ -345,8 +345,10 @@ router.get(["/class-measurements/:studentID/:classID", "/stage-3-data/:studentID classID = 159; } - const invalidStudent = (await findStudentById(studentID)) === null; - const invalidClass = (await findClassById(classID)) === null; + const student = await findStudentById(studentID); + const invalidStudent = student === null; + const cls = await findClassById(classID); + const invalidClass = cls === null; if (invalidStudent || invalidClass) { const invalidItems = []; if (invalidStudent) { invalidItems.push("student"); } @@ -358,7 +360,7 @@ router.get(["/class-measurements/:studentID/:classID", "/stage-3-data/:studentID return; } - const measurements = await getClassMeasurements(studentID, classID, lastChecked, completeOnly); + const measurements = await getClassMeasurements(student.id, cls.id, lastChecked, completeOnly); res.status(200).json({ student_id: studentID, class_id: classID, @@ -372,12 +374,20 @@ router.get(["/class-measurements/:studentID", "stage-3-measurements/:studentID"] const isValidStudent = (await findStudentById(studentID)) !== null; if (!isValidStudent) { res.status(404).json({ - message: "Invalid student ID" + message: "Invalid student ID", + }); + return; + } + + const cls = await classForStudentStory(studentID, "hubbles_law"); + if (cls === null) { + res.status(404).json({ + message: `Student ${studentID} is not in a class signed up for the Hubble's Law story`, }); return; } - const measurements = await getClassMeasurements(studentID, null); + const measurements = await getClassMeasurements(studentID, cls.id); res.status(200).json({ student_id: studentID, class_id: null, From 0e0aa1618560270bec7f249b03fb4b533e48ced5 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 29 Oct 2024 17:39:55 -0400 Subject: [PATCH 4/8] Update merge groups table definition. --- src/stories/hubbles_law/models/hubble_class_merge_group.ts | 6 +++++- src/stories/hubbles_law/sql/create_merge_groups_table.sql | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/stories/hubbles_law/models/hubble_class_merge_group.ts b/src/stories/hubbles_law/models/hubble_class_merge_group.ts index a99bc83..b2c65fa 100644 --- a/src/stories/hubbles_law/models/hubble_class_merge_group.ts +++ b/src/stories/hubbles_law/models/hubble_class_merge_group.ts @@ -12,12 +12,13 @@ export function initializeHubbleClassMergeGroupModel(sequelize: Sequelize) { group_id: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, - autoIncrement: true, primaryKey: true, }, class_id: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, + unique: true, + primaryKey: true, references: { model: Class, key: "id", @@ -30,6 +31,9 @@ export function initializeHubbleClassMergeGroupModel(sequelize: Sequelize) { }, { sequelize, indexes: [ + { + fields: ["group_id"], + }, { fields: ["class_id"], }, diff --git a/src/stories/hubbles_law/sql/create_merge_groups_table.sql b/src/stories/hubbles_law/sql/create_merge_groups_table.sql index 42dac10..f5b3256 100644 --- a/src/stories/hubbles_law/sql/create_merge_groups_table.sql +++ b/src/stories/hubbles_law/sql/create_merge_groups_table.sql @@ -1,9 +1,10 @@ CREATE TABLE HubbleClassMergeGroups( - group_id int(11) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + group_id int(11) UNSIGNED NOT NULL, class_id int(11) UNSIGNED NOT NULL UNIQUE, merge_order int(11) UNSIGNED NOT NULL, - PRIMARY KEY(group_id), + PRIMARY KEY(group_id, class_id), + INDEX(group_id), INDEX(class_id), FOREIGN KEY(class_id) REFERENCES Classes(id) From e48ade6f2ee3b39311414c7b4edba3b326ca838a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 29 Oct 2024 18:17:05 -0400 Subject: [PATCH 5/8] Consider merge order when determining class IDs. --- src/stories/hubbles_law/database.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stories/hubbles_law/database.ts b/src/stories/hubbles_law/database.ts index e655d74..45fb9f0 100644 --- a/src/stories/hubbles_law/database.ts +++ b/src/stories/hubbles_law/database.ts @@ -337,6 +337,9 @@ async function getMergedIDsForClass(classID: number): Promise { const mergeEntries = await HubbleClassMergeGroup.findAll({ where: { group_id: mergeGroup.group_id, + merge_order: { + [Op.lte] : mergeGroup.merge_order, + } } }); return mergeEntries.map(entry => entry.class_id); From f98143734bff9977c04af9f262158bd45cab850b Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 4 Nov 2024 11:39:16 -0500 Subject: [PATCH 6/8] Add function for finding next candidate merge class. --- src/stories/hubbles_law/database.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/stories/hubbles_law/database.ts b/src/stories/hubbles_law/database.ts index 45fb9f0..fe9f8ee 100644 --- a/src/stories/hubbles_law/database.ts +++ b/src/stories/hubbles_law/database.ts @@ -781,6 +781,35 @@ export async function eligibleClassesForMerge(database: Sequelize, classID: numb `, { type: QueryTypes.SELECT }) as Promise; } +export async function findClassForMerge(database: Sequelize): Promise { + // The SQL is complicated enough here; doing this with the ORM + // will probably be unreadable + const result = await database.query( + ` + SELECT + id, + COUNT(*) as group_count, + IFNULL(group_id, UUID()) AS unique_gid, + IFNULL(group_id, 0) AS is_group + FROM + Classes + LEFT OUTER JOIN + HubbleClassMergeGroups ON Classes.id = HubbleClassMergeGroups.class_id + INNER JOIN + ( + SELECT * FROM StudentsClasses GROUP BY class_id + HAVING COUNT(student_id) >= 12 + ) C + ON Classes.id = C.class_id + GROUP BY unique_gid + ORDER BY is_group ASC, group_count ASC + LIMIT 1; + `, + { type: QueryTypes.SELECT } + ) as Class[]; + return result[0]; +} + // Try and merge the class with the given ID with another class such that the total size is above the threshold // We say "try" because if a client doesn't know that the merge has already occurred, we may get // multiple such requests from different student clients. From eec2bb453bab87fec21a754813f97407fdcb9d2c Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 4 Nov 2024 12:20:37 -0500 Subject: [PATCH 7/8] Add function for adding a class to a merge group - either create a new one, or add it to an existing one if no other ungrouped classes are available. --- src/stories/hubbles_law/database.ts | 65 +++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/stories/hubbles_law/database.ts b/src/stories/hubbles_law/database.ts index fe9f8ee..86fc962 100644 --- a/src/stories/hubbles_law/database.ts +++ b/src/stories/hubbles_law/database.ts @@ -781,7 +781,12 @@ export async function eligibleClassesForMerge(database: Sequelize, classID: numb `, { type: QueryTypes.SELECT }) as Promise; } -export async function findClassForMerge(database: Sequelize): Promise { +type GroupData = { + unique_gid: string; + is_group: boolean; + merged_count: number; +}; +export async function findClassForMerge(database: Sequelize): Promise { // The SQL is complicated enough here; doing this with the ORM // will probably be unreadable const result = await database.query( @@ -790,26 +795,62 @@ export async function findClassForMerge(database: Sequelize): Promise { id, COUNT(*) as group_count, IFNULL(group_id, UUID()) AS unique_gid, - IFNULL(group_id, 0) AS is_group + (group_id IS NOT NULL) AS is_group, + MAX(merge_order) as merged_count FROM Classes LEFT OUTER JOIN - HubbleClassMergeGroups ON Classes.id = HubbleClassMergeGroups.class_id - INNER JOIN - ( - SELECT * FROM StudentsClasses GROUP BY class_id - HAVING COUNT(student_id) >= 12 - ) C - ON Classes.id = C.class_id + (SELECT * FROM HubbleClassMergeGroups ORDER BY merge_order DESC) G ON Classes.id = G.class_id + INNER JOIN + ( + SELECT * FROM StudentsClasses GROUP BY class_id + HAVING COUNT(student_id) >= 12 + ) C + ON Classes.id = C.class_id GROUP BY unique_gid - ORDER BY is_group ASC, group_count ASC + ORDER BY is_group ASC, group_count ASC, merged_count DESC LIMIT 1; - `, + `, { type: QueryTypes.SELECT } - ) as Class[]; + ) as (Class & GroupData)[]; return result[0]; } +async function nextMergeGroupID(): Promise { + const max = (await HubbleClassMergeGroup.findAll({ + attributes: [ + [Sequelize.fn("MAX", Sequelize.col("group_id")), "group_id"] + ] + })) as (HubbleClassMergeGroup & { group_id: number })[]; + return max[0].group_id as number; +} + +export async function addClassToMergeGroup(classID: number): Promise { + + // Sanity check + const existingGroup = await HubbleClassMergeGroup.findOne({ where: { class_id: classID } }); + if (existingGroup !== null) { + return existingGroup.group_id; + } + + const database = Class.sequelize; + if (database === undefined) { + return null; + } + + const clsToMerge = await findClassForMerge(database); + let mergeGroup; + if (clsToMerge.is_group) { + mergeGroup = await HubbleClassMergeGroup.create({ class_id: classID, group_id: Number(clsToMerge.unique_gid), merge_order: clsToMerge.merged_count + 1 }); + } else { + const newGroupID = await nextMergeGroupID(); + mergeGroup = await HubbleClassMergeGroup.create({ class_id: classID, group_id: newGroupID, merge_order: 1 }); + } + + return mergeGroup.group_id; + +} + // Try and merge the class with the given ID with another class such that the total size is above the threshold // We say "try" because if a client doesn't know that the merge has already occurred, we may get // multiple such requests from different student clients. From c2c7e2a913b74f88f7da3dcfb1837754fbbcbde7 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 4 Nov 2024 14:04:12 -0500 Subject: [PATCH 8/8] Add endpoints for getting merged classes and adding a class to a merge group. --- src/stories/hubbles_law/database.ts | 70 +++------------------- src/stories/hubbles_law/router.ts | 91 ++++++++++++++++++----------- 2 files changed, 65 insertions(+), 96 deletions(-) diff --git a/src/stories/hubbles_law/database.ts b/src/stories/hubbles_law/database.ts index 86fc962..8051e6d 100644 --- a/src/stories/hubbles_law/database.ts +++ b/src/stories/hubbles_law/database.ts @@ -320,7 +320,7 @@ async function getClassIDsForSyncClass(classID: number): Promise { return classIDs; } -async function getMergedIDsForClass(classID: number): Promise { +export async function getMergedIDsForClass(classID: number): Promise { // TODO: Currently this uses two queries: // The first to get the merge group (if there is one) // Then a second to get all of the classes in the merge group @@ -758,35 +758,12 @@ export async function getMergeDataForClass(classID: number): Promise { - const size = await classSize(classID); - - // Running into the limits of the ORM a bit here - // Maybe there's a clever way to write this? - // But straight SQL gets the job done - return database.query( - `SELECT * FROM (SELECT - id, - test, - (SELECT - COUNT(*) - FROM - StudentsClasses - WHERE - StudentsClasses.class_id = id) AS size - FROM - Classes) q - WHERE - (size >= ${sizeThreshold - size} AND test = 0) - `, { type: QueryTypes.SELECT }) as Promise; -} - type GroupData = { unique_gid: string; is_group: boolean; merged_count: number; }; -export async function findClassForMerge(database: Sequelize): Promise { +export async function findClassForMerge(database: Sequelize, classID: number): Promise { // The SQL is complicated enough here; doing this with the ORM // will probably be unreadable const result = await database.query( @@ -807,6 +784,7 @@ export async function findClassForMerge(database: Sequelize): Promise= 12 ) C ON Classes.id = C.class_id + WHERE id != ${classID} GROUP BY unique_gid ORDER BY is_group ASC, group_count ASC, merged_count DESC LIMIT 1; @@ -822,7 +800,7 @@ async function nextMergeGroupID(): Promise { [Sequelize.fn("MAX", Sequelize.col("group_id")), "group_id"] ] })) as (HubbleClassMergeGroup & { group_id: number })[]; - return max[0].group_id as number; + return (max[0].group_id + 1) as number; } export async function addClassToMergeGroup(classID: number): Promise { @@ -838,50 +816,16 @@ export async function addClassToMergeGroup(classID: number): Promise { - const cls = await findClassById(classID); - if (cls === null) { - return { mergeData: null, message: "Invalid class ID!" }; - } - - let mergeData = await getMergeDataForClass(classID); - if (mergeData !== null) { - return { mergeData, message: "Class already merged" }; - } - - const eligibleClasses = await eligibleClassesForMerge(db, classID); - if (eligibleClasses.length > 0) { - const index = Math.floor(Math.random() * eligibleClasses.length); - const classToMerge = eligibleClasses[index]; - mergeData = await SyncMergedHubbleClasses.create({ class_id: classID, merged_class_id: classToMerge.id }); - if (mergeData === null) { - return { mergeData, message: "Error creating merge!" }; - } - return { mergeData, message: "New merge created" }; - } - - return { mergeData: null, message: "No eligible classes to merge" }; - -} diff --git a/src/stories/hubbles_law/router.ts b/src/stories/hubbles_law/router.ts index e721ac0..1443a62 100644 --- a/src/stories/hubbles_law/router.ts +++ b/src/stories/hubbles_law/router.ts @@ -1,3 +1,6 @@ +import * as S from "@effect/schema/Schema"; +import * as Either from "effect/Either"; + import { Galaxy } from "./models/galaxy"; import { @@ -32,9 +35,10 @@ import { getGalaxyById, removeSampleHubbleMeasurement, getAllNthSampleHubbleMeasurements, - tryToMergeClass, getClassMeasurementCount, - getStudentsWithCompleteMeasurementsCount + getStudentsWithCompleteMeasurementsCount, + getMergedIDsForClass, + addClassToMergeGroup } from "./database"; import { @@ -45,7 +49,7 @@ import { import { Express, Router } from "express"; import { Sequelize } from "sequelize"; import { classForStudentStory, findClassById, findStudentById } from "../../database"; -import { SyncMergedHubbleClasses, initializeModels } from "./models"; +import { initializeModels } from "./models"; import { setUpHubbleAssociations } from "./associations"; export const router = Router(); @@ -395,51 +399,72 @@ router.get(["/class-measurements/:studentID", "stage-3-measurements/:studentID"] }); }); -router.get("/all-data", async (req, res) => { - const minimal = (req.query?.minimal as string)?.toLowerCase() === "true"; - const beforeMs: number = parseInt(req.query.before as string); - const before = isNaN(beforeMs) ? null : new Date(beforeMs); - const [measurements, studentData, classData] = - await Promise.all([ - getAllHubbleMeasurements(before, minimal), - getAllHubbleStudentData(before, minimal), - getAllHubbleClassData(before, minimal) - ]); +router.get("/merged-classes/:classID", async (req, res) => { + const classID = Number(req.params.classID); + const cls = await findClassById(classID); + if (cls === null) { + res.status(404).json({ + message: `No class found with ID ${classID}`, + }); + return; + } + const classIDs = await getMergedIDsForClass(classID); res.json({ - measurements, - studentData, - classData + merged_class_ids: classIDs, }); }); -router.put("/sync-merged-class/:classID", async(req, res) => { - const classID = parseInt(req.params.classID); - if (isNaN(classID)) { - res.statusCode = 400; - res.json({ - error: "Class ID must be a number" +const MergeClassInfo = S.struct({ + class_id: S.number.pipe(S.int()), +}); +router.put("/merge-class", async (req, res) => { + const body = req.body; + const maybe = S.decodeUnknownEither(MergeClassInfo)(body); + + if (Either.isLeft(maybe)) { + res.status(400).json({ + message: `Expected class ID to be an integer, got ${body.class_id}`, }); return; } - const database = SyncMergedHubbleClasses.sequelize; - if (database === undefined) { - res.status(500).json({ - error: "Error connecting to database", + + const data = maybe.right; + const cls = await findClassById(data.class_id); + if (cls === null) { + res.status(404).json({ + message: `No class found with ID ${data.class_id}`, }); return; } - const data = await tryToMergeClass(database, classID); - if (data.mergeData === null) { - res.statusCode = 404; - res.json({ - error: data.message + + const groupID = await addClassToMergeGroup(data.class_id); + if (groupID === null) { + res.status(500).json({ + message: `There was an error while adding class ${data.class_id} to a merge group`, }); return; } res.json({ - merge_info: data.mergeData, - message: data.message + class_id: data.class_id, + group_id: groupID, + }); +}); + +router.get("/all-data", async (req, res) => { + const minimal = (req.query?.minimal as string)?.toLowerCase() === "true"; + const beforeMs: number = parseInt(req.query.before as string); + const before = isNaN(beforeMs) ? null : new Date(beforeMs); + const [measurements, studentData, classData] = + await Promise.all([ + getAllHubbleMeasurements(before, minimal), + getAllHubbleStudentData(before, minimal), + getAllHubbleClassData(before, minimal) + ]); + res.json({ + measurements, + studentData, + classData }); });