190 lines
7.6 KiB
TypeScript
190 lines
7.6 KiB
TypeScript
import type { IResourceInventoryService } from "@metis/resources/services";
|
|
import type { UniqueID } from "@repo/rdx-ddd";
|
|
import { Result } from "@repo/rdx-utils";
|
|
import { QueryTypes, type Sequelize, type Transaction } from "sequelize";
|
|
import { z } from "zod/v4";
|
|
|
|
import type {
|
|
ITicketInventoryService,
|
|
ITicketRepository,
|
|
TicketContext,
|
|
TicketLookups,
|
|
TicketSaveContext,
|
|
} from "../../application/ticket.repository";
|
|
import type { Ticket } from "../../domain/ticket.aggregate";
|
|
import { TicketError } from "../../domain/ticket-values";
|
|
|
|
import { TicketPersistenceMapper, ticketIdentity } from "./ticket-persistence.mapper";
|
|
export class SequelizeTicketRepository implements ITicketRepository {
|
|
private readonly mapper = new TicketPersistenceMapper();
|
|
constructor(
|
|
private readonly database: Sequelize,
|
|
private readonly inventory: ITicketInventoryService,
|
|
private readonly resources: IResourceInventoryService
|
|
) {}
|
|
private async read(
|
|
company: UniqueID | undefined,
|
|
transaction: Transaction
|
|
): Promise<Result<TicketContext, TicketError>> {
|
|
const companies = await this.database.query<{ id: string; name: string; active: boolean }>(
|
|
"SELECT id,name,active FROM companies ORDER BY name",
|
|
{ transaction, type: QueryTypes.SELECT }
|
|
);
|
|
if (company && !companies.some((item) => item.id === company.toPrimitive())) {
|
|
return Result.fail(new TicketError("COMPANY_NOT_FOUND"));
|
|
}
|
|
const users = await this.database.query<{ id: string; name: string; active: boolean }>(
|
|
"SELECT id,name,active FROM users ORDER BY name",
|
|
{ transaction, type: QueryTypes.SELECT }
|
|
);
|
|
const people = await this.database.query<{
|
|
id: string;
|
|
companyId: string;
|
|
name: string;
|
|
active: boolean;
|
|
}>("SELECT id,companyId,name,active FROM company_contacts", {
|
|
transaction,
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
const where = company ? " WHERE companyId=:companyId" : "";
|
|
const roots = await this.database.query<Record<string, unknown>>(
|
|
"SELECT id,companyId,reference,requestKey,requesterId,assigneeId,`values` FROM tickets" +
|
|
where,
|
|
{
|
|
transaction,
|
|
type: QueryTypes.SELECT,
|
|
replacements: { companyId: company?.toPrimitive() ?? null },
|
|
}
|
|
);
|
|
const revisions = await this.database.query<Record<string, unknown>>(
|
|
"SELECT r.id,r.ticketId,r.actorId,r.requesterId,r.assigneeId,r.`values`,r.recordedAt,r.position FROM ticket_revisions r JOIN tickets t ON t.id=r.ticketId" +
|
|
(company ? " WHERE t.companyId=:companyId" : ""),
|
|
{
|
|
transaction,
|
|
type: QueryTypes.SELECT,
|
|
replacements: { companyId: company?.toPrimitive() ?? null },
|
|
}
|
|
);
|
|
const option = (item: { id: string; name: string; active: boolean }) => ({
|
|
id: ticketIdentity(item.id),
|
|
name: item.name,
|
|
active: Boolean(item.active),
|
|
});
|
|
return Result.ok({
|
|
companies: companies.map(option),
|
|
technicians: users.map(option),
|
|
people: people.map((item) => ({
|
|
...option(item),
|
|
companyId: ticketIdentity(item.companyId),
|
|
})),
|
|
actorNames: new Map(users.map((item) => [item.id, item.name])),
|
|
tickets: roots.map((row) =>
|
|
this.mapper.toDomain(
|
|
row,
|
|
revisions.filter((item) => item.ticketId === row.id)
|
|
)
|
|
),
|
|
});
|
|
}
|
|
list(company?: UniqueID) {
|
|
return this.database.transaction((transaction) => this.read(company, transaction));
|
|
}
|
|
async lookups(company: UniqueID): Promise<Result<TicketLookups, TicketError>> {
|
|
const rows = await this.database.query<{ id: string }>(
|
|
"SELECT id FROM companies WHERE id=:id",
|
|
{ replacements: { id: company.toPrimitive() }, type: QueryTypes.SELECT }
|
|
);
|
|
if (!rows.length) {
|
|
return Result.fail(new TicketError("COMPANY_NOT_FOUND"));
|
|
}
|
|
return Result.ok(await this.readLookups(company));
|
|
}
|
|
private async readLookups(company: UniqueID): Promise<TicketLookups> {
|
|
const people = await this.database.query<{ id: string; name: string; active: boolean }>(
|
|
"SELECT id,name,active FROM company_contacts WHERE companyId=:id",
|
|
{ replacements: { id: company.toPrimitive() }, type: QueryTypes.SELECT }
|
|
);
|
|
const [assets, resources, workstations] = await Promise.all([
|
|
this.inventory.list(company),
|
|
this.resources.list(company),
|
|
this.inventory.workstations(company),
|
|
]);
|
|
return {
|
|
people: people.map((item) => ({
|
|
id: ticketIdentity(item.id),
|
|
name: item.name,
|
|
active: Boolean(item.active),
|
|
})),
|
|
assets: assets.map((item) => ({ id: item.id, name: item.name, active: item.available })),
|
|
resources: resources.map((item) => ({ id: item.id, name: item.name, active: item.active })),
|
|
workstations,
|
|
};
|
|
}
|
|
save(
|
|
company: UniqueID,
|
|
requestKey: UniqueID,
|
|
action: (context: TicketSaveContext) => Result<Ticket, TicketError>
|
|
) {
|
|
return this.database.transaction(async (transaction) => {
|
|
await this.database.query("SELECT id FROM companies WHERE id=:id FOR UPDATE", {
|
|
replacements: { id: company.toPrimitive() },
|
|
transaction,
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
await this.database.query("SELECT id FROM ticket_counter WHERE id=1 FOR UPDATE", {
|
|
transaction,
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
const context = await this.read(undefined, transaction);
|
|
if (!context.isSuccess) {
|
|
return Result.fail(context.error);
|
|
}
|
|
if (!context.data.companies.some((item) => item.id.equals(company))) {
|
|
return Result.fail(new TicketError("COMPANY_NOT_FOUND"));
|
|
}
|
|
const sequence = await this.database.query<{ nextNumber: number }>(
|
|
"SELECT nextNumber FROM ticket_counter WHERE id=1",
|
|
{ transaction, type: QueryTypes.SELECT }
|
|
);
|
|
const nextNumber = z.number().int().positive().parse(sequence[0]?.nextNumber);
|
|
const scoped = {
|
|
...context.data,
|
|
tickets: context.data.tickets.filter((item) => item.companyId.equals(company)),
|
|
lookups: await this.readLookups(company),
|
|
nextNumber,
|
|
requestTicket: context.data.tickets.find((item) => item.requestKey.equals(requestKey)),
|
|
};
|
|
const result = action(scoped);
|
|
if (!result.isSuccess) {
|
|
return Result.fail(result.error);
|
|
}
|
|
const previous = context.data.tickets.find((item) => item.id.equals(result.data.id));
|
|
await this.write(result.data, previous, transaction);
|
|
return Result.ok({ ticket: result.data, context: context.data });
|
|
});
|
|
}
|
|
private async write(ticket: Ticket, previous: Ticket | undefined, transaction: Transaction) {
|
|
const data = this.mapper.toPersistence(ticket);
|
|
await this.database.query(
|
|
previous
|
|
? "UPDATE tickets SET requesterId=:requesterId,assigneeId=:assigneeId,`values`=:values WHERE id=:id"
|
|
: "INSERT INTO tickets (id,companyId,reference,requestKey,requesterId,assigneeId,`values`) VALUES (:id,:companyId,:reference,:requestKey,:requesterId,:assigneeId,:values)",
|
|
{ replacements: data.root, transaction }
|
|
);
|
|
const existing = new Set(previous?.history.map((item) => item.id.toPrimitive()) ?? []);
|
|
for (const row of data.revisions) {
|
|
if (!existing.has(row.id)) {
|
|
await this.database.query(
|
|
"INSERT INTO ticket_revisions (id,ticketId,actorId,requesterId,assigneeId,`values`,recordedAt,position) VALUES (:id,:ticketId,:actorId,:requesterId,:assigneeId,:values,:recordedAt,:position)",
|
|
{ replacements: row, transaction }
|
|
);
|
|
}
|
|
}
|
|
if (!previous) {
|
|
await this.database.query("UPDATE ticket_counter SET nextNumber=nextNumber+1 WHERE id=1", {
|
|
transaction,
|
|
});
|
|
}
|
|
}
|
|
}
|