Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | 2x 2x 2x 2x 2x 2x 2x 9x 9x 2x 6x 6x 3x 1x 1x 1x 1x 1x 2x 1x 1x 1x 1x 2x 1x 1x 1x 1x | 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 };
}
}
|