feat: add application layer
- Implement TaskService with business logic - Implement StateService with business logic
This commit is contained in:
parent
7bf617bd4b
commit
8eafb8ee81
|
|
@ -0,0 +1,10 @@
|
|||
import type { State } from "../../domain/state/state.entity.js";
|
||||
import type { StateRepository } from "../../domain/state/state.repository.js";
|
||||
|
||||
export class StateService {
|
||||
constructor(private readonly stateRepo: StateRepository) {}
|
||||
|
||||
async getAll(): Promise<State[]> {
|
||||
return this.stateRepo.findAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import type { Task, TaskState } from "../../domain/task/task.entity.js";
|
||||
import type { TaskRepository } from "../../domain/task/task.repository.js";
|
||||
import type { StateRepository } from "../../domain/state/state.repository.js";
|
||||
import type { CreateTaskDto, UpdateTaskDto, AddNoteDto } from "../../shared/dto.js";
|
||||
import { AppError } from "../../shared/app-error.js";
|
||||
|
||||
const VALID_TRANSITIONS: Record<TaskState, TaskState[]> = {
|
||||
new: ["active"],
|
||||
active: ["resolved", "closed"],
|
||||
resolved: ["closed", "active"],
|
||||
closed: [],
|
||||
};
|
||||
|
||||
export class TaskService {
|
||||
constructor(
|
||||
private readonly taskRepo: TaskRepository,
|
||||
private readonly stateRepo: StateRepository
|
||||
) {}
|
||||
|
||||
async getAll(): Promise<Task[]> {
|
||||
return this.taskRepo.findAll();
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<Task> {
|
||||
const task = await this.taskRepo.findById(id);
|
||||
if (!task) {
|
||||
throw new AppError(404, `Task with id '${id}' not found`);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
async create(dto: CreateTaskDto): Promise<Task> {
|
||||
const now = new Date().toISOString().split("T")[0];
|
||||
|
||||
const newTask: Task = {
|
||||
id: crypto.randomUUID(),
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
dueDate: dto.dueDate,
|
||||
completed: false,
|
||||
deletedAt: null,
|
||||
stateHistory: [{ state: "new", date: now }],
|
||||
notes: dto.notes ?? [],
|
||||
};
|
||||
|
||||
return this.taskRepo.create(newTask);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateTaskDto): Promise<Task> {
|
||||
await this.getById(id);
|
||||
|
||||
const updated = await this.taskRepo.update(id, {
|
||||
...(dto.title !== undefined && { title: dto.title }),
|
||||
...(dto.description !== undefined && { description: dto.description }),
|
||||
...(dto.dueDate !== undefined && { dueDate: dto.dueDate }),
|
||||
...(dto.completed !== undefined && { completed: dto.completed }),
|
||||
...(dto.notes !== undefined && { notes: dto.notes }),
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new AppError(500, "Failed to update task");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.getById(id);
|
||||
const deleted = await this.taskRepo.delete(id);
|
||||
if (!deleted) {
|
||||
throw new AppError(500, "Failed to delete task");
|
||||
}
|
||||
}
|
||||
|
||||
async transitionState(id: string, newState: TaskState): Promise<Task> {
|
||||
const task = await this.getById(id);
|
||||
const currentState = task.stateHistory[task.stateHistory.length - 1]?.state;
|
||||
|
||||
if (!currentState) {
|
||||
throw new AppError(500, "Task has no state history");
|
||||
}
|
||||
|
||||
const allowed = VALID_TRANSITIONS[currentState];
|
||||
if (!allowed.includes(newState)) {
|
||||
throw new AppError(400, `Cannot transition from '${currentState}' to '${newState}'. Allowed: ${allowed.join(", ") || "none"}`);
|
||||
}
|
||||
|
||||
const validState = await this.stateRepo.findByName(newState);
|
||||
if (!validState) {
|
||||
throw new AppError(400, `Invalid state '${newState}'`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString().split("T")[0];
|
||||
const updated = await this.taskRepo.update(id, {
|
||||
stateHistory: [...task.stateHistory, { state: newState, date: now }],
|
||||
completed: newState === "closed",
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new AppError(500, "Failed to transition task state");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async addNote(id: string, dto: AddNoteDto): Promise<Task> {
|
||||
const task = await this.getById(id);
|
||||
|
||||
const updated = await this.taskRepo.update(id, {
|
||||
notes: [...task.notes, dto.note],
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new AppError(500, "Failed to add note");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteNote(id: string, noteIndex: number): Promise<Task> {
|
||||
const task = await this.getById(id);
|
||||
|
||||
if (noteIndex < 0 || noteIndex >= task.notes.length) {
|
||||
throw new AppError(400, `Invalid note index ${noteIndex}`);
|
||||
}
|
||||
|
||||
const notes = [...task.notes];
|
||||
notes.splice(noteIndex, 1);
|
||||
|
||||
const updated = await this.taskRepo.update(id, { notes });
|
||||
if (!updated) {
|
||||
throw new AppError(500, "Failed to delete note");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue