FHIR, API, datamodel

This commit is contained in:
colinislit
2025-11-21 22:32:29 +01:00
parent 5f7ef0801d
commit d5c6cf208b
39 changed files with 17974 additions and 1004 deletions

8
lib/fhir/index.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* FHIR Library
* Main entry point for FHIR functionality
*/
export * from './types';
export * from './transforms';
export * from './utils';

View File

@@ -0,0 +1,7 @@
/**
* FHIR Transforms
* Export all transform functions
*/
export * from './patient';
export * from './practitioner';

View File

@@ -0,0 +1,198 @@
/**
* FHIR Patient Transforms
* Convert between Database rows and FHIR Patient resources
*/
import type { Tables, TablesInsert } from '../../database.types';
import type { FHIRPatient } from '../types';
type PatientRow = Tables<'patients'>;
type PatientInsert = TablesInsert<'patients'>;
/**
* Transform database Patient row to FHIR Patient resource
*/
export function dbPatientToFHIR(row: PatientRow): FHIRPatient {
return {
resourceType: 'Patient',
id: row.id,
// Identifiers (BSN, client number)
identifier: [
{
system: 'http://fhir.nl/fhir/NamingSystem/bsn',
value: row.identifier_bsn || undefined,
use: 'official' as const,
},
row.identifier_client_number
? {
system: 'urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6',
value: row.identifier_client_number,
use: 'usual' as const,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Active status
active: row.active ?? true,
// Name
name: [
{
use: (row.name_use as any) || 'official',
family: row.name_family,
given: row.name_given,
prefix: row.name_prefix ? [row.name_prefix] : undefined,
},
],
// Telecom (contact)
telecom: [
row.telecom_phone
? {
system: 'phone' as const,
value: row.telecom_phone,
use: 'mobile' as const,
}
: undefined,
row.telecom_email
? {
system: 'email' as const,
value: row.telecom_email,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Gender
gender: row.gender,
// Birth date
birthDate: row.birth_date,
// Address
address: row.address_line
? [
{
use: 'home',
line: row.address_line,
city: row.address_city || undefined,
postalCode: row.address_postal_code || undefined,
country: row.address_country || 'NL',
},
]
: undefined,
// Contact person (emergency contact)
contact: row.emergency_contact_name
? [
{
relationship: [
{
coding: [
{
system: 'http://terminology.hl7.org/CodeSystem/v2-0131',
code: 'C',
display: row.emergency_contact_relationship || 'Emergency Contact',
},
],
},
],
name: {
text: row.emergency_contact_name,
},
telecom: row.emergency_contact_phone
? [
{
system: 'phone',
value: row.emergency_contact_phone,
},
]
: undefined,
},
]
: undefined,
// General Practitioner
generalPractitioner: row.general_practitioner_name
? [
{
display: row.general_practitioner_name,
identifier: row.general_practitioner_agb
? {
system: 'http://fhir.nl/fhir/NamingSystem/agb-z',
value: row.general_practitioner_agb,
}
: undefined,
},
]
: undefined,
// Meta (timestamps)
meta: {
lastUpdated: row.updated_at || undefined,
},
};
}
/**
* Transform FHIR Patient resource to database insert
*/
export function fhirPatientToDB(fhir: FHIRPatient): PatientInsert {
// Extract BSN from identifiers
const bsn = fhir.identifier?.find(
(i) => i.system === 'http://fhir.nl/fhir/NamingSystem/bsn'
)?.value;
// Extract client number from identifiers
const clientNumber = fhir.identifier?.find(
(i) => i.system === 'urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6'
)?.value;
// Extract name (use first name)
const name = fhir.name?.[0];
// Extract address (use first address)
const address = fhir.address?.[0];
// Extract phone and email from telecom
const phone = fhir.telecom?.find((t) => t.system === 'phone')?.value;
const email = fhir.telecom?.find((t) => t.system === 'email')?.value;
// Extract emergency contact
const emergencyContact = fhir.contact?.[0];
const emergencyContactName = emergencyContact?.name?.text;
const emergencyContactPhone = emergencyContact?.telecom?.find(
(t) => t.system === 'phone'
)?.value;
const emergencyContactRelationship =
emergencyContact?.relationship?.[0]?.coding?.[0]?.display;
// Extract general practitioner
const gp = fhir.generalPractitioner?.[0];
const gpName = gp?.display;
const gpAgb = gp?.identifier?.value;
return {
id: fhir.id,
identifier_bsn: bsn || '999999990', // Default placeholder
identifier_client_number: clientNumber || undefined,
name_family: name?.family || '',
name_given: name?.given || [],
name_prefix: name?.prefix?.[0] || undefined,
name_use: name?.use || 'official',
birth_date: fhir.birthDate || '',
gender: fhir.gender || 'unknown',
telecom_phone: phone || undefined,
telecom_email: email || undefined,
address_line: address?.line || undefined,
address_city: address?.city || undefined,
address_postal_code: address?.postalCode || undefined,
address_country: address?.country || 'NL',
emergency_contact_name: emergencyContactName || undefined,
emergency_contact_phone: emergencyContactPhone || undefined,
emergency_contact_relationship: emergencyContactRelationship || undefined,
general_practitioner_name: gpName || undefined,
general_practitioner_agb: gpAgb || undefined,
active: fhir.active ?? true,
};
}

View File

@@ -0,0 +1,133 @@
/**
* FHIR Practitioner Transforms
* Convert between Database rows and FHIR Practitioner resources
*/
import type { Tables, TablesInsert } from '../../database.types';
import type { FHIRPractitioner } from '../types';
type PractitionerRow = Tables<'practitioners'>;
type PractitionerInsert = TablesInsert<'practitioners'>;
/**
* Transform database Practitioner row to FHIR Practitioner resource
*/
export function dbPractitionerToFHIR(row: PractitionerRow): FHIRPractitioner {
return {
resourceType: 'Practitioner',
id: row.id,
// Identifiers (BIG, AGB)
identifier: [
row.identifier_big
? {
system: 'http://fhir.nl/fhir/NamingSystem/big',
value: row.identifier_big,
use: 'official' as const,
}
: undefined,
row.identifier_agb
? {
system: 'http://fhir.nl/fhir/NamingSystem/agb-z',
value: row.identifier_agb,
use: 'official' as const,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Active status
active: row.active ?? true,
// Name
name: [
{
use: 'official',
family: row.name_family,
given: row.name_given,
prefix: row.name_prefix ? [row.name_prefix] : undefined,
suffix: row.name_suffix ? [row.name_suffix] : undefined,
},
],
// Telecom (contact)
telecom: [
row.telecom_phone
? {
system: 'phone' as const,
value: row.telecom_phone,
use: 'work' as const,
}
: undefined,
row.telecom_email
? {
system: 'email' as const,
value: row.telecom_email,
use: 'work' as const,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Qualifications (professional titles and specializations)
qualification: row.qualification
? row.qualification.map((qual) => ({
code: {
coding: [
{
system: 'http://terminology.hl7.org/CodeSystem/v2-0360',
display: qual,
},
],
text: qual,
},
}))
: undefined,
// Meta (timestamps)
meta: {
lastUpdated: row.updated_at || undefined,
},
};
}
/**
* Transform FHIR Practitioner resource to database insert
*/
export function fhirPractitionerToDB(
fhir: FHIRPractitioner
): PractitionerInsert {
// Extract BIG from identifiers
const big = fhir.identifier?.find(
(i) => i.system === 'http://fhir.nl/fhir/NamingSystem/big'
)?.value;
// Extract AGB from identifiers
const agb = fhir.identifier?.find(
(i) => i.system === 'http://fhir.nl/fhir/NamingSystem/agb-z'
)?.value;
// Extract name (use first name)
const name = fhir.name?.[0];
// Extract phone and email from telecom
const phone = fhir.telecom?.find((t) => t.system === 'phone')?.value;
const email = fhir.telecom?.find((t) => t.system === 'email')?.value;
// Extract qualifications
const qualifications = fhir.qualification?.map(
(q) => q.code.text || q.code.coding?.[0]?.display || ''
).filter(Boolean);
return {
id: fhir.id,
identifier_big: big || undefined,
identifier_agb: agb || undefined,
name_prefix: name?.prefix?.[0] || undefined,
name_given: name?.given || [],
name_family: name?.family || '',
name_suffix: name?.suffix?.[0] || undefined,
qualification: qualifications || undefined,
telecom_phone: phone || undefined,
telecom_email: email || undefined,
active: fhir.active ?? true,
};
}

220
lib/fhir/types/index.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* FHIR R4 Type Definitions
* Simplified types for pragmatic implementation
* Based on: http://hl7.org/fhir/R4/
*/
// ============================================================================
// Common FHIR Types
// ============================================================================
export interface FHIRIdentifier {
system?: string;
value?: string;
use?: 'usual' | 'official' | 'temp' | 'secondary';
}
export interface FHIRHumanName {
use?: 'usual' | 'official' | 'temp' | 'nickname' | 'anonymous' | 'old' | 'maiden';
text?: string;
family?: string;
given?: string[];
prefix?: string[];
suffix?: string[];
}
export interface FHIRContactPoint {
system?: 'phone' | 'fax' | 'email' | 'pager' | 'url' | 'sms' | 'other';
value?: string;
use?: 'home' | 'work' | 'temp' | 'old' | 'mobile';
rank?: number;
}
export interface FHIRAddress {
use?: 'home' | 'work' | 'temp' | 'old' | 'billing';
type?: 'postal' | 'physical' | 'both';
text?: string;
line?: string[];
city?: string;
district?: string;
state?: string;
postalCode?: string;
country?: string;
}
export interface FHIRCodeableConcept {
coding?: Array<{
system?: string;
version?: string;
code?: string;
display?: string;
}>;
text?: string;
}
export interface FHIRReference {
reference?: string;
type?: string;
identifier?: FHIRIdentifier;
display?: string;
}
export interface FHIRMeta {
versionId?: string;
lastUpdated?: string;
source?: string;
profile?: string[];
security?: FHIRCodeableConcept[];
tag?: FHIRCodeableConcept[];
}
// ============================================================================
// FHIR Patient Resource
// ============================================================================
export interface FHIRPatient {
resourceType: 'Patient';
id?: string;
meta?: FHIRMeta;
implicitRules?: string;
language?: string;
// Patient fields
identifier?: FHIRIdentifier[];
active?: boolean;
name?: FHIRHumanName[];
telecom?: FHIRContactPoint[];
gender?: 'male' | 'female' | 'other' | 'unknown';
birthDate?: string;
deceasedBoolean?: boolean;
deceasedDateTime?: string;
address?: FHIRAddress[];
maritalStatus?: FHIRCodeableConcept;
multipleBirthBoolean?: boolean;
multipleBirthInteger?: number;
photo?: Array<{
contentType?: string;
data?: string;
url?: string;
}>;
contact?: Array<{
relationship?: FHIRCodeableConcept[];
name?: FHIRHumanName;
telecom?: FHIRContactPoint[];
address?: FHIRAddress;
gender?: 'male' | 'female' | 'other' | 'unknown';
organization?: FHIRReference;
}>;
communication?: Array<{
language: FHIRCodeableConcept;
preferred?: boolean;
}>;
generalPractitioner?: FHIRReference[];
managingOrganization?: FHIRReference;
link?: Array<{
other: FHIRReference;
type: 'replaced-by' | 'replaces' | 'refer' | 'seealso';
}>;
}
// ============================================================================
// FHIR Practitioner Resource
// ============================================================================
export interface FHIRPractitioner {
resourceType: 'Practitioner';
id?: string;
meta?: FHIRMeta;
implicitRules?: string;
language?: string;
// Practitioner fields
identifier?: FHIRIdentifier[];
active?: boolean;
name?: FHIRHumanName[];
telecom?: FHIRContactPoint[];
address?: FHIRAddress[];
gender?: 'male' | 'female' | 'other' | 'unknown';
birthDate?: string;
photo?: Array<{
contentType?: string;
data?: string;
url?: string;
}>;
qualification?: Array<{
identifier?: FHIRIdentifier[];
code: FHIRCodeableConcept;
period?: {
start?: string;
end?: string;
};
issuer?: FHIRReference;
}>;
communication?: FHIRCodeableConcept[];
}
// ============================================================================
// FHIR Organization Resource
// ============================================================================
export interface FHIROrganization {
resourceType: 'Organization';
id?: string;
meta?: FHIRMeta;
identifier?: FHIRIdentifier[];
active?: boolean;
type?: FHIRCodeableConcept[];
name?: string;
alias?: string[];
telecom?: FHIRContactPoint[];
address?: FHIRAddress[];
partOf?: FHIRReference;
contact?: Array<{
purpose?: FHIRCodeableConcept;
name?: FHIRHumanName;
telecom?: FHIRContactPoint[];
address?: FHIRAddress;
}>;
endpoint?: FHIRReference[];
}
// ============================================================================
// Helper Types
// ============================================================================
/**
* FHIR Bundle for search results
*/
export interface FHIRBundle<T = any> {
resourceType: 'Bundle';
type: 'searchset' | 'collection' | 'transaction' | 'transaction-response' | 'batch' | 'batch-response' | 'history' | 'document' | 'message';
total?: number;
link?: Array<{
relation: string;
url: string;
}>;
entry?: Array<{
fullUrl?: string;
resource?: T;
search?: {
mode?: 'match' | 'include' | 'outcome';
score?: number;
};
}>;
}
/**
* FHIR OperationOutcome for errors
*/
export interface FHIROperationOutcome {
resourceType: 'OperationOutcome';
issue: Array<{
severity: 'fatal' | 'error' | 'warning' | 'information';
code: string;
details?: FHIRCodeableConcept;
diagnostics?: string;
location?: string[];
expression?: string[];
}>;
}

93
lib/fhir/utils.ts Normal file
View File

@@ -0,0 +1,93 @@
/**
* FHIR Utilities
* Helper functions for working with FHIR resources
*/
import type { FHIRReference, FHIROperationOutcome } from './types';
/**
* Extract resource ID from FHIR reference string
* Example: "Patient/123" -> "123"
*/
export function extractIdFromReference(reference?: string): string | null {
if (!reference) return null;
const parts = reference.split('/');
return parts.length === 2 ? parts[1] : null;
}
/**
* Create FHIR reference from resource type and ID
* Example: ("Patient", "123") -> "Patient/123"
*/
export function createReference(
resourceType: string,
id: string,
display?: string
): FHIRReference {
return {
reference: `${resourceType}/${id}`,
type: resourceType,
display,
};
}
/**
* Create FHIR OperationOutcome for errors
*/
export function createOperationOutcome(
severity: 'fatal' | 'error' | 'warning' | 'information',
code: string,
diagnostics: string
): FHIROperationOutcome {
return {
resourceType: 'OperationOutcome',
issue: [
{
severity,
code,
diagnostics,
},
],
};
}
/**
* Validate FHIR resource has required fields
*/
export function validateFHIRResource(
resource: any,
requiredFields: string[]
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
for (const field of requiredFields) {
if (!resource[field]) {
errors.push(`Missing required field: ${field}`);
}
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Format date to FHIR date format (YYYY-MM-DD)
*/
export function toFHIRDate(date: Date | string): string {
if (typeof date === 'string') {
return date.split('T')[0];
}
return date.toISOString().split('T')[0];
}
/**
* Format datetime to FHIR datetime format (ISO 8601)
*/
export function toFHIRDateTime(date: Date | string): string {
if (typeof date === 'string') {
return date;
}
return date.toISOString();
}