Files
task-stream/libs/api/tracker/infrastructure/src/lib/mongo/sprints.repositories.ts
Josep Piguillem 50d3ffd1dd feat: sprints with burndown and carry-over
Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:59:01 +02:00

73 lines
2.1 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import {
FindSprintsRepository,
UpsertMaterializedSprintRepository,
} from '@task-stream/api/tracker/application';
import { SprintReadModel, SprintStatus } from '@task-stream/api/tracker/domain';
import { Model } from 'mongoose';
import { ReadModels, SprintDoc, fromDoc } from './read-model.schemas';
import { upsertIfNewer } from './upsert-if-newer';
const ORDER: Record<SprintStatus, number> = {
[SprintStatus.ACTIVE]: 0,
[SprintStatus.PLANNED]: 1,
[SprintStatus.CLOSED]: 2,
};
@Injectable()
export class MongoFindSprintsRepository extends FindSprintsRepository {
constructor(
@InjectModel(ReadModels.Sprint) private readonly sprints: Model<SprintDoc>,
) {
super();
}
async findById(id: string): Promise<SprintReadModel | null> {
const doc = await this.sprints.findById(id).lean<SprintDoc>();
return doc ? fromDoc(doc) : null;
}
async findByProject(projectId: string): Promise<SprintReadModel[]> {
const docs = await this.sprints.find({ projectId }).lean<SprintDoc[]>();
return docs
.map(fromDoc)
.sort(
(a, b) =>
ORDER[a.status] - ORDER[b.status] ||
(a.status === SprintStatus.CLOSED
? b.startDate.localeCompare(a.startDate)
: a.startDate.localeCompare(b.startDate)) ||
a.name.localeCompare(b.name),
);
}
async findActive(projectId: string): Promise<SprintReadModel | null> {
const doc = await this.sprints
.findOne({ projectId, status: SprintStatus.ACTIVE })
.lean<SprintDoc>();
return doc ? fromDoc(doc) : null;
}
}
@Injectable()
export class MongoUpsertMaterializedSprintRepository extends UpsertMaterializedSprintRepository {
constructor(
@InjectModel(ReadModels.Sprint) private readonly sprints: Model<SprintDoc>,
) {
super();
}
async upsert({
id,
version,
...fields
}: Pick<SprintReadModel, 'id' | 'version'> & Partial<SprintReadModel>) {
await upsertIfNewer(this.sprints, id, version, fields);
}
async remove(sprintId: string) {
await this.sprints.deleteOne({ _id: sprintId });
}
}