diff --git a/apps/server/src/infrastructure/audit/sequelize/sequelize-audit-store.ts b/apps/server/src/infrastructure/audit/sequelize/sequelize-audit-store.ts index 4d5a973f..e3645ff1 100644 --- a/apps/server/src/infrastructure/audit/sequelize/sequelize-audit-store.ts +++ b/apps/server/src/infrastructure/audit/sequelize/sequelize-audit-store.ts @@ -1,8 +1,8 @@ import type { ISequelizeTransactionContext } from "@erp/core/api"; -import type { AuditEntry, AuditRecordOptions, IAuditStore } from "@repo/rdx-audit"; -import { AuditStoreError } from "@repo/rdx-audit"; +import type { AuditEntry, IAuditStore } from "@repo/rdx-audit"; +import { type AuditRecordOptions, AuditStoreError } from "@repo/rdx-audit"; import { Result } from "@repo/rdx-utils"; -import type { ModelStatic } from "sequelize"; +import type { ModelStatic, Transaction } from "sequelize"; import type { AuditChangeModel } from "./audit-change.model"; import type { AuditEventModel } from "./audit-event.model"; @@ -19,8 +19,12 @@ export class SequelizeAuditStore implements IAuditStore { private readonly transactionContext: ISequelizeTransactionContext ) {} - async append(entry: AuditEntry, options?: AuditRecordOptions): Promise> { - const transaction = options?.transaction ?? this.transactionContext.current(); + async append( + entry: AuditEntry, + options?: AuditRecordOptions + ): Promise> { + const transaction = (options?.transaction as Transaction) ?? this.transactionContext.current(); + if (entry.kind === "mutation" && !transaction) { return Result.fail( new AuditStoreError("An active transaction is required for audit mutations.") diff --git a/modules/core/src/api/infrastructure/audit/sequelize/sequelize-audit-store.ts b/modules/core/src/api/infrastructure/audit/sequelize/sequelize-audit-store.ts index 95107d76..a9d0da71 100644 --- a/modules/core/src/api/infrastructure/audit/sequelize/sequelize-audit-store.ts +++ b/modules/core/src/api/infrastructure/audit/sequelize/sequelize-audit-store.ts @@ -1,7 +1,7 @@ -import type { AuditEntry, IAuditStore } from "@repo/rdx-audit"; +import type { AuditEntry, AuditRecordOptions, IAuditStore } from "@repo/rdx-audit"; import { AuditStoreError } from "@repo/rdx-audit"; import { Result } from "@repo/rdx-utils"; -import type { ModelStatic } from "sequelize"; +import type { ModelStatic, Transaction } from "sequelize"; import type { ISequelizeTransactionContext } from "../../persistence/sequelize"; @@ -18,7 +18,7 @@ export class SequelizeAuditStore implements IAuditStore { entry: AuditEntry, options?: AuditRecordOptions ): Promise> { - const transaction = options?.transaction ?? this.transactionContext.current(); + const transaction = (options?.transaction as Transaction) ?? this.transactionContext.current(); if (entry.kind === "mutation" && !transaction) return Result.fail( new AuditStoreError("An active transaction is required for audit mutations.") diff --git a/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts b/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts index 01bc91ea..e3fa6c3c 100644 --- a/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts +++ b/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts @@ -1,11 +1,5 @@ -// core/api/infrastructure/documents/certificates/ -// env-company-signing-context-store.ts - -import type { - ICompanyCertificateContext, - ISigningContextResolver, -} from "@erp/core/api/application"; -import type { ModuleParams } from "@erp/core/api/modules"; +import type { ICompanyCertificateContext, ISigningContextResolver } from "../../../application"; +import type { ModuleParams } from "../../../modules"; import type { ICompanySigningContextRecord } from "./company-signing-context-record.interface"; diff --git a/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts b/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts index 6f550760..bcc718e7 100644 --- a/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts +++ b/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts @@ -1,5 +1,4 @@ -import type { IDocumentSigningService } from "@erp/core/api/application"; -import type { ICompanyCertificateContext } from "@erp/core/api/application/documents/application-models/company-certificate-context.interface"; +import type { ICompanyCertificateContext, IDocumentSigningService } from "../../../application"; import type { RestDocumentSigningConfig } from "./rest-document-signing-config"; import { SimpleCircuitBreaker } from "./simple-circuit-breaker"; diff --git a/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-mapper.interface.ts b/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-mapper.interface.ts index b3d4e3b7..fb206566 100644 --- a/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-mapper.interface.ts +++ b/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-mapper.interface.ts @@ -1,4 +1,4 @@ -import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "@erp/core/api/application"; +import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../../application"; export interface ISequelizeDomainMapper extends DomainMapperWithBulk {} diff --git a/modules/core/src/api/modules/index.ts b/modules/core/src/api/modules/index.ts index 29fb8cbe..3e7770e1 100644 --- a/modules/core/src/api/modules/index.ts +++ b/modules/core/src/api/modules/index.ts @@ -1,4 +1,4 @@ -export * from "./module-server.interface"; export * from "./module-runtime"; +export * from "./module-server.interface"; export * from "./modules-runtime"; export * from "./types"; diff --git a/modules/core/src/api/modules/module-runtime.ts b/modules/core/src/api/modules/module-runtime.ts index 3adfd2b0..41f3579d 100644 --- a/modules/core/src/api/modules/module-runtime.ts +++ b/modules/core/src/api/modules/module-runtime.ts @@ -1,11 +1,11 @@ import type { ILogger } from "@repo/rdx-logger"; -import type { ModelStatic, Sequelize } from "sequelize"; +import type { Model, ModelStatic, Sequelize } from "sequelize"; import type { IModuleServer } from "./module-server.interface"; -import type { ModuleParams, ModuleSetupResult, StartParams, WarmupParams } from "./types"; import type { ModulesRuntime } from "./modules-runtime"; +import type { ModuleParams, ModuleSetupResult, StartParams, WarmupParams } from "./types"; -type ModelInitializer = (database: Sequelize) => ModelStatic; +type ModelInitializer = (database: Sequelize) => ModelStatic; type ModelEntry = { initializer: ModelInitializer; moduleName: string }; const modules = new Map(); @@ -29,13 +29,28 @@ export function registerModels(initializers: ModelInitializer[], moduleName: str } /** Inicializa únicamente infraestructura backend: setup, modelos y warmups. */ -export async function initModules(params: ModuleParams & { logger: ILogger; database: Sequelize; warmup: { timeoutMs: number; strict: boolean }; databaseSyncMode: string }): Promise { +export async function initModules( + params: ModuleParams & { + logger: ILogger; + database: Sequelize; + warmup: { timeoutMs: number; strict: boolean }; + databaseSyncMode: string; + } +): Promise { for (const name of modules.keys()) await setupModule(name, params, []); await initializeModels(params.database, params.databaseSyncMode, params.logger); await warmupModules(params); validateDependencies(); - const customerInvoices = internals.get("customer-invoices") as { proformas: { recurringProformas: ModulesRuntime["customerInvoices"]["recurringProformas"] } } | undefined; - if (!customerInvoices) throw new Error('Module "customer-invoices" did not expose recurring runtime capabilities'); + + const customerInvoices = internals.get("customer-invoices") as + | { + proformas: { recurringProformas: ModulesRuntime["customerInvoices"]["recurringProformas"] }; + } + | undefined; + + if (!customerInvoices) + throw new Error('Module "customer-invoices" did not expose recurring runtime capabilities'); + return { customerInvoices: customerInvoices.proformas.recurringProformas }; } @@ -44,71 +59,228 @@ export async function startModules(params: ModuleParams & { logger: ILogger }): for (const name of setupOrder) { const module = modules.get(name); if (!module?.start) continue; - await module.start({ ...params, getService: makeGetService(name, module), getInternal: makeGetInternal(name), listServices } as StartParams); + await module.start({ + ...params, + getService: makeGetService(name, module), + getInternal: makeGetInternal(name), + listServices, + } as StartParams); } validateDependencies(); } -async function setupModule(name: string, params: ModuleParams & { logger: ILogger }, stack: string[]): Promise { - if (setupDone.has(name)) return; - if (visiting.has(name)) throw new Error(`Cyclic dependency detected: ${[...stack, name].join(" -> ")}`); +async function setupModule( + name: string, + params: ModuleParams & { logger: ILogger }, + stack: string[] +): Promise { + if (setupDone.has(name)) { + return; + } + + if (visiting.has(name)) { + throw new Error(`Cyclic dependency detected: ${[...stack, name].join(" -> ")}`); + } + const module = modules.get(name); - if (!module) throw new Error(`Module "${name}" not found`); + + if (!module) { + throw new Error(`Module "${name}" not found`); + } + visiting.add(name); - for (const dependency of module.dependencies ?? []) await setupModule(dependency, params, [...stack, name]); - const result = (await module.setup?.({ ...params, getService: makeGetService(name, module), listServices })) as ModuleSetupResult | undefined; + for (const dependency of module.dependencies ?? []) { + await setupModule(dependency, params, [...stack, name]); + } + + const result = (await module.setup?.({ + ...params, + getService: makeGetService(name, module), + listServices, + })) as ModuleSetupResult | undefined; + internals.set(name, result?.internal ?? {}); - if (result?.models) registerModels(result.models as ModelInitializer[], name); - for (const [key, service] of Object.entries(result?.services ?? {})) registerService(`${name}:${key}`, service); + + if (result?.models) { + registerModels(result.models as ModelInitializer[], name); + } + + for (const [key, service] of Object.entries(result?.services ?? {})) { + registerService(`${name}:${key}`, service); + } + setupDone.add(name); + setupOrder.push(name); + visiting.delete(name); } -async function initializeModels(database: Sequelize, syncMode: string, logger: ILogger): Promise { - if (modelsInitialized) return; - const registered = new Map(); +async function initializeModels( + database: Sequelize, + syncMode: string, + logger: ILogger +): Promise { + if (modelsInitialized) { + return; + } + + const registered = new Map; moduleName: string }>(); + for (const entry of models) { const model = entry.initializer(database); - if (!model?.name) throw new Error(`Invalid model initializer from "${entry.moduleName}"`); - if (registered.has(model.name)) throw new Error(`Model name collision: "${model.name}"`); + if (!model?.name) { + throw new Error(`Invalid model initializer from "${entry.moduleName}"`); + } + if (registered.has(model.name)) { + throw new Error(`Model name collision: "${model.name}"`); + } registered.set(model.name, { model, moduleName: entry.moduleName }); } + for (const { model } of registered.values()) { - const associate = model as ModelStatic & { associate?: (sequelize: Sequelize) => void }; + const associate = model as ModelStatic & { associate?: (sequelize: Sequelize) => void }; associate.associate?.(database); } - if (syncMode === "alter") await database.sync({ force: false, alter: true }); - if (syncMode === "force") await database.sync({ force: true, alter: false }); - logger.info("Backend module models initialized", { label: "moduleRuntime", modelCount: registered.size }); + + if (syncMode === "alter") { + await database.sync({ force: false, alter: true }); + } + + if (syncMode === "force") { + await database.sync({ force: true, alter: false }); + } + + logger.info("Backend module models initialized", { + label: "moduleRuntime", + modelCount: registered.size, + }); modelsInitialized = true; } -async function warmupModules(params: ModuleParams & { logger: ILogger; warmup: { timeoutMs: number; strict: boolean } }): Promise { +async function warmupModules( + params: ModuleParams & { logger: ILogger; warmup: { timeoutMs: number; strict: boolean } } +): Promise { for (const name of setupOrder) { const module = modules.get(name); if (!module?.warmup) continue; try { - await withTimeout(Promise.resolve(module.warmup({ ...params, getService: makeGetService(name, module), getInternal: makeGetInternal(name), listServices } as WarmupParams)), params.warmup.timeoutMs, `${name}.warmup`); + await withTimeout( + Promise.resolve( + module.warmup({ + ...params, + getService: makeGetService(name, module), + getInternal: makeGetInternal(name), + listServices, + } as WarmupParams) + ), + params.warmup.timeoutMs, + `${name}.warmup` + ); } catch (error) { if (params.warmup.strict) throw error; - params.logger.warn("Module warmup failed but continuing", { label: "moduleRuntime", module: name, error }); + params.logger.warn("Module warmup failed but continuing", { + label: "moduleRuntime", + module: name, + error, + }); } } } -function registerService(name: string, service: unknown): void { if (services[name]) throw new Error(`Service "${name}" already registered`); services[name] = service; } -function makeGetService(moduleName: string, module: IModuleServer) { return (name: string): T => { const [dependency, ...key] = name.split(":"); if (dependency !== "self") { if (!(module.dependencies ?? []).includes(dependency)) throw new Error(`Module "${moduleName}" tried to access service "${name}" without declaring dependency on "${dependency}"`); trackDependency(moduleName, dependency); return getService(name); } return getService(`${moduleName}:${key.join(":")}`); }; } -function makeGetInternal(moduleName: string) { return (requestedModule: string, key?: string): T => { if (requestedModule !== moduleName) throw new Error(`Module "${moduleName}" attempted to access private internal state`); const internal = internals.get(moduleName) ?? {}; return (key ? internal[key] : internal) as T; }; } -function getService(name: string): T { const service = services[name]; if (!service) throw new Error(`Service "${name}" not found`); return service as T; } -function trackDependency(moduleName: string, dependency: string): void { const used = usedDependencies.get(moduleName) ?? new Set(); used.add(dependency); usedDependencies.set(moduleName, used); } -function validateDependencies(): void { for (const [name, module] of modules) { const declared = new Set(module.dependencies ?? []); const used = usedDependencies.get(name) ?? new Set(); const undeclared = [...used].filter((dependency) => !declared.has(dependency)); if (undeclared.length) throw new Error(`Module "${name}" used undeclared dependencies: ${undeclared.join(", ")}`); } } -function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { let timeout: NodeJS.Timeout | undefined; const timeoutPromise = new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms (${label})`)), timeoutMs); }); return Promise.race([promise, timeoutPromise]).finally(() => { if (timeout) clearTimeout(timeout); }) as Promise; } -export function listServices(): string[] { return Object.keys(services); } -export function getServiceScoped(requesterModule: string, allowedDependencies: readonly string[], name: string): T { +function registerService(name: string, service: unknown): void { + if (services[name]) throw new Error(`Service "${name}" already registered`); + services[name] = service; +} + +function makeGetService(moduleName: string, module: IModuleServer) { + return (name: string): T => { + const [dependency, ...key] = name.split(":"); + if (dependency !== "self") { + if (!(module.dependencies ?? []).includes(dependency)) + throw new Error( + `Module "${moduleName}" tried to access service "${name}" without declaring dependency on "${dependency}"` + ); + trackDependency(moduleName, dependency); + return getService(name); + } + return getService(`${moduleName}:${key.join(":")}`); + }; +} + +function makeGetInternal(moduleName: string) { + return (requestedModule: string, key?: string): T => { + if (requestedModule !== moduleName) + throw new Error(`Module "${moduleName}" attempted to access private internal state`); + const internal = internals.get(moduleName) ?? {}; + return (key ? internal[key] : internal) as T; + }; +} + +function getService(name: string): T { + const service = services[name]; + if (!service) throw new Error(`Service "${name}" not found`); + return service as T; +} + +function trackDependency(moduleName: string, dependency: string): void { + const used = usedDependencies.get(moduleName) ?? new Set(); + used.add(dependency); + usedDependencies.set(moduleName, used); +} + +function validateDependencies(): void { + for (const [name, module] of modules) { + const declared = new Set(module.dependencies ?? []); + const used = usedDependencies.get(name) ?? new Set(); + const undeclared = [...used].filter((dependency) => !declared.has(dependency)); + if (undeclared.length) + throw new Error(`Module "${name}" used undeclared dependencies: ${undeclared.join(", ")}`); + } +} + +function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { + let timeout: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error(`Timeout after ${timeoutMs}ms (${label})`)), + timeoutMs + ); + }); + return Promise.race([promise, timeoutPromise]).finally(() => { + if (timeout) clearTimeout(timeout); + }) as Promise; +} + +export function listServices(): string[] { + return Object.keys(services); +} + +export function getServiceScoped( + requesterModule: string, + allowedDependencies: readonly string[], + name: string +): T { const [moduleName, ...key] = name.split(":"); if (moduleName === "self") return getService(`${requesterModule}:${key.join(":")}`); - if (!allowedDependencies.includes(moduleName)) throw new Error(`Module "${requesterModule}" tried to access service "${name}" without declaring dependency on "${moduleName}"`); + if (!allowedDependencies.includes(moduleName)) + throw new Error( + `Module "${requesterModule}" tried to access service "${name}" without declaring dependency on "${moduleName}"` + ); return getService(name); } -export function resetModuleRuntimeForTests(): void { modules.clear(); models.length = 0; setupOrder.length = 0; setupDone.clear(); visiting.clear(); internals.clear(); usedDependencies.clear(); for (const key of Object.keys(services)) delete services[key]; modelsInitialized = false; } + +export function resetModuleRuntimeForTests(): void { + modules.clear(); + models.length = 0; + setupOrder.length = 0; + setupDone.clear(); + visiting.clear(); + internals.clear(); + usedDependencies.clear(); + + for (const key of Object.keys(services)) { + delete services[key]; + } + modelsInitialized = false; +} diff --git a/modules/core/src/api/modules/modules-runtime.ts b/modules/core/src/api/modules/modules-runtime.ts index ece56a0e..abaa7299 100644 --- a/modules/core/src/api/modules/modules-runtime.ts +++ b/modules/core/src/api/modules/modules-runtime.ts @@ -8,10 +8,17 @@ export interface RecurringProformaOccurrenceProcessorCapability { } export interface RecurringProformaDiscoveryCapability { - findDue(dueThrough: Date, limit: number): Promise<{ + findDue( + dueThrough: Date, + limit: number + ): Promise<{ isFailure: boolean; error: Error; - data: readonly { recurringProformaId: UniqueID; companyId: UniqueID; nextOccurrenceDate: Date }[]; + data: readonly { + recurringProformaId: UniqueID; + companyId: UniqueID; + nextOccurrenceDate: Date; + }[]; }>; } diff --git a/modules/core/src/common/helpers/money-dto-helper.ts b/modules/core/src/common/helpers/money-dto-helper.ts index 7371654c..c9f2bf13 100644 --- a/modules/core/src/common/helpers/money-dto-helper.ts +++ b/modules/core/src/common/helpers/money-dto-helper.ts @@ -1,8 +1,6 @@ -import Dinero, { type Currency } from "dinero.js"; - import type { MoneyDTO } from "../dto"; -type DineroPlain = { amount: number; precision: number; currency: string }; +//type DineroPlain = { amount: number; precision: number; currency: string }; const isEmptyMoneyDTO = (dto?: MoneyDTO | null) => !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; @@ -149,40 +147,40 @@ export const MoneyDTOHelper = { /** * Convierte un DTO a una instancia Dinero.js. */ -function dineroFromDTO(dto: MoneyDTO, fallbackCurrency = "EUR"): Dinero.Dinero { +/*function dineroFromDTO(dto: MoneyDTO, fallbackCurrency = "EUR"): Dinero.Dinero { const n = normalizeDTO(dto, fallbackCurrency); return Dinero({ amount: Number.parseInt(n.value, 10), precision: Number.parseInt(n.scale, 10), currency: n.currency_code as Currency, }); -} +}*/ /** * Convierte una instancia Dinero a un DTO. */ -function dtoFromDinero(d: Dinero.Dinero): MoneyDTO { +/*function dtoFromDinero(d: Dinero.Dinero): MoneyDTO { const { amount, precision, currency } = d.toObject() as DineroPlain; return { value: amount.toString(), scale: precision.toString(), currency_code: currency, }; -} +}*/ /** * Suma una lista de MoneyDTO. */ -function sumDTO(list: MoneyDTO[], fallbackCurrency = "EUR"): MoneyDTO { +/*function sumDTO(list: MoneyDTO[], fallbackCurrency = "EUR"): MoneyDTO { if (list.length === 0) return { value: "0", scale: "2", currency_code: fallbackCurrency }; const sum = list.map((x) => dineroFromDTO(x, fallbackCurrency)).reduce((a, b) => a.add(b)); return dtoFromDinero(sum); -} +}*/ /** * Multiplica un MoneyDTO por un número. */ -function multiplyDTO( +/*function multiplyDTO( dto: MoneyDTO, multiplier: number, rounding: Dinero.RoundingMode = "HALF_EVEN", @@ -190,12 +188,12 @@ function multiplyDTO( ): MoneyDTO { const d = dineroFromDTO(dto, fallbackCurrency).multiply(multiplier, rounding); return dtoFromDinero(d); -} +}*/ /** * Calcula un porcentaje de un MoneyDTO. */ -function percentageDTO( +/*function percentageDTO( dto: MoneyDTO, percent: number, rounding: Dinero.RoundingMode = "HALF_EVEN", @@ -203,4 +201,4 @@ function percentageDTO( ): MoneyDTO { const d = dineroFromDTO(dto, fallbackCurrency).percentage(percent, rounding); return dtoFromDinero(d); -} +}*/ diff --git a/packages/rdx-audit/src/index.ts b/packages/rdx-audit/src/index.ts index 7a4d2ba1..a4c760ab 100644 --- a/packages/rdx-audit/src/index.ts +++ b/packages/rdx-audit/src/index.ts @@ -1,5 +1,4 @@ export { Audit, type AuditDependencies } from "./audit"; -export type { AuditArray, AuditObject, AuditScalar, AuditState, AuditValue } from "./audit-state.types"; export type { AuditActor, AuditChange, @@ -11,6 +10,7 @@ export type { AuditMutationEntry, AuditMutationTarget, AuditOperation, + AuditRecordOptions, AuditScope, AuditSource, IAudit, @@ -22,6 +22,13 @@ export type { RecordAuditMutationInput, RecordAuditMutationShortcutInput, } from "./audit.types"; +export type { + AuditArray, + AuditObject, + AuditScalar, + AuditState, + AuditValue, +} from "./audit-state.types"; export { AuditContextUnavailableError, AuditError, diff --git a/packages/rdx-criteria/src/criteria-from-url-converter.ts b/packages/rdx-criteria/src/criteria-from-url-converter.ts index eca80679..5f0fd0bd 100644 --- a/packages/rdx-criteria/src/criteria-from-url-converter.ts +++ b/packages/rdx-criteria/src/criteria-from-url-converter.ts @@ -75,7 +75,7 @@ export class CriteriaFromUrlConverter { }); return Object.values(tempFilters).filter( - (filter) => + (filter): filter is FiltersPrimitives => filter.field !== undefined && filter.operator !== undefined && (filter.value !== undefined || diff --git a/packages/rdx-ddd/src/aggregate-root-repository.interface.ts b/packages/rdx-ddd/src/aggregate-root-repository.interface.ts index cecc1664..d0ca63ad 100644 --- a/packages/rdx-ddd/src/aggregate-root-repository.interface.ts +++ b/packages/rdx-ddd/src/aggregate-root-repository.interface.ts @@ -1 +1,4 @@ -export type IAggregateRootRepository = {}; +// biome-ignore lint/correctness/noUnusedVariables: +export type IAggregateRootRepository = { + // Define the methods for the aggregate root repository +};