Expose project revision timeline

This commit is contained in:
2026-07-25 21:57:03 +02:00
parent 4b47bc2bcc
commit 5a0c9019af
14 changed files with 365 additions and 8 deletions
@@ -1,11 +1,14 @@
import { and, asc, desc, eq } from "drizzle-orm";
import { and, asc, desc, eq, lt } from "drizzle-orm";
import { deserializeProjectCommand } from "../../domain/models/project-command.model.js";
import type {
ProjectHistoryCommand,
ProjectHistoryDirection,
ListProjectRevisionsInput,
ProjectRevisionPage,
ProjectHistoryState,
ProjectHistoryStore,
} from "../../domain/ports/project-history.store.js";
import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js";
import type { AppDatabase } from "../database-context.js";
import { projectChangeSets } from "../schema/project-change-sets.js";
import { projectHistoryStackEntries } from "../schema/project-history-stack-entries.js";
@@ -47,6 +50,74 @@ export class ProjectHistoryRepository implements ProjectHistoryStore {
};
}
listRevisions(
input: ListProjectRevisionsInput
): ProjectRevisionPage | null {
validateRevisionPageInput(input);
const project = this.database
.select({ currentRevision: projects.currentRevision })
.from(projects)
.where(eq(projects.id, input.projectId))
.get();
if (!project) {
return null;
}
const projectCondition = eq(
projectRevisions.projectId,
input.projectId
);
const rows = 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(
input.beforeRevision === undefined
? projectCondition
: and(
projectCondition,
lt(
projectRevisions.revisionNumber,
input.beforeRevision
)
)
)
.orderBy(desc(projectRevisions.revisionNumber))
.limit(input.limit + 1)
.all();
const hasMore = rows.length > input.limit;
const revisions = rows.slice(0, input.limit).map((row) => ({
...row,
source: row.source as ProjectRevisionSource,
}));
return {
projectId: input.projectId,
currentRevision: project.currentRevision,
revisions,
nextBeforeRevision:
hasMore && revisions.length > 0
? revisions.at(-1)!.revisionNumber
: null,
};
}
getNextCommand(
projectId: string,
direction: ProjectHistoryDirection
@@ -93,3 +164,22 @@ export class ProjectHistoryRepository implements ProjectHistoryStore {
};
}
}
function validateRevisionPageInput(input: ListProjectRevisionsInput) {
if (
!Number.isSafeInteger(input.limit) ||
input.limit < 1 ||
input.limit > 100
) {
throw new Error("Project revision page limit must be between 1 and 100.");
}
if (
input.beforeRevision !== undefined &&
(!Number.isSafeInteger(input.beforeRevision) ||
input.beforeRevision < 1)
) {
throw new Error(
"Project revision cursor must be a positive integer."
);
}
}