297 lines
11 KiB
TypeScript
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 });
|
|
});
|
|
}
|
|
}
|