eab91174db
Co-Authored-By: Claude <noreply@anthropic.com>
120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
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 加密入 enc;indicator/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),
|
||
};
|
||
}
|
||
}
|