134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import { Result } from "@repo/rdx-utils";
|
|
|
|
import type {
|
|
AuditContext,
|
|
AuditEntry,
|
|
AuditEventEntry,
|
|
AuditMutationEntry,
|
|
AuditRecordOptions,
|
|
AuditSource,
|
|
IAudit,
|
|
IAuditContextProvider,
|
|
IAuditStore,
|
|
RecordAuditEventShortcutInput,
|
|
RecordAuditInput,
|
|
RecordAuditMutationShortcutInput,
|
|
} from "./audit.types";
|
|
import { AuditContextUnavailableError, AuditError, AuditStoreError } from "./errors";
|
|
import { diffAuditState } from "./internal/audit-diff";
|
|
import {
|
|
validateAtomicPaths,
|
|
validateContext,
|
|
validateRecordInput,
|
|
validateSource,
|
|
} from "./internal/audit-input-validator";
|
|
import { validateAuditState } from "./internal/audit-state-validator";
|
|
|
|
export interface AuditDependencies {
|
|
readonly source: AuditSource;
|
|
readonly contextProvider: IAuditContextProvider;
|
|
readonly store: IAuditStore;
|
|
readonly generateId?: () => string;
|
|
readonly now?: () => Date;
|
|
}
|
|
|
|
/**
|
|
* Implementación portable append-only. Los shortcuts delegan en `record` para
|
|
* mantener una única ruta de validación, diff y persistencia.
|
|
*/
|
|
export class Audit implements IAudit {
|
|
private readonly generateId: () => string;
|
|
private readonly now: () => Date;
|
|
|
|
constructor(private readonly deps: AuditDependencies) {
|
|
validateSource(deps.source);
|
|
this.generateId = deps.generateId ?? randomUUID;
|
|
this.now = deps.now ?? (() => new Date());
|
|
}
|
|
|
|
async record(
|
|
input: RecordAuditInput,
|
|
options?: AuditRecordOptions
|
|
): Promise<Result<void, AuditError>> {
|
|
try {
|
|
validateRecordInput(input);
|
|
|
|
if (input.kind === "mutation") {
|
|
validateAuditState(input.before, "before");
|
|
validateAuditState(input.after, "after");
|
|
const atomicPaths = validateAtomicPaths(input.atomicPaths);
|
|
const changes = diffAuditState(input.before, input.after, atomicPaths);
|
|
if (changes.length === 0) return Result.ok();
|
|
|
|
const context = this.getContext();
|
|
const entry: AuditMutationEntry = {
|
|
...this.buildBaseEntry(context, input.action),
|
|
kind: "mutation",
|
|
target: input.target,
|
|
changes,
|
|
};
|
|
return await this.append(entry, options);
|
|
}
|
|
|
|
const context = this.getContext();
|
|
const entry: AuditEventEntry = {
|
|
...this.buildBaseEntry(context, input.action),
|
|
kind: "event",
|
|
target: input.target,
|
|
changes: [],
|
|
};
|
|
return await this.append(entry, options);
|
|
} catch (error: unknown) {
|
|
return Result.fail(this.toAuditError(error));
|
|
}
|
|
}
|
|
|
|
async recordEvent(
|
|
input: RecordAuditEventShortcutInput,
|
|
options?: AuditRecordOptions
|
|
): Promise<Result<void, AuditError>> {
|
|
return await this.record({ kind: "event", ...input }, options);
|
|
}
|
|
|
|
async recordMutation(
|
|
input: RecordAuditMutationShortcutInput,
|
|
options?: AuditRecordOptions
|
|
): Promise<Result<void, AuditError>> {
|
|
return await this.record({ kind: "mutation", ...input }, options);
|
|
}
|
|
|
|
private getContext(): AuditContext {
|
|
const context = this.deps.contextProvider.get();
|
|
if (!context) throw new AuditContextUnavailableError();
|
|
validateContext(context);
|
|
return context;
|
|
}
|
|
|
|
private buildBaseEntry(context: AuditContext, action: string) {
|
|
return {
|
|
id: this.generateId(),
|
|
occurredAt: this.now().toISOString(),
|
|
source: this.deps.source,
|
|
operation: context.operation,
|
|
actor: context.actor,
|
|
scope: context.scope,
|
|
action,
|
|
};
|
|
}
|
|
|
|
private async append(
|
|
entry: AuditEntry,
|
|
options?: AuditRecordOptions
|
|
): Promise<Result<void, AuditError>> {
|
|
const result = await this.deps.store.append(entry, options);
|
|
return result.isFailure ? Result.fail(result.error) : Result.ok();
|
|
}
|
|
|
|
private toAuditError(error: unknown): AuditError {
|
|
if (error instanceof AuditError) return error;
|
|
return new AuditStoreError(error instanceof Error ? error.message : "Audit store failed.");
|
|
}
|
|
}
|