feat(goals): add goals.service
This commit is contained in:
parent
9d9870877e
commit
160daab7c9
1 changed files with 111 additions and 0 deletions
111
src/goals/goals.service.ts
Normal file
111
src/goals/goals.service.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { and, desc, eq, isNull, sql } from 'drizzle-orm';
|
||||
import { db } from '../db/client';
|
||||
import { goals } from '../db/schema';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { OutboxService } from '../events/outbox.service';
|
||||
import type { AuthenticatedSession } from '../auth/tenant.guard';
|
||||
import type { CreateGoalDto, GoalStatusValue, UpdateGoalDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class GoalsService {
|
||||
constructor(
|
||||
private readonly outbox: OutboxService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
async list(session: AuthenticatedSession, status?: GoalStatusValue) {
|
||||
return db.query.goals.findMany({
|
||||
where: and(
|
||||
eq(goals.tenantId, session.tenantId),
|
||||
isNull(goals.deletedAt),
|
||||
...(status ? [eq(goals.status, status)] : []),
|
||||
),
|
||||
orderBy: desc(goals.createdAt),
|
||||
});
|
||||
}
|
||||
|
||||
async getById(session: AuthenticatedSession, id: string) {
|
||||
const row = await db.query.goals.findFirst({
|
||||
where: and(eq(goals.id, id), eq(goals.tenantId, session.tenantId), isNull(goals.deletedAt)),
|
||||
});
|
||||
if (!row) throw new NotFoundException('Goal not found');
|
||||
return row;
|
||||
}
|
||||
|
||||
async create(session: AuthenticatedSession, dto: CreateGoalDto) {
|
||||
return this.outbox.withTransaction(async (tx) => {
|
||||
const [row] = await tx
|
||||
.insert(goals)
|
||||
.values({
|
||||
tenantId: session.tenantId,
|
||||
ownerUserId: session.userId,
|
||||
horizon: dto.horizon,
|
||||
metric: dto.metric,
|
||||
target: dto.target,
|
||||
milestones: (dto.milestones ?? []) as unknown[],
|
||||
})
|
||||
.returning();
|
||||
await this.audit.record(tx, {
|
||||
tenantId: session.tenantId,
|
||||
actorId: session.userId,
|
||||
action: 'goal.created',
|
||||
resource: `goal:${row.id}`,
|
||||
});
|
||||
await this.outbox.record(tx, {
|
||||
tenantId: session.tenantId,
|
||||
workspaceId: session.workspaceId,
|
||||
actorId: session.userId,
|
||||
eventType: 'goal.created',
|
||||
aggregateType: 'goal',
|
||||
subjectId: row.id,
|
||||
correlationId: session.correlationId,
|
||||
payload: { horizon: row.horizon, metric: row.metric },
|
||||
});
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
async update(session: AuthenticatedSession, id: string, dto: UpdateGoalDto) {
|
||||
await this.getById(session, id);
|
||||
return this.outbox.withTransaction(async (tx) => {
|
||||
const [row] = await tx
|
||||
.update(goals)
|
||||
.set({
|
||||
...(dto.horizon !== undefined ? { horizon: dto.horizon } : {}),
|
||||
...(dto.metric !== undefined ? { metric: dto.metric } : {}),
|
||||
...(dto.target !== undefined ? { target: dto.target } : {}),
|
||||
...(dto.milestones !== undefined ? { milestones: dto.milestones as unknown[] } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status } : {}),
|
||||
updatedAt: new Date(),
|
||||
version: sql`${goals.version} + 1`,
|
||||
})
|
||||
.where(and(eq(goals.id, id), eq(goals.tenantId, session.tenantId)))
|
||||
.returning();
|
||||
await this.audit.record(tx, {
|
||||
tenantId: session.tenantId,
|
||||
actorId: session.userId,
|
||||
action: dto.status !== undefined ? `goal.status_changed:${dto.status}` : 'goal.updated',
|
||||
resource: `goal:${id}`,
|
||||
});
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
async softDelete(session: AuthenticatedSession, id: string) {
|
||||
await this.getById(session, id);
|
||||
await this.outbox.withTransaction(async (tx) => {
|
||||
await tx
|
||||
.update(goals)
|
||||
.set({ deletedAt: new Date() })
|
||||
.where(and(eq(goals.id, id), eq(goals.tenantId, session.tenantId)));
|
||||
await this.audit.record(tx, {
|
||||
tenantId: session.tenantId,
|
||||
actorId: session.userId,
|
||||
action: 'goal.deleted',
|
||||
resource: `goal:${id}`,
|
||||
});
|
||||
});
|
||||
return { deleted: true };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue