Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,21 @@ export const judgingFinalDeliberationResolver: GraphQLFieldResolver<
}

return {
divisionId: deliberation.divisionId,
divisionId: deliberation.division_id,
stage: deliberation.stage,
status: deliberation.status,
startTime: deliberation.startTime ? new Date(deliberation.startTime).toISOString() : null,
completionTime: deliberation.completionTime
? new Date(deliberation.completionTime).toISOString()
startTime: deliberation.start_time ? new Date(deliberation.start_time).toISOString() : null,
completionTime: deliberation.completion_time
? new Date(deliberation.completion_time).toISOString()
: null,
champions: JSON.stringify(deliberation.awards.champions || {}),
innovationProject: deliberation.awards['innovation-project'] || [],
robotDesign: deliberation.awards['robot-design'] || [],
coreValues: deliberation.awards['core-values'] || [],
robotPerformance: deliberation.awards['robot-performance'] || [],
optionalAwards: JSON.stringify(deliberation.awards.optionalAwards || {}),
coreAwardsManualEligibility: deliberation.stageData?.['core-awards']?.manualEligibility || [],
coreAwardsManualEligibility: deliberation.stage_data?.['core-awards']?.manualEligibility || [],
optionalAwardsManualEligibility:
deliberation.stageData?.['optional-awards']?.manualEligibility || []
deliberation.stage_data?.['optional-awards']?.manualEligibility || []
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver<
const updated = await db.finalDeliberations.byDivision(divisionId).update({
stage: nextStage,
status: 'not-started',
stageData: {
...deliberation.stageData,
stage_data: {
...deliberation.stage_data,
[nextStage]: {}
}
});
Expand All @@ -134,7 +134,7 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver<
pubSub.publish(divisionId, RedisEventTypes.FINAL_DELIBERATION_UPDATED, {
divisionId,
stage: updated.stage,
stageData: updated.stageData
stageData: updated.stage_data
}),
pubSub.publish(divisionId, RedisEventTypes.FINAL_DELIBERATION_UPDATED, {
divisionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const completeFinalDeliberationResolver: GraphQLFieldResolver<
const now = new Date();
const updated = await db.finalDeliberations.byDivision(divisionId).update({
status: 'completed',
completionTime: now
completion_time: now
});

if (!updated) {
Expand All @@ -88,7 +88,7 @@ export const completeFinalDeliberationResolver: GraphQLFieldResolver<
pubSub.publish(divisionId, RedisEventTypes.FINAL_DELIBERATION_UPDATED, {
divisionId,
status: updated.status,
completionTime: updated.completionTime
completionTime: updated.completion_time
}),
pubSub.publish(divisionId, RedisEventTypes.FINAL_DELIBERATION_STATUS_CHANGED, {
divisionId,
Expand All @@ -99,7 +99,7 @@ export const completeFinalDeliberationResolver: GraphQLFieldResolver<

return {
status: updated.status,
completionTime: updated.completionTime?.toISOString() || now.toISOString()
completionTime: updated.completion_time?.toISOString() || now.toISOString()
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const startFinalDeliberationResolver: GraphQLFieldResolver<
const now = new Date();
const updated = await db.finalDeliberations.byDivision(divisionId).update({
status: 'in-progress',
startTime: now
start_time: now
});

if (!updated) {
Expand All @@ -83,7 +83,7 @@ export const startFinalDeliberationResolver: GraphQLFieldResolver<
divisionId,
status: updated.status,
stage: updated.stage,
startTime: updated.startTime
startTime: updated.start_time
}),
pubSub.publish(divisionId, RedisEventTypes.FINAL_DELIBERATION_STATUS_CHANGED, {
divisionId,
Expand All @@ -95,6 +95,6 @@ export const startFinalDeliberationResolver: GraphQLFieldResolver<
return {
status: updated.status,
stage: updated.stage,
startTime: updated.startTime?.toISOString() || now.toISOString()
startTime: updated.start_time?.toISOString() || now.toISOString()
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,16 @@ export const updateManualEligibilityResolver: GraphQLFieldResolver<

// Update stage data
const updatedStageData = {
...deliberation.stageData,
...deliberation.stage_data,
[stage]: {
...deliberation.stageData[stage],
...deliberation.stage_data[stage],
manualEligibility: teamIds
}
};

// Update the deliberation
const updated = await db.finalDeliberations.byDivision(divisionId).update({
stageData: updatedStageData
stage_data: updatedStageData
});

if (!updated) {
Expand All @@ -92,7 +92,7 @@ export const updateManualEligibilityResolver: GraphQLFieldResolver<
const pubSub = getRedisPubSub();
await pubSub.publish(divisionId, RedisEventTypes.FINAL_DELIBERATION_UPDATED, {
divisionId,
stageData: updated.stageData
stageData: updated.stage_data
});

return {
Expand Down
2 changes: 1 addition & 1 deletion libs/database/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export class Database {
this.rooms = new RoomsRepository(this.kysely);
this.judgingSessions = new JudgingSessionsRepository(this.kysely);
this.judgingDeliberations = new JudgingDeliberationsRepository(this.kysely);
this.finalDeliberations = new FinalDeliberationsRepository(this.mongoDb);
this.finalDeliberations = new FinalDeliberationsRepository(this.kysely);
this.rubrics = new RubricsRepository(this.kysely, this.mongoDb);

this.tables = new TablesRepository(this.kysely);
Expand Down
107 changes: 107 additions & 0 deletions libs/database/src/migrations/032_migrate_final_deliberations_to_sql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Kysely, sql } from 'kysely';
import { MongoClient } from 'mongodb';

const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017';
const DB_NAME = process.env.DB_NAME || 'lems-local';

const MONGO_COLLECTION = 'final_deliberations';

/**
* Shape of documents that used to live in the `final_deliberations` MongoDB collection.
*/
interface LegacyFinalDeliberation {
divisionId: string;
stage: string;
status: string;
startTime: Date | null;
completionTime: Date | null;
awards: Record<string, unknown>;
stageData: Record<string, unknown>;
}

export async function up(db: Kysely<any>): Promise<void> {
// Create the final_deliberations table
await db.schema
.createTable('final_deliberations')
.addColumn('pk', 'serial', col => col.primaryKey())
.addColumn('id', 'uuid', col =>
col
.notNull()
.unique()
.defaultTo(sql`gen_random_uuid()`)
)
.addColumn('division_id', 'uuid', col => col.notNull())
.addColumn('stage', 'text', col => col.notNull().defaultTo('champions'))
.addColumn('status', 'text', col => col.notNull().defaultTo('not-started'))
.addColumn('start_time', 'timestamptz')
.addColumn('completion_time', 'timestamptz')
.addColumn('awards', 'jsonb', col => col.notNull().defaultTo(sql`'{}'::jsonb`))
.addColumn('stage_data', 'jsonb', col => col.notNull().defaultTo(sql`'{}'::jsonb`))
.execute();

// Create foreign key constraint for division_id
await db.schema
.alterTable('final_deliberations')
.addForeignKeyConstraint('fk_final_deliberations_division_id', ['division_id'], 'divisions', [
'id'
])
.onDelete('cascade')
.execute();

// Create unique constraint for division_id (one final deliberation per division)
await db.schema
.alterTable('final_deliberations')
.addUniqueConstraint('uq_final_deliberations_division_id', ['division_id'])
.execute();

// Create index
await db.schema
.createIndex('idx_final_deliberations_division_id')
.on('final_deliberations')
.column('division_id')
.execute();

// Migrate existing data from MongoDB's final_deliberations collection, if any is present
const mongoClient = new MongoClient(MONGODB_URI, { tlsAllowInvalidCertificates: true });

try {
await mongoClient.connect();
const mongoDb = mongoClient.db(DB_NAME);
const collections = await mongoDb.listCollections({ name: MONGO_COLLECTION }).toArray();

if (collections.length > 0) {
const deliberations = await mongoDb
.collection<LegacyFinalDeliberation>(MONGO_COLLECTION)
.find({})
.toArray();

for (const deliberation of deliberations) {
await db
.insertInto('final_deliberations')
.values({
division_id: deliberation.divisionId,
stage: deliberation.stage,
status: deliberation.status,
start_time: deliberation.startTime,
completion_time: deliberation.completionTime,
awards: JSON.stringify(deliberation.awards || {}),
stage_data: JSON.stringify(deliberation.stageData || {})
})
.execute();
}

// The MongoDB collection is no longer needed now that its data lives in SQL
await mongoDb.collection(MONGO_COLLECTION).drop();
}
} finally {
await mongoClient.close();
}
}

export async function down(db: Kysely<any>): Promise<void> {
// Note: this only reverts the schema change. The MongoDB collection dropped in `up`
// cannot be restored by this migration.
await db.schema.dropIndex('idx_final_deliberations_division_id').ifExists().execute();
await db.schema.dropTable('final_deliberations').ifExists().execute();
}
86 changes: 39 additions & 47 deletions libs/database/src/repositories/final-deliberations.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,60 @@
import { Db } from 'mongodb';
import { FinalDeliberation } from '../schema/documents/final-deliberation';

const COLLECTION_NAME = 'final_deliberations';
import { Kysely } from 'kysely';
import { KyselyDatabaseSchema } from '../schema/kysely';
import { FinalDeliberation, FinalDeliberationUpdate } from '../schema/tables/final-deliberation';

export class FinalDeliberationSelector {
constructor(
private mongoDb: Db,
private db: Kysely<KyselyDatabaseSchema>,
private divisionId: string
) {}

async get(): Promise<FinalDeliberation | null> {
const deliberation = await this.mongoDb
.collection<FinalDeliberation>(COLLECTION_NAME)
.findOne({ divisionId: this.divisionId });
return deliberation;
private getDeliberationQuery() {
return this.db
.selectFrom('final_deliberations')
.selectAll()
.where('division_id', '=', this.divisionId);
}

async update(
updates: Omit<Partial<FinalDeliberation>, 'divisionId'>
): Promise<FinalDeliberation | null> {
const result = await this.mongoDb
.collection<FinalDeliberation>(COLLECTION_NAME)
.findOneAndUpdate(
{ divisionId: this.divisionId },
{ $set: updates },
{ returnDocument: 'after' }
);
async get(): Promise<FinalDeliberation | null> {
const deliberation = await this.getDeliberationQuery().executeTakeFirst();
return deliberation || null;
}

return result;
async update(updates: FinalDeliberationUpdate): Promise<FinalDeliberation | undefined> {
return this.db
.updateTable('final_deliberations')
.set(updates)
.where('division_id', '=', this.divisionId)
.returningAll()
.executeTakeFirst();
}

async delete(): Promise<void> {
await this.mongoDb
.collection<FinalDeliberation>(COLLECTION_NAME)
.deleteOne({ divisionId: this.divisionId });
await this.db
.deleteFrom('final_deliberations')
.where('division_id', '=', this.divisionId)
.execute();
}
}

export class FinalDeliberationsRepository {
constructor(private mongoDb: Db) {}

async create(divisionId: string): Promise<FinalDeliberation> {
const document: FinalDeliberation = {
divisionId,
stage: 'champions',
status: 'not-started',
startTime: null,
completionTime: null,
awards: {},
stageData: {}
};
constructor(private db: Kysely<KyselyDatabaseSchema>) {}

const result = await this.mongoDb
.collection<FinalDeliberation>(COLLECTION_NAME)
.insertOne(document);

if (!result.insertedId) {
throw new Error(`Failed to create final deliberation for division ${divisionId}`);
}

return document;
byDivision(divisionId: string): FinalDeliberationSelector {
return new FinalDeliberationSelector(this.db, divisionId);
}

byDivision(divisionId: string): FinalDeliberationSelector {
return new FinalDeliberationSelector(this.mongoDb, divisionId);
async create(divisionId: string): Promise<FinalDeliberation> {
return this.db
.insertInto('final_deliberations')
.values({
division_id: divisionId,
stage: 'champions',
status: 'not-started',
awards: {},
stage_data: {}
})
.returningAll()
.executeTakeFirstOrThrow();
}
}
2 changes: 1 addition & 1 deletion libs/database/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export * from './tables/team-division-notifications';
// States
export * from './documents/rubric';
export * from './documents/scoresheet';
export * from './documents/final-deliberation';

export * from './tables/judging-deliberation';
export * from './tables/final-deliberation';
export * from './tables/awards';
2 changes: 2 additions & 0 deletions libs/database/src/schema/kysely.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { RobotGameMatchParticipantsTable } from './tables/robot-game-match-parti
import { AwardsTable } from './tables/awards';
import { AgendaEventsTable } from './tables/agenda-events';
import { JudgingDeliberationsTable } from './tables/judging-deliberation';
import { FinalDeliberationsTable } from './tables/final-deliberation';

export interface KyselyDatabaseSchema {
admins: AdminsTable;
Expand All @@ -42,4 +43,5 @@ export interface KyselyDatabaseSchema {
awards: AwardsTable;
agenda_events: AgendaEventsTable;
judging_deliberations: JudgingDeliberationsTable;
final_deliberations: FinalDeliberationsTable;
}
Loading