FHIR, API, datamodel
This commit is contained in:
8
lib/fhir/index.ts
Normal file
8
lib/fhir/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* FHIR Library
|
||||
* Main entry point for FHIR functionality
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './transforms';
|
||||
export * from './utils';
|
||||
7
lib/fhir/transforms/index.ts
Normal file
7
lib/fhir/transforms/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* FHIR Transforms
|
||||
* Export all transform functions
|
||||
*/
|
||||
|
||||
export * from './patient';
|
||||
export * from './practitioner';
|
||||
198
lib/fhir/transforms/patient.ts
Normal file
198
lib/fhir/transforms/patient.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
133
lib/fhir/transforms/practitioner.ts
Normal file
133
lib/fhir/transforms/practitioner.ts
Normal 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
220
lib/fhir/types/index.ts
Normal 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
93
lib/fhir/utils.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user