fix: close final F4 review gaps
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class F4AlignAssetVersionFunctionChange1790000700000 implements MigrationInterface {
|
||||
name = 'F4AlignAssetVersionFunctionChange1790000700000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_versions
|
||||
DROP CONSTRAINT IF EXISTS chk_asset_versions_change_type
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_versions
|
||||
ADD CONSTRAINT chk_asset_versions_change_type CHECK (change_type IN (
|
||||
'BASELINE', 'CREATED', 'UPDATED', 'CONTEXT_CHANGED', 'FUNCTION_CHANGED',
|
||||
'STATUS_CHANGED', 'OPERATIONAL_STATUS_CHANGED', 'REGISTRY_UPDATED',
|
||||
'GEOMETRY_UPDATED', 'GEOMETRY_REMOVED',
|
||||
'MEDIA_UPLOADED', 'MEDIA_UPDATED', 'MEDIA_REMOVED',
|
||||
'PROVENANCE_BASELINE', 'PROVENANCE_UPDATED', 'PROVENANCE_VERIFIED'
|
||||
))
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
UPDATE asset_versions
|
||||
SET change_type='UPDATED'
|
||||
WHERE change_type='FUNCTION_CHANGED'
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_versions
|
||||
DROP CONSTRAINT IF EXISTS chk_asset_versions_change_type
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_versions
|
||||
ADD CONSTRAINT chk_asset_versions_change_type CHECK (change_type IN (
|
||||
'BASELINE', 'CREATED', 'UPDATED', 'CONTEXT_CHANGED',
|
||||
'STATUS_CHANGED', 'OPERATIONAL_STATUS_CHANGED', 'REGISTRY_UPDATED',
|
||||
'GEOMETRY_UPDATED', 'GEOMETRY_REMOVED',
|
||||
'MEDIA_UPLOADED', 'MEDIA_UPDATED', 'MEDIA_REMOVED',
|
||||
'PROVENANCE_BASELINE', 'PROVENANCE_UPDATED', 'PROVENANCE_VERIFIED'
|
||||
))
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export class InspectionReportWordService {
|
||||
reportDescription: row.reportDescription,
|
||||
});
|
||||
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
||||
const storedName = `${row.id}.docx`;
|
||||
const storedName = `${row.id}-${built.sha256.slice(0, 16)}.docx`;
|
||||
const originalName = `${row.code}.docx`;
|
||||
const filePath = resolve(this.root, storedName);
|
||||
await writeFile(filePath, built.buffer, { mode: 0o600 });
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { mkdir, unlink, writeFile } from 'node:fs/promises';
|
||||
import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';
|
||||
import { isAbsolute, parse, resolve } from 'node:path';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@@ -58,7 +59,7 @@ export class InspectionReportWorkflowService {
|
||||
config: ConfigService,
|
||||
) {
|
||||
const configured = config.get<string>('INSPECTION_REPORT_UPLOAD_ROOT')
|
||||
?? '/app/storage/inspection-reports';
|
||||
?? '/app/storage/asset-media/inspection-reports';
|
||||
if (!isAbsolute(configured)) throw new Error('INSPECTION_REPORT_UPLOAD_ROOT must be absolute');
|
||||
this.root = resolve(configured);
|
||||
if (this.root === parse(this.root).root) {
|
||||
@@ -97,6 +98,30 @@ export class InspectionReportWorkflowService {
|
||||
UPDATE inspection_reports
|
||||
SET executive_summary=$2,
|
||||
report_description=$3,
|
||||
word_status=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN 'PENDING' ELSE word_status END,
|
||||
word_original_name=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_original_name END,
|
||||
word_stored_name=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_stored_name END,
|
||||
word_mime_type=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_mime_type END,
|
||||
word_size_bytes=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_size_bytes END,
|
||||
word_sha256=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_sha256 END,
|
||||
word_generated_at=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_generated_at END,
|
||||
word_error=CASE
|
||||
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
|
||||
THEN NULL ELSE word_error END,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE id=$1
|
||||
`, [reportId, executiveSummary, reportDescription]);
|
||||
@@ -195,6 +220,44 @@ export class InspectionReportWorkflowService {
|
||||
}
|
||||
}
|
||||
|
||||
async officialPdfContent(reportId: string): Promise<{ filePath: string; originalName: string; mimeType: string }> {
|
||||
const [row] = await this.dataSource.query(`
|
||||
SELECT
|
||||
gedo_pdf_original_name AS "originalName",
|
||||
gedo_pdf_stored_name AS "storedName",
|
||||
gedo_pdf_mime_type AS "mimeType",
|
||||
gedo_pdf_size_bytes::integer AS "sizeBytes",
|
||||
gedo_pdf_sha256 AS sha256
|
||||
FROM inspection_reports
|
||||
WHERE id=$1
|
||||
`, [reportId]) as Array<{
|
||||
originalName: string | null;
|
||||
storedName: string | null;
|
||||
mimeType: string | null;
|
||||
sizeBytes: number | null;
|
||||
sha256: string | null;
|
||||
}>;
|
||||
if (!row) throw reportNotFound();
|
||||
if (!row.originalName || !row.storedName || !row.sizeBytes || !row.sha256) {
|
||||
throw new NotFoundException({
|
||||
code: 'INSPECTION_REPORT_GEDO_PDF_NOT_AVAILABLE',
|
||||
message: 'El PDF oficial de GEDO todavía no está disponible',
|
||||
});
|
||||
}
|
||||
const filePath = resolve(this.root, row.storedName);
|
||||
if (filePath === this.root || !filePath.startsWith(`${this.root}/`)) throw this.reportStorageError();
|
||||
const fileStat = await stat(filePath).catch(() => null);
|
||||
if (!fileStat?.isFile() || fileStat.size !== row.sizeBytes) throw this.reportStorageError();
|
||||
const buffer = await readFile(filePath);
|
||||
const sha256 = createHash('sha256').update(buffer).digest('hex');
|
||||
if (sha256 !== row.sha256) throw this.reportStorageError();
|
||||
return {
|
||||
filePath,
|
||||
originalName: row.originalName,
|
||||
mimeType: row.mimeType ?? 'application/pdf',
|
||||
};
|
||||
}
|
||||
|
||||
async addFollowUp(
|
||||
reportId: string,
|
||||
dto: CreateInspectionReportFollowUpDto,
|
||||
@@ -291,6 +354,13 @@ export class InspectionReportWorkflowService {
|
||||
`, [reportId]);
|
||||
}
|
||||
|
||||
private reportStorageError(): InternalServerErrorException {
|
||||
return new InternalServerErrorException({
|
||||
code: 'INSPECTION_REPORT_GEDO_PDF_STORAGE_ERROR',
|
||||
message: 'El PDF oficial de GEDO no está disponible o no supera la validación de integridad',
|
||||
});
|
||||
}
|
||||
|
||||
private assertPdf(file: UploadedInspectionReportFile | undefined): void {
|
||||
if (!file?.buffer?.length) {
|
||||
throw new BadRequestException({
|
||||
|
||||
@@ -68,6 +68,18 @@ export class InspectionReportsController {
|
||||
return response.sendFile(content.filePath);
|
||||
}
|
||||
|
||||
@Get(':id/gedo-pdf')
|
||||
@RequirePermissions('inspection_reports.read')
|
||||
async gedoPdfContent(
|
||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||
@Res() response: Response,
|
||||
) {
|
||||
const content = await this.workflow.officialPdfContent(id);
|
||||
response.setHeader('Content-Type', content.mimeType);
|
||||
response.setHeader('Content-Disposition', `attachment; filename="${content.originalName.replaceAll('\"', '')}"`);
|
||||
return response.sendFile(content.filePath);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions('inspection_reports.generate')
|
||||
updateNarrative(
|
||||
|
||||
Reference in New Issue
Block a user