Files
HealthCarePregnant/pcm-platform/backend/src/modules/followup/postgres-followup.repository.ts
T
2026-06-18 09:48:05 +08:00

120 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Pool } from 'pg';
import { FollowupRepository } from './followup.repository';
import { FollowUp, FollowUpOutcome, FollowUpStatus, TargetOperator } from './followup.types';
import { FieldSealer } from '../../common/crypto/field-sealer';
interface SensitiveFollowUp {
targetOperator: TargetOperator;
targetValue: number;
windowDays: number;
evaluatedObservationId: string | null;
}
interface FollowUpRow {
id: string;
disposition_id: string;
patient_id: string;
indicator: string;
status: string;
outcome: string | null;
enc: string;
due_at: Date;
created_at: Date;
evaluated_at: Date | null;
}
const iso = (v: Date | string): string => (v instanceof Date ? v.toISOString() : String(v));
const isoOrNull = (v: Date | string | null): string | null => (v == null ? null : iso(v));
/** PostgreSQL 跟进项仓储(T-D.2)。目标值/复测观测ID 加密入 encindicator/status/due 留列供匹配。 */
export class PostgresFollowupRepository extends FollowupRepository {
constructor(
private readonly pool: Pool,
private readonly sealer: FieldSealer,
) {
super();
}
async save(f: FollowUp): Promise<FollowUp> {
const sensitive: SensitiveFollowUp = {
targetOperator: f.targetOperator,
targetValue: f.targetValue,
windowDays: f.windowDays,
evaluatedObservationId: f.evaluatedObservationId ?? null,
};
await this.pool.query(
`INSERT INTO followups (id, disposition_id, patient_id, indicator, status, outcome, enc, due_at, created_at, evaluated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status, outcome = EXCLUDED.outcome,
enc = EXCLUDED.enc, evaluated_at = EXCLUDED.evaluated_at`,
[
f.id,
f.dispositionId,
f.patientId,
f.indicator,
f.status,
f.outcome ?? null,
this.sealer.seal(sensitive),
f.dueAt,
f.createdAt,
f.evaluatedAt ?? null,
],
);
return f;
}
async findById(id: string): Promise<FollowUp | null> {
const res = await this.pool.query<FollowUpRow>('SELECT * FROM followups WHERE id = $1', [id]);
return res.rows[0] ? this.toDomain(res.rows[0]) : null;
}
async findByPatient(patientId: string): Promise<FollowUp[]> {
const res = await this.pool.query<FollowUpRow>(
'SELECT * FROM followups WHERE patient_id = $1 ORDER BY created_at',
[patientId],
);
return res.rows.map((r) => this.toDomain(r));
}
async findByDisposition(dispositionId: string): Promise<FollowUp[]> {
const res = await this.pool.query<FollowUpRow>(
'SELECT * FROM followups WHERE disposition_id = $1 ORDER BY created_at',
[dispositionId],
);
return res.rows.map((r) => this.toDomain(r));
}
async findAll(): Promise<FollowUp[]> {
const res = await this.pool.query<FollowUpRow>('SELECT * FROM followups ORDER BY created_at DESC');
return res.rows.map((r) => this.toDomain(r));
}
async findPendingByIndicator(patientId: string, indicator: string): Promise<FollowUp[]> {
const res = await this.pool.query<FollowUpRow>(
`SELECT * FROM followups WHERE patient_id = $1 AND indicator = $2 AND status <> 'evaluated' ORDER BY created_at`,
[patientId, indicator],
);
return res.rows.map((r) => this.toDomain(r));
}
private toDomain(row: FollowUpRow): FollowUp {
const s = this.sealer.open<SensitiveFollowUp>(row.enc);
return {
id: row.id,
dispositionId: row.disposition_id,
patientId: row.patient_id,
indicator: row.indicator,
targetOperator: s.targetOperator,
targetValue: s.targetValue,
windowDays: s.windowDays,
dueAt: iso(row.due_at),
status: row.status as FollowUpStatus,
outcome: (row.outcome as FollowUpOutcome | null) ?? null,
evaluatedObservationId: s.evaluatedObservationId,
evaluatedAt: isoOrNull(row.evaluated_at),
createdAt: iso(row.created_at),
};
}
}