refactor: remove unused classroom fields

This commit is contained in:
2026-07-13 09:34:15 +08:00
parent a266d450e1
commit b9b295c997
14 changed files with 374 additions and 53 deletions

View File

@@ -14,6 +14,26 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
await this.backfillOrganizations();
await this.normalizeClassDates();
await this.protectAttendanceHistory();
await this.removeUnusedClassroomColumns();
}
private async removeUnusedClassroomColumns(): Promise<void> {
const runner = this.dataSource.createQueryRunner();
await runner.connect();
try {
const tables = await runner.getTables(['classrooms']);
if (tables.length === 0) return;
const table = await runner.getTable('classrooms');
const columnNames = new Set(table?.columns.map((column) => column.name) ?? []);
for (const columnName of ['course_type', 'supervisor']) {
if (columnNames.has(columnName)) {
await runner.query(`ALTER TABLE classrooms DROP COLUMN ${columnName}`);
}
}
} finally {
await runner.release();
}
}
private async ensureAiConfigTable(): Promise<void> {

View File

@@ -10,6 +10,13 @@ interface MockTable {
name: string;
columns: MockColumn[];
}
interface MockRunner {
release: jest.Mock;
connect: jest.Mock;
query: jest.Mock;
getTables: jest.Mock;
getTable: jest.Mock;
}
function mockRunner(overrides: {
getTables?: MockTable[];
@@ -28,10 +35,10 @@ function mockRunner(overrides: {
query.mockRejectedValue(overrides.queryError);
}
return { release, connect, query, getTables, getTable };
return { release, connect, query, getTables, getTable } satisfies MockRunner;
}
function createDataSource(runner: ReturnType<typeof mockRunner>, dbType: string = 'better-sqlite3') {
function createDataSource(runner: MockRunner, dbType: string = 'better-sqlite3') {
return {
options: { type: dbType },
createQueryRunner: jest.fn().mockReturnValue(runner),
@@ -46,12 +53,13 @@ interface MigrationsPrivate {
normalizeClassDates(): Promise<void>;
ensureCourseAttendanceSchema(): Promise<void>;
protectAttendanceHistory(): Promise<void>;
removeUnusedClassroomColumns(): Promise<void>;
}
describe('DatabaseMigrationsService — ensureAiConfigTable', () => {
let service: MigrationsPrivate & DatabaseMigrationsService;
async function bootstrap(runner: ReturnType<typeof mockRunner>) {
async function bootstrap(runner: MockRunner) {
const dataSource = createDataSource(runner);
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -220,7 +228,7 @@ describe('DatabaseMigrationsService — course attendance schema', () => {
describe('DatabaseMigrationsService — protectAttendanceHistory', () => {
let service: MigrationsPrivate & DatabaseMigrationsService;
async function bootstrap(runner: ReturnType<typeof mockRunner>, dbType: string = 'better-sqlite3') {
async function bootstrap(runner: MockRunner, dbType: string = 'better-sqlite3') {
const dataSource = createDataSource(runner, dbType);
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -416,7 +424,49 @@ describe('DatabaseMigrationsService — protectAttendanceHistory', () => {
});
});
async function bootstrapCourseAttendance(runner: ReturnType<typeof mockRunner>) {
describe('DatabaseMigrationsService — classroom cleanup', () => {
let cleanupService: MigrationsPrivate & DatabaseMigrationsService;
async function bootstrapClassroomCleanup(runner: MockRunner) {
const dataSource = createDataSource(runner);
const module: TestingModule = await Test.createTestingModule({
providers: [
DatabaseMigrationsService,
{ provide: getDataSourceToken(), useValue: dataSource },
],
}).compile();
cleanupService = module.get(DatabaseMigrationsService);
}
it('drops legacy classroom fields when present', async () => {
const runner = mockRunner({
getTables: [{ name: 'classrooms', columns: [] }],
getTable: {
name: 'classrooms',
columns: [{ name: 'id' }, { name: 'course_type' }, { name: 'supervisor' }],
},
});
await bootstrapClassroomCleanup(runner);
await cleanupService.removeUnusedClassroomColumns();
expect(runner.query).toHaveBeenCalledWith('ALTER TABLE classrooms DROP COLUMN course_type');
expect(runner.query).toHaveBeenCalledWith('ALTER TABLE classrooms DROP COLUMN supervisor');
expect(runner.release).toHaveBeenCalled();
});
it('does nothing when the classrooms table is absent', async () => {
const runner = mockRunner({ getTables: [] });
await bootstrapClassroomCleanup(runner);
await cleanupService.removeUnusedClassroomColumns();
expect(runner.query).not.toHaveBeenCalled();
expect(runner.release).toHaveBeenCalled();
});
});
async function bootstrapCourseAttendance(runner: MockRunner) {
const dataSource = createDataSource(runner);
const module: TestingModule = await Test.createTestingModule({
providers: [