690602:1245 ADR-033-233 #02
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
// File: src/modules/ai/workers/cleanup-temp-files.worker.spec.ts
|
||||||
|
// Change Log:
|
||||||
|
// - 2026-06-02: เพิ่ม regression test สำหรับ schema drift ของ temp_attachment_id
|
||||||
|
|
||||||
|
import { CleanupTempFilesWorker } from './cleanup-temp-files.worker';
|
||||||
|
import { MigrationReviewStatus } from '../../migration/entities/migration-review-queue.entity';
|
||||||
|
|
||||||
|
describe('CleanupTempFilesWorker', () => {
|
||||||
|
const reviewQueueRepository = {
|
||||||
|
find: jest.fn(),
|
||||||
|
};
|
||||||
|
const attachmentQueryBuilder = {
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
getMany: jest.fn(),
|
||||||
|
};
|
||||||
|
const attachmentRepository = {
|
||||||
|
createQueryBuilder: jest.fn().mockReturnValue(attachmentQueryBuilder),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let worker: CleanupTempFilesWorker;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
attachmentQueryBuilder.where.mockReturnThis();
|
||||||
|
attachmentQueryBuilder.andWhere.mockReturnThis();
|
||||||
|
worker = new CleanupTempFilesWorker(
|
||||||
|
attachmentRepository as never,
|
||||||
|
reviewQueueRepository as never
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip cleanup safely when temp_attachment_id column is missing', async () => {
|
||||||
|
reviewQueueRepository.find.mockRejectedValue(
|
||||||
|
new Error(
|
||||||
|
"Unknown column 'MigrationReviewQueue.temp_attachment_id' in 'SELECT'"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const warnSpy = jest.spyOn(worker['logger'], 'warn');
|
||||||
|
const errorSpy = jest.spyOn(worker['logger'], 'error');
|
||||||
|
|
||||||
|
await worker.handleCleanup();
|
||||||
|
|
||||||
|
expect(reviewQueueRepository.find).toHaveBeenCalledWith({
|
||||||
|
select: ['tempAttachmentId'],
|
||||||
|
where: { status: MigrationReviewStatus.PENDING },
|
||||||
|
});
|
||||||
|
expect(attachmentRepository.createQueryBuilder).not.toHaveBeenCalled();
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
'migration_review_queue.temp_attachment_id is missing'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// File: src/modules/ai/workers/cleanup-temp-files.worker.ts
|
// File: src/modules/ai/workers/cleanup-temp-files.worker.ts
|
||||||
// Change Log:
|
// Change Log:
|
||||||
// - 2026-05-22: อัปเดตและสร้างตัวล้างไฟล์ชั่วคราว (T016) เพื่อลบไฟล์ที่หมดอายุ 24 ชม.
|
// - 2026-05-22: อัปเดตและสร้างตัวล้างไฟล์ชั่วคราว (T016) เพื่อลบไฟล์ที่หมดอายุ 24 ชม.
|
||||||
|
// - 2026-06-02: ข้าม cleanup อย่างปลอดภัยเมื่อ schema migration_review_queue ยังไม่มี temp_attachment_id
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
@@ -16,6 +17,8 @@ import {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class CleanupTempFilesWorker {
|
export class CleanupTempFilesWorker {
|
||||||
private readonly logger = new Logger(CleanupTempFilesWorker.name);
|
private readonly logger = new Logger(CleanupTempFilesWorker.name);
|
||||||
|
private static readonly MISSING_TEMP_ATTACHMENT_COLUMN_MESSAGE =
|
||||||
|
'temp_attachment_id';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Attachment)
|
@InjectRepository(Attachment)
|
||||||
@@ -34,13 +37,10 @@ export class CleanupTempFilesWorker {
|
|||||||
try {
|
try {
|
||||||
const oneDayAgo = new Date();
|
const oneDayAgo = new Date();
|
||||||
oneDayAgo.setHours(oneDayAgo.getHours() - 24);
|
oneDayAgo.setHours(oneDayAgo.getHours() - 24);
|
||||||
const pendingRecords = await this.reviewQueueRepository.find({
|
const pendingAttachmentIds = await this.getPendingAttachmentIds();
|
||||||
select: ['tempAttachmentId'],
|
if (pendingAttachmentIds === null) {
|
||||||
where: { status: MigrationReviewStatus.PENDING },
|
return;
|
||||||
});
|
}
|
||||||
const pendingAttachmentIds = pendingRecords
|
|
||||||
.map((r) => r.tempAttachmentId)
|
|
||||||
.filter((id): id is number => id !== undefined && id !== null);
|
|
||||||
const query = this.attachmentRepository
|
const query = this.attachmentRepository
|
||||||
.createQueryBuilder('attachment')
|
.createQueryBuilder('attachment')
|
||||||
.where('attachment.isTemporary = :isTemporary', { isTemporary: true })
|
.where('attachment.isTemporary = :isTemporary', { isTemporary: true })
|
||||||
@@ -85,4 +85,37 @@ export class CleanupTempFilesWorker {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* อ่านรายการ temp attachment ที่ยังถูกใช้งานโดย migration review status=PENDING
|
||||||
|
* ถ้า schema ยังไม่พร้อม ให้ข้าม cleanup รอบนี้เพื่อป้องกันการลบไฟล์ที่ยังถูกอ้างอิง
|
||||||
|
*/
|
||||||
|
private async getPendingAttachmentIds(): Promise<number[] | null> {
|
||||||
|
try {
|
||||||
|
const pendingRecords = await this.reviewQueueRepository.find({
|
||||||
|
select: ['tempAttachmentId'],
|
||||||
|
where: { status: MigrationReviewStatus.PENDING },
|
||||||
|
});
|
||||||
|
return pendingRecords
|
||||||
|
.map((record) => record.tempAttachmentId)
|
||||||
|
.filter((id): id is number => id !== undefined && id !== null);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isMissingTempAttachmentIdColumnError(error)) {
|
||||||
|
this.logger.warn(
|
||||||
|
'Skipping temporary files cleanup because migration_review_queue.temp_attachment_id is missing. Apply delta specs/03-Data-and-Storage/deltas/2026-06-02-add-temp-attachment-id-to-migration-review-queue.sql first.'
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isMissingTempAttachmentIdColumnError(error: unknown): boolean {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return error.message.includes(
|
||||||
|
CleanupTempFilesWorker.MISSING_TEMP_ATTACHMENT_COLUMN_MESSAGE
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-- File: specs/03-Data-and-Storage/deltas/2026-06-02-add-temp-attachment-id-to-migration-review-queue.rollback.sql
|
||||||
|
-- Change Log:
|
||||||
|
-- - 2026-06-02: ลบคอลัมน์ temp_attachment_id ออกจากตาราง migration_review_queue
|
||||||
|
|
||||||
|
-- Rollback Delta: ลบคอลัมน์ temp_attachment_id ออกจากตาราง migration_review_queue
|
||||||
|
-- Date: 2026-06-02
|
||||||
|
-- Related ADR: ADR-028, ADR-023A
|
||||||
|
-- Applied in: v1.9.8
|
||||||
|
|
||||||
|
ALTER TABLE migration_review_queue
|
||||||
|
DROP COLUMN temp_attachment_id;
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
-- File: specs/03-Data-and-Storage/deltas/2026-06-02-add-temp-attachment-id-to-migration-review-queue.sql
|
||||||
|
-- Change Log:
|
||||||
|
-- - 2026-06-02: เพิ่มคอลัมน์ temp_attachment_id ในตาราง migration_review_queue เพื่อแก้บั๊ก CleanupTempFilesWorker
|
||||||
|
|
||||||
|
-- Delta: เพิ่มคอลัมน์ temp_attachment_id ในตาราง migration_review_queue
|
||||||
|
-- Date: 2026-06-02
|
||||||
|
-- Related ADR: ADR-028, ADR-023A
|
||||||
|
-- Applied in: v1.9.8
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- การปรับปรุงตาราง migration_review_queue (Schema changes)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE migration_review_queue
|
||||||
|
ADD COLUMN temp_attachment_id INT NULL COMMENT 'Temporary attachment ID referencing attachments.id (ADR-028)'
|
||||||
|
AFTER STATUS;
|
||||||
@@ -1512,6 +1512,7 @@ CREATE TABLE migration_review_queue (
|
|||||||
confidence_score DECIMAL(5, 4) NOT NULL COMMENT 'AI confidence score 0.0000-1.0000',
|
confidence_score DECIMAL(5, 4) NOT NULL COMMENT 'AI confidence score 0.0000-1.0000',
|
||||||
ocr_used TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'ระบุว่าใช้ OCR path หรือไม่',
|
ocr_used TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'ระบุว่าใช้ OCR path หรือไม่',
|
||||||
STATUS ENUM('PENDING', 'APPROVED', 'IMPORTED', 'REJECTED') NOT NULL DEFAULT 'PENDING',
|
STATUS ENUM('PENDING', 'APPROVED', 'IMPORTED', 'REJECTED') NOT NULL DEFAULT 'PENDING',
|
||||||
|
temp_attachment_id INT NULL COMMENT 'Temporary attachment ID referencing attachments.id (ADR-028)',
|
||||||
reviewed_by INT NULL COMMENT 'Internal users.user_id ของผู้ review',
|
reviewed_by INT NULL COMMENT 'Internal users.user_id ของผู้ review',
|
||||||
reviewed_at DATETIME NULL COMMENT 'เวลาที่ review record',
|
reviewed_at DATETIME NULL COMMENT 'เวลาที่ review record',
|
||||||
rejection_reason VARCHAR(500) NULL COMMENT 'เหตุผลเมื่อ reject',
|
rejection_reason VARCHAR(500) NULL COMMENT 'เหตุผลเมื่อ reject',
|
||||||
|
|||||||
Reference in New Issue
Block a user