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