metis-desk/modules/companies/src/api/infrastructure/persistence/sequelize-company.repository.ts

233 lines
7.1 KiB
TypeScript

import type { UniqueID } from "@repo/rdx-ddd";
import type { Result as DomainResult } from "@repo/rdx-utils";
import { Maybe, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import { Op } from "sequelize";
import type { ICompanyRepository } from "../../application/company.repository";
import type { Company } from "../../domain/company.aggregate";
import { CompanyError } from "../../domain/company-error";
import { CompanyModel, PersonModel, SiteModel } from "./company.model";
import type { CompanyPersistenceMapper } from "./company-persistence.mapper";
import { AssignmentModel, AssignmentRevisionModel, DepartmentModel } from "./people.model";
export class SequelizeCompanyRepository implements ICompanyRepository {
constructor(
private readonly database: Sequelize,
private readonly mapper: CompanyPersistenceMapper
) {}
private async children(ids: readonly string[], transaction?: Transaction) {
const options = {
where: { companyId: { [Op.in]: ids } },
...(transaction ? { transaction } : {}),
};
const [people, sites, departments, assignments] = await Promise.all([
PersonModel.findAll({
...options,
order: [
["name", "ASC"],
["id", "ASC"],
],
}),
SiteModel.findAll({ ...options, order: [["name", "ASC"]] }),
DepartmentModel.findAll({ ...options, order: [["name", "ASC"]] }),
AssignmentModel.findAll({
...options,
order: [
["startDate", "DESC"],
["id", "ASC"],
],
}),
]);
const revisions = assignments.length
? await AssignmentRevisionModel.findAll({
where: { assignmentId: { [Op.in]: assignments.map((item) => item.id) } },
...(transaction ? { transaction } : {}),
order: [["sequence", "ASC"]],
})
: [];
return { people, sites, departments, assignments, revisions };
}
async list(): Promise<readonly Company[]> {
return this.database.transaction(async (transaction) => {
const companies = await CompanyModel.findAll({
transaction,
order: [
["name", "ASC"],
["id", "ASC"],
],
});
if (!companies.length) {
return [];
}
const all = await this.children(
companies.map((item) => item.id),
transaction
);
return companies.map((company) => {
const assignments = all.assignments.filter((item) => item.companyId === company.id);
return this.mapper.toDomain({
company,
people: all.people.filter((item) => item.companyId === company.id),
sites: all.sites.filter((item) => item.companyId === company.id),
departments: all.departments.filter((item) => item.companyId === company.id),
assignments,
revisions: all.revisions.filter((item) =>
assignments.some((assignment) => assignment.id === item.assignmentId)
),
});
});
});
}
private async load(
id: UniqueID,
transaction: Transaction,
lock = false
): Promise<Maybe<Company>> {
const company = await CompanyModel.findByPk(id.toPrimitive(), {
transaction,
...(lock ? { lock: transaction.LOCK.UPDATE } : {}),
});
if (!company) {
return Maybe.none();
}
return Maybe.some(
this.mapper.toDomain({ company, ...(await this.children([company.id], transaction)) })
);
}
findById(id: UniqueID) {
return this.database.transaction((transaction) => this.load(id, transaction));
}
async create(company: Company): Promise<Company> {
await CompanyModel.create(this.mapper.toPersistence(company).company);
return company;
}
async removePerson(
id: UniqueID,
personId: UniqueID
): Promise<DomainResult<Company, CompanyError>> {
return this.database.transaction(async (transaction) => {
const current = (await this.load(id, transaction, true)).getOrUndefined();
if (!current) {
return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
}
const changed = current.removePerson(personId);
if (!changed.isSuccess) {
return changed;
}
try {
await PersonModel.destroy({
where: { id: personId.toPrimitive(), companyId: id.toPrimitive() },
transaction,
});
} catch {
return Result.fail(new CompanyError("PERSON_IN_USE"));
}
return changed;
});
}
async removeAssignment(
id: UniqueID,
personId: UniqueID,
assignmentId: UniqueID
): Promise<DomainResult<Company, CompanyError>> {
return this.database.transaction(async (transaction) => {
const current = (await this.load(id, transaction, true)).getOrUndefined();
if (!current) {
return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
}
const changed = current.removeAssignment(personId, assignmentId);
if (!changed.isSuccess) {
return changed;
}
await AssignmentRevisionModel.destroy({
where: { assignmentId: assignmentId.toPrimitive() },
transaction,
});
await AssignmentModel.destroy({
where: { id: assignmentId.toPrimitive(), companyId: id.toPrimitive() },
transaction,
});
return changed;
});
}
async update(
id: UniqueID,
change: (company: Company) => DomainResult<Company, CompanyError>
): Promise<DomainResult<Company, CompanyError>> {
return this.database.transaction(async (transaction) => {
const current = (await this.load(id, transaction, true)).getOrUndefined();
if (!current) {
return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
}
const changed = change(current);
if (!changed.isSuccess || changed.data === current) {
return changed;
}
const record = this.mapper.toPersistence(changed.data);
await CompanyModel.update(record.company, { where: { id: id.toPrimitive() }, transaction });
if (record.people.length) {
await PersonModel.bulkCreate(record.people, {
transaction,
updateOnDuplicate: ["name", "position", "email", "phone", "active", "isContact"],
});
}
if (record.sites.length) {
await SiteModel.bulkCreate(record.sites, {
transaction,
updateOnDuplicate: ["name", "address", "phone"],
});
}
if (record.departments.length) {
await DepartmentModel.bulkCreate(record.departments, {
transaction,
updateOnDuplicate: ["name", "nameKey", "active"],
});
}
if (record.assignments.length) {
await AssignmentModel.bulkCreate(record.assignments, {
transaction,
updateOnDuplicate: ["position", "startDate", "endDate", "isPrimary"],
});
}
const persistedIds = new Set(
current.assignments.flatMap((item) =>
item.history.map((revision) => revision.id.toPrimitive())
)
);
const newRevisions = record.revisions.filter((item) => !persistedIds.has(item.id));
if (newRevisions.length) {
await AssignmentRevisionModel.bulkCreate(newRevisions, { transaction });
}
return changed;
});
}
}