metis-desk/modules/companies/src/api/infrastructure/persistence/sequelize-application-configuration.repository.ts
2026-10-25 01:19:17 +02:00

297 lines
11 KiB
TypeScript

import type { IResourceReferenceSession } from "@metis/resources/services";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import { QueryTypes } from "sequelize";
import type {
ApplicationContext,
IApplicationConfigurationRepository,
} from "../../application/application-configuration.repository";
import {
applicationReferences,
requiresAvailableApplication,
} from "../../application/application-configuration-policy";
import type { ResourceClientCapabilities } from "../../application/resource-client-capabilities";
import type { ApplicationConfiguration } from "../../domain/application-configuration.aggregate";
import { ApplicationConfigurationError } from "../../domain/application-configuration-values";
import type {
ApplicationRevisionRow,
ApplicationRow,
} from "./application-configuration-persistence.mapper";
import { ApplicationConfigurationPersistenceMapper } from "./application-configuration-persistence.mapper";
import { loadGlobalConfigurationReferences } from "./configuration-resource-references";
import type { ResourceConfigurationDependencies } from "./resource-configuration-dependencies";
import type { ProfileRevisionRow, ProfileRow } from "./work-profile-persistence.mapper";
import { WorkProfilePersistenceMapper, profileIdentity } from "./work-profile-persistence.mapper";
export class SequelizeApplicationConfigurationRepository
implements IApplicationConfigurationRepository
{
private readonly mapper = new ApplicationConfigurationPersistenceMapper();
constructor(
private readonly database: Sequelize,
private readonly dependencies: ResourceConfigurationDependencies
) {}
private async load(
company: UniqueID,
transaction: Transaction,
write: boolean,
session: IResourceReferenceSession,
additionalId?: string
): Promise<Result<ApplicationContext, ApplicationConfigurationError>> {
const replacements = { companyId: company.toPrimitive() };
const options = { replacements, transaction, type: QueryTypes.SELECT };
const companies = await this.database.query<{ id: string }>(
`SELECT id FROM companies WHERE id=:companyId${write ? " FOR UPDATE" : ""}`,
{ ...options, type: QueryTypes.SELECT }
);
if (!companies.length) {
return Result.fail(new ApplicationConfigurationError("COMPANY_NOT_FOUND"));
}
const rows = await this.database.query<ApplicationRow>(
"SELECT * FROM application_configurations WHERE companyId=:companyId",
{ ...options, type: QueryTypes.SELECT }
);
const revisions = await this.database.query<ApplicationRevisionRow>(
"SELECT r.* FROM application_configuration_revisions r JOIN application_configurations m ON m.id=r.configurationId WHERE m.companyId=:companyId",
{ ...options, type: QueryTypes.SELECT }
);
const profiles = await this.database.query<ProfileRow>(
"SELECT * FROM work_profiles WHERE companyId=:companyId",
{ ...options, type: QueryTypes.SELECT }
);
const profileRevisions = await this.database.query<ProfileRevisionRow>(
"SELECT r.* FROM work_profile_revisions r JOIN work_profiles p ON p.id=r.profileId WHERE p.companyId=:companyId",
{ ...options, type: QueryTypes.SELECT }
);
const people = await this.database.query<{ id: string; active: boolean }>(
"SELECT id,active FROM company_contacts WHERE companyId=:companyId",
{ ...options, type: QueryTypes.SELECT }
);
const actors = await this.database.query<{ id: string; name: string }>(
"SELECT id,name FROM users",
{ transaction, type: QueryTypes.SELECT }
);
const ids = [
...new Set([...rows.map((row) => row.resourceId), ...(additionalId ? [additionalId] : [])]),
];
const globalResources = await loadGlobalConfigurationReferences(
session,
"application-configuration",
ids
);
const context: ApplicationContext = {
globalResourceIds: new Set(globalResources.map((item) => item.id.toPrimitive())),
configurations: rows.map((row) =>
this.mapper.toDomain(
row,
revisions.filter((r) => r.configurationId === row.id)
)
),
profiles: profiles.map((row) =>
new WorkProfilePersistenceMapper().toDomain(
row,
profileRevisions.filter((r) => r.profileId === row.id)
)
),
people: people.map((item) => ({
id: profileIdentity(item.id),
active: Boolean(item.active),
})),
assets: await this.dependencies.assets(transaction).list(company),
resources: [
...(await this.dependencies.resources(transaction).list(company)),
...globalResources,
],
actors: new Map(actors.map((actor) => [actor.id, actor.name])),
};
if (
context.profiles.some(
(profile) =>
!(
context.people.some((person) => person.id.equals(profile.values.personId)) &&
context.assets.some((asset) => asset.id.equals(profile.values.assetId))
)
) ||
context.configurations.some((mapping) => !applicationReferences(mapping.values, context))
) {
throw new Error("Ámbito de aplicación persistido inconsistente.");
}
return Result.ok(context);
}
get(company: UniqueID) {
return this.database.transaction(async (transaction) => {
const session = await this.dependencies.references(transaction, company, { write: false });
return session.isSuccess
? this.load(company, transaction, false, session.data)
: Result.fail(new ApplicationConfigurationError(session.error.code));
});
}
remove(
company: UniqueID,
id: UniqueID,
client: ResourceClientCapabilities
): Promise<Result<void, ApplicationConfigurationError>> {
return this.database.transaction(async (transaction) => {
const session = await this.dependencies.references(transaction, company, { write: true });
if (!session.isSuccess) {
return Result.fail(new ApplicationConfigurationError(session.error.code));
}
const replacements = { companyId: company.toPrimitive(), id: id.toPrimitive() };
await this.database.query("SELECT id FROM companies WHERE id=:companyId FOR UPDATE", {
replacements,
transaction,
type: QueryTypes.SELECT,
});
const rows = await this.database.query<{ id: string; resourceId: string }>(
"SELECT id,resourceId FROM application_configurations WHERE companyId=:companyId AND id=:id FOR UPDATE",
{ replacements, transaction, type: QueryTypes.SELECT }
);
if (!rows.length) {
return Result.fail(
new ApplicationConfigurationError("APPLICATION_CONFIGURATION_NOT_FOUND")
);
}
const reference = await session.data.resolve({
purpose: "application-configuration",
ids: rows.map((row) => row.resourceId),
});
if (
!client.globalReferences &&
reference.items.some(
(item) => item.status === "resolved" && item.reference.scope === "global"
)
) {
return Result.fail(new ApplicationConfigurationError("RESOURCE_CLIENT_UPGRADE_REQUIRED"));
}
await this.database.query(
"DELETE FROM application_configuration_revisions WHERE configurationId=:id",
{ replacements, transaction }
);
await this.database.query(
"DELETE FROM application_configurations WHERE companyId=:companyId AND id=:id",
{ replacements, transaction }
);
return Result.ok(undefined);
});
}
private async persist(
candidate: ApplicationConfiguration,
previous: ApplicationConfiguration | undefined,
transaction: Transaction
) {
if (previous !== candidate) {
const record = this.mapper.toPersistence(candidate);
await this.database.query(
"INSERT INTO application_configurations (id,companyId,profileId,resourceId,delivery,version,state,notes,currentKey) VALUES (:id,:companyId,:profileId,:resourceId,:delivery,:version,:state,:notes,:currentKey) ON DUPLICATE KEY UPDATE delivery=VALUES(delivery),version=VALUES(version),state=VALUES(state),notes=VALUES(notes),currentKey=VALUES(currentKey)",
{ replacements: record.root, transaction }
);
const stored = new Set(previous?.history.map((r) => r.id.toPrimitive()));
for (const revision of record.revisions.filter((r) => !stored.has(r.id))) {
await this.database.query(
"INSERT INTO application_configuration_revisions (id,configurationId,position,actorId,recordedAt,profileId,resourceId,delivery,version,state,notes) VALUES (:id,:configurationId,:position,:actorId,:recordedAt,:profileId,:resourceId,:delivery,:version,:state,:notes)",
{ replacements: revision, transaction }
);
}
}
}
private clientSupports(
context: ApplicationContext,
request: Parameters<IApplicationConfigurationRepository["save"]>[2]
) {
if (request.globalReferences) {
return true;
}
const previous = context.configurations.find(
(item) => item.id.toPrimitive() === request.id?.toLowerCase()
);
return !(
context.globalResourceIds.has(request.resourceId.toLowerCase()) ||
(previous && context.globalResourceIds.has(previous.values.resourceId.toPrimitive()))
);
}
private async validateAssociation(
session: IResourceReferenceSession,
candidate: ApplicationConfiguration,
previous: ApplicationConfiguration | undefined
) {
if (!requiresAvailableApplication(candidate.values, previous)) {
return Result.ok(undefined);
}
const eligible = await session.validateNewAssociation(
candidate.values.resourceId.toPrimitive(),
"application-configuration"
);
if (eligible.isSuccess) {
return Result.ok(undefined);
}
switch (eligible.error.code) {
case "RESOURCE_REFERENCE_NOT_FOUND":
return Result.fail(new ApplicationConfigurationError("APPLICATION_REFERENCE_NOT_FOUND"));
case "RESOURCE_REFERENCE_UNAVAILABLE":
return Result.fail(new ApplicationConfigurationError("APPLICATION_REFERENCE_UNAVAILABLE"));
default:
return Result.fail(new ApplicationConfigurationError(eligible.error.code));
}
}
save(
company: UniqueID,
action: Parameters<IApplicationConfigurationRepository["save"]>[1],
request: Parameters<IApplicationConfigurationRepository["save"]>[2]
) {
return this.database.transaction(async (transaction) => {
const session = await this.dependencies.references(transaction, company, { write: true });
if (!session.isSuccess) {
return Result.fail(new ApplicationConfigurationError(session.error.code));
}
const loaded = await this.load(company, transaction, true, session.data, request.resourceId);
if (!loaded.isSuccess) {
return loaded;
}
if (!this.clientSupports(loaded.data, request)) {
return Result.fail(new ApplicationConfigurationError("RESOURCE_CLIENT_UPGRADE_REQUIRED"));
}
const candidate = action(loaded.data);
if (!candidate.isSuccess) {
return candidate;
}
const previous = loaded.data.configurations.find((item) => item.id.equals(candidate.data.id));
const eligible = await this.validateAssociation(session.data, candidate.data, previous);
if (!eligible.isSuccess) {
return eligible;
}
await this.persist(candidate.data, previous, transaction);
return Result.ok({ configuration: candidate.data, context: loaded.data });
});
}
}