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> { 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( "SELECT * FROM application_configurations WHERE companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const revisions = await this.database.query( "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( "SELECT * FROM work_profiles WHERE companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const profileRevisions = await this.database.query( "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> { 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[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[1], request: Parameters[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 }); }); } }