Describe snapshot source revisions
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { and, asc, desc, eq, lt } from "drizzle-orm";
|
||||
import { and, asc, desc, eq, inArray, lt } from "drizzle-orm";
|
||||
import { deserializeProjectCommand } from "../../domain/models/project-command.model.js";
|
||||
import type {
|
||||
ProjectHistoryCommand,
|
||||
ProjectHistoryDirection,
|
||||
ListProjectRevisionsInput,
|
||||
ProjectRevisionPage,
|
||||
ProjectRevisionSummary,
|
||||
ProjectHistoryState,
|
||||
ProjectHistoryStore,
|
||||
} from "../../domain/ports/project-history.store.js";
|
||||
@@ -118,6 +119,64 @@ export class ProjectHistoryRepository implements ProjectHistoryStore {
|
||||
};
|
||||
}
|
||||
|
||||
listRevisionsByNumbers(
|
||||
projectId: string,
|
||||
revisionNumbers: number[]
|
||||
): ProjectRevisionSummary[] {
|
||||
for (const revisionNumber of revisionNumbers) {
|
||||
if (
|
||||
!Number.isSafeInteger(revisionNumber) ||
|
||||
revisionNumber < 0
|
||||
) {
|
||||
throw new Error(
|
||||
"Project revision numbers must be non-negative integers."
|
||||
);
|
||||
}
|
||||
}
|
||||
const normalizedRevisionNumbers = [
|
||||
...new Set(revisionNumbers),
|
||||
].filter((revisionNumber) => revisionNumber > 0);
|
||||
if (!normalizedRevisionNumbers.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.database
|
||||
.select({
|
||||
revisionId: projectRevisions.id,
|
||||
changeSetId: projectChangeSets.id,
|
||||
revisionNumber: projectRevisions.revisionNumber,
|
||||
createdAtIso: projectRevisions.createdAtIso,
|
||||
actorId: projectRevisions.actorId,
|
||||
source: projectRevisions.source,
|
||||
description: projectRevisions.description,
|
||||
commandType: projectChangeSets.commandType,
|
||||
payloadSchemaVersion: projectChangeSets.payloadSchemaVersion,
|
||||
})
|
||||
.from(projectRevisions)
|
||||
.innerJoin(
|
||||
projectChangeSets,
|
||||
eq(
|
||||
projectChangeSets.projectRevisionId,
|
||||
projectRevisions.id
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(projectRevisions.projectId, projectId),
|
||||
inArray(
|
||||
projectRevisions.revisionNumber,
|
||||
normalizedRevisionNumbers
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(projectRevisions.revisionNumber))
|
||||
.all()
|
||||
.map((row) => ({
|
||||
...row,
|
||||
source: row.source as ProjectRevisionSource,
|
||||
}));
|
||||
}
|
||||
|
||||
getNextCommand(
|
||||
projectId: string,
|
||||
direction: ProjectHistoryDirection
|
||||
|
||||
@@ -47,6 +47,10 @@ export interface ProjectHistoryStore {
|
||||
listRevisions(
|
||||
input: ListProjectRevisionsInput
|
||||
): ProjectRevisionPage | null;
|
||||
listRevisionsByNumbers(
|
||||
projectId: string,
|
||||
revisionNumbers: number[]
|
||||
): ProjectRevisionSummary[];
|
||||
getNextCommand(
|
||||
projectId: string,
|
||||
direction: ProjectHistoryDirection
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getProjectRevisionDescription,
|
||||
getProjectRevisionSourceLabel,
|
||||
getProjectSnapshotKindLabel,
|
||||
getProjectSnapshotRevisionDescription,
|
||||
mergeProjectRevisionPages,
|
||||
} from "../utils/project-version-history";
|
||||
|
||||
@@ -343,7 +344,7 @@ export function ProjectVersionHistory({
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Projektstand</th>
|
||||
<th>Gesicherter Projektstand</th>
|
||||
<th>Erstellt</th>
|
||||
<th className="text-end">Aktion</th>
|
||||
</tr>
|
||||
@@ -368,7 +369,16 @@ export function ProjectVersionHistory({
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td>Revision {snapshot.sourceRevision}</td>
|
||||
<td>
|
||||
<strong>
|
||||
{snapshot.sourceRevision === 0
|
||||
? "Ausgangsstand"
|
||||
: `Revision ${snapshot.sourceRevision}`}
|
||||
</strong>
|
||||
<div className="small text-secondary">
|
||||
{getProjectSnapshotRevisionDescription(snapshot)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{formatDateTime(snapshot.createdAtIso)}</td>
|
||||
<td className="text-end">
|
||||
<button
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface ProjectSnapshotMetadataDto {
|
||||
id: string;
|
||||
projectId: string;
|
||||
sourceRevision: number;
|
||||
sourceRevisionMetadata: ProjectRevisionSummaryDto | null;
|
||||
schemaVersion: number;
|
||||
kind: "named" | "automatic";
|
||||
name: string;
|
||||
|
||||
@@ -49,8 +49,15 @@ export function getProjectRevisionSourceLabel(
|
||||
export function getProjectRevisionDescription(
|
||||
revision: ProjectRevisionSummaryDto
|
||||
) {
|
||||
const description = revision.description?.trim();
|
||||
if (
|
||||
description === `Undo ${revision.commandType}` ||
|
||||
description === `Redo ${revision.commandType}`
|
||||
) {
|
||||
return commandTypeLabels[revision.commandType] || revision.commandType;
|
||||
}
|
||||
return (
|
||||
revision.description?.trim() ||
|
||||
description ||
|
||||
commandTypeLabels[revision.commandType] ||
|
||||
revision.commandType
|
||||
);
|
||||
@@ -62,6 +69,26 @@ export function getProjectSnapshotKindLabel(
|
||||
return kind === "automatic" ? "Automatisch" : "Benannt";
|
||||
}
|
||||
|
||||
export function getProjectSnapshotRevisionDescription(
|
||||
snapshot: ProjectSnapshotMetadataDto
|
||||
) {
|
||||
if (snapshot.sourceRevision === 0) {
|
||||
return "Projektstart";
|
||||
}
|
||||
if (
|
||||
!snapshot.sourceRevisionMetadata ||
|
||||
snapshot.sourceRevisionMetadata.revisionNumber !==
|
||||
snapshot.sourceRevision
|
||||
) {
|
||||
return "Änderungsdetails nicht verfügbar";
|
||||
}
|
||||
return `${getProjectRevisionSourceLabel(
|
||||
snapshot.sourceRevisionMetadata.source
|
||||
)}: ${getProjectRevisionDescription(
|
||||
snapshot.sourceRevisionMetadata
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function mergeProjectRevisionPages(
|
||||
current: ProjectRevisionSummaryDto[],
|
||||
next: ProjectRevisionSummaryDto[]
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
createNamedProjectSnapshotSchema,
|
||||
restoreProjectSnapshotSchema,
|
||||
} from "../../shared/validation/project-snapshot.schemas.js";
|
||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||
import {
|
||||
projectCommandService,
|
||||
projectHistoryStore,
|
||||
} from "../composition/project-command-stores.js";
|
||||
import { projectSnapshotStore } from "../composition/project-snapshot-store.js";
|
||||
|
||||
export function listProjectSnapshots(req: Request, res: Response) {
|
||||
@@ -18,7 +21,7 @@ export function listProjectSnapshots(req: Request, res: Response) {
|
||||
if (!snapshots) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
return res.json(snapshots);
|
||||
return res.json(withSourceRevisionMetadata(projectId, snapshots));
|
||||
}
|
||||
|
||||
export function createNamedProjectSnapshot(
|
||||
@@ -43,7 +46,9 @@ export function createNamedProjectSnapshot(
|
||||
if (!snapshot) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
return res.status(201).json(snapshot);
|
||||
return res
|
||||
.status(201)
|
||||
.json(withSourceRevisionMetadata(projectId, [snapshot])[0]);
|
||||
} catch (error) {
|
||||
if (error instanceof ProjectRevisionConflictError) {
|
||||
return res.status(409).json({
|
||||
@@ -97,7 +102,9 @@ export function restoreProjectSnapshot(
|
||||
command: prepared.command,
|
||||
});
|
||||
return res.json({
|
||||
snapshot: prepared.snapshot,
|
||||
snapshot: withSourceRevisionMetadata(projectId, [
|
||||
prepared.snapshot,
|
||||
])[0],
|
||||
revision: result.revision,
|
||||
history: result.history,
|
||||
});
|
||||
@@ -120,6 +127,29 @@ export function restoreProjectSnapshot(
|
||||
}
|
||||
}
|
||||
|
||||
function withSourceRevisionMetadata<
|
||||
TSnapshot extends { sourceRevision: number },
|
||||
>(
|
||||
projectId: string,
|
||||
snapshots: TSnapshot[]
|
||||
) {
|
||||
const revisions = projectHistoryStore.listRevisionsByNumbers(
|
||||
projectId,
|
||||
snapshots.map((snapshot) => snapshot.sourceRevision)
|
||||
);
|
||||
const revisionByNumber = new Map(
|
||||
revisions.map((revision) => [
|
||||
revision.revisionNumber,
|
||||
revision,
|
||||
])
|
||||
);
|
||||
return snapshots.map((snapshot) => ({
|
||||
...snapshot,
|
||||
sourceRevisionMetadata:
|
||||
revisionByNumber.get(snapshot.sourceRevision) ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function getProjectId(req: Request, res: Response) {
|
||||
const { projectId } = req.params;
|
||||
if (typeof projectId !== "string" || !projectId.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user