Assessor_2.0/server/gpo/gpo-backup-management-servi...

86 lines
4.5 KiB
TypeScript

import { createHash } from 'node:crypto';
import type { ArtifactDeletionRequest, AssessorRepository } from '../../src/persistence/assessor-repository';
import { RepositoryConflictError, RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors';
import { createGpoReprocessingJob } from './gpo-backup-submission-service';
import type { BackendArtifactStore } from '../artifacts/backend-artifact-store';
function stableDeletionId(storageKey: string): string {
return `artifact-delete-${createHash('sha256').update(storageKey).digest('hex').slice(0, 32)}`;
}
export class GpoBackupManagementService {
constructor(
private readonly repository: AssessorRepository,
private readonly artifactStore: BackendArtifactStore,
private readonly now: () => Date = () => new Date()
) {}
async retry(projectId: string, artifactId: string) {
const jobs = (await this.repository.listIngestionJobs(projectId))
.filter(item => item.artifactId === artifactId && item.kind === 'analyze-gpo-backup')
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt) || right.id.localeCompare(left.id));
if (!jobs[0]) throw new RepositoryNotFoundError('Der GPO-Analysejob ist im Mandanten nicht vorhanden.');
const job = await this.repository.retryIngestionJob(projectId, jobs[0].id, this.now().toISOString());
return { version: 1 as const, artifactId, jobId: job.id, status: job.status, attempts: job.attempts };
}
async reprocess(projectId: string, artifactId: string, expectedLatestProcessingRunId: string) {
if (!expectedLatestProcessingRunId.trim()) throw new RepositoryValidationError('Der erwartete letzte Processing Run ist erforderlich.');
const artifact = (await this.repository.listImportArtifacts(projectId)).find(item => item.id === artifactId);
if (!artifact || artifact.storage.kind !== 'backend-controlled') throw new RepositoryNotFoundError('Das GPO-Artefakt ist im Mandanten nicht vorhanden.');
const runs = (await this.repository.listProcessingRuns(artifactId))
.filter(item => item.projectId === projectId)
.sort((left, right) => Date.parse(right.completedAt) - Date.parse(left.completedAt) || right.id.localeCompare(left.id));
const latest = runs[0];
if (!latest) throw new RepositoryNotFoundError('Für das GPO-Artefakt ist noch kein Processing Run vorhanden.');
if (latest.id !== expectedLatestProcessingRunId) {
throw new RepositoryConflictError('Der erwartete letzte Processing Run ist nicht mehr aktuell.');
}
const job = await this.repository.enqueueIngestionJob(createGpoReprocessingJob(artifact, latest.id, this.now().toISOString()));
return {
version: 1 as const,
artifactId,
jobId: job.id,
status: job.status,
supersedesProcessingRunId: latest.id
};
}
async delete(projectId: string, artifactId: string) {
const artifact = (await this.repository.listImportArtifacts(projectId)).find(item => item.id === artifactId);
if (!artifact || artifact.storage.kind !== 'backend-controlled') throw new RepositoryNotFoundError('Das GPO-Artefakt ist im Mandanten nicht vorhanden.');
const timestamp = this.now().toISOString();
const request: ArtifactDeletionRequest = {
version: 1,
id: stableDeletionId(artifact.storage.storageKey),
projectId,
artifactId,
storageKey: artifact.storage.storageKey,
attempts: 0,
nextAttemptAt: timestamp,
createdAt: timestamp
};
const deleted = await this.repository.deleteGpoArtifact(projectId, artifactId, request);
const storageRemoval = await processArtifactDeletionRequest(this.repository, this.artifactStore, deleted.request, this.now);
return { version: 1 as const, artifactId, deleted: true as const, storageRemoval };
}
}
export async function processArtifactDeletionRequest(
repository: AssessorRepository,
artifactStore: BackendArtifactStore,
request: ArtifactDeletionRequest,
now: () => Date = () => new Date()
): Promise<'complete' | 'pending'> {
try {
await artifactStore.remove(request.storageKey);
await repository.completeArtifactDeletionRequest(request.id);
return 'complete';
} catch {
const failedAt = now();
const retryAt = new Date(failedAt.getTime() + Math.min(60 * 60 * 1_000, 5_000 * 2 ** Math.min(request.attempts, 8)));
await repository.failArtifactDeletionRequest(request.id, failedAt.toISOString(), 'artifact-storage-delete-failed', retryAt.toISOString())
.catch(() => undefined);
return 'pending';
}
}