mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-07 19:56:17 -04:00
Improved FHIR handling
This commit is contained in:
373
app/fhir/fhir.go
373
app/fhir/fhir.go
@@ -160,18 +160,37 @@ func (p *Practitioner) ResourceType() string { return "Practitioner" }
|
||||
// Clinical resources (R4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ObservationComponent represents a component of an Observation (used for
|
||||
// compound observations like blood pressure with systolic/diastolic values).
|
||||
type ObservationComponent struct {
|
||||
Code CodeableConcept `json:"code"`
|
||||
ValueQuantity *Quantity `json:"valueQuantity,omitempty"`
|
||||
ValueString string `json:"valueString,omitempty"`
|
||||
}
|
||||
|
||||
// ObservationReferenceRange represents a reference range for an Observation.
|
||||
type ObservationReferenceRange struct {
|
||||
Low *Quantity `json:"low,omitempty"`
|
||||
High *Quantity `json:"high,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Type CodeableConcept `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Observation represents a FHIR R4 Observation resource.
|
||||
// https://www.hl7.org/fhir/observation.html
|
||||
type Observation struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Category []CodeableConcept `json:"category"`
|
||||
Code CodeableConcept `json:"code"`
|
||||
Subject Reference `json:"subject"`
|
||||
EffectiveDateTime string `json:"effectiveDateTime"`
|
||||
ValueQuantity *Quantity `json:"valueQuantity,omitempty"`
|
||||
ValueString string `json:"valueString,omitempty"`
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Category []CodeableConcept `json:"category"`
|
||||
Code CodeableConcept `json:"code"`
|
||||
Subject Reference `json:"subject"`
|
||||
EffectiveDateTime string `json:"effectiveDateTime"`
|
||||
ValueQuantity *Quantity `json:"valueQuantity,omitempty"`
|
||||
ValueString string `json:"valueString,omitempty"`
|
||||
Interpretation []CodeableConcept `json:"interpretation,omitempty"`
|
||||
ReferenceRange []ObservationReferenceRange `json:"referenceRange,omitempty"`
|
||||
Component []ObservationComponent `json:"component,omitempty"`
|
||||
}
|
||||
|
||||
func (o *Observation) ResourceType() string { return "Observation" }
|
||||
@@ -225,6 +244,54 @@ type DocumentReference struct {
|
||||
|
||||
func (d *DocumentReference) ResourceType() string { return "DocumentReference" }
|
||||
|
||||
// Dosage represents the FHIR Dosage data type (simplified).
|
||||
type Dosage struct {
|
||||
Text string `json:"text"`
|
||||
Timing interface{} `json:"timing,omitempty"`
|
||||
Route CodeableConcept `json:"route,omitempty"`
|
||||
}
|
||||
|
||||
// DoseAndRate represents a dose and rate in a Dosage.
|
||||
type DoseAndRate struct {
|
||||
DoseQuantity *Quantity `json:"doseQuantity,omitempty"`
|
||||
DoseRange interface{} `json:"doseRange,omitempty"`
|
||||
RateQuantity *Quantity `json:"rateQuantity,omitempty"`
|
||||
RateRange interface{} `json:"rateRange,omitempty"`
|
||||
}
|
||||
|
||||
// MedicationRequest represents a FHIR R4 MedicationRequest resource.
|
||||
// https://www.hl7.org/fhir/medicationrequest.html
|
||||
type MedicationRequest struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Intent string `json:"intent"`
|
||||
MedicationCodeableConcept CodeableConcept `json:"medicationCodeableConcept"`
|
||||
Subject Reference `json:"subject"`
|
||||
AuthoredOn string `json:"authoredOn"`
|
||||
Requester Reference `json:"requester"`
|
||||
DosageInstruction []Dosage `json:"dosageInstruction"`
|
||||
}
|
||||
|
||||
func (m *MedicationRequest) ResourceType() string { return "MedicationRequest" }
|
||||
|
||||
// AllergyIntolerance represents a FHIR R4 AllergyIntolerance resource.
|
||||
// https://www.hl7.org/fhir/allergyintolerance.html
|
||||
type AllergyIntolerance struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
ClinicalStatus CodeableConcept `json:"clinicalStatus"`
|
||||
VerificationStatus CodeableConcept `json:"verificationStatus"`
|
||||
Type string `json:"type"`
|
||||
Category []string `json:"category"`
|
||||
Criticality string `json:"criticality"`
|
||||
Code CodeableConcept `json:"code"`
|
||||
Patient Reference `json:"patient"`
|
||||
RecordedDate string `json:"recordedDate"`
|
||||
}
|
||||
|
||||
func (a *AllergyIntolerance) ResourceType() string { return "AllergyIntolerance" }
|
||||
|
||||
// Quantity represents the FHIR Quantity data type.
|
||||
type Quantity struct {
|
||||
Value float64 `json:"value"`
|
||||
@@ -233,11 +300,18 @@ type Quantity struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// BundleLink represents a link element in a Bundle (used for pagination).
|
||||
type BundleLink struct {
|
||||
Relation string `json:"relation"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Bundle represents a FHIR R4 Bundle resource, used for search results.
|
||||
type Bundle struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
Type string `json:"type"`
|
||||
Total int `json:"total"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
Type string `json:"type"`
|
||||
Total int `json:"total"`
|
||||
Link []BundleLink `json:"link"`
|
||||
Entry []struct {
|
||||
FullUrl string `json:"fullUrl"`
|
||||
Resource json.RawMessage `json:"resource"`
|
||||
@@ -275,8 +349,8 @@ type TokenResponse struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
|
||||
// SMART launch context extensions
|
||||
Patient string `json:"patient"`
|
||||
Encounter string `json:"encounter"`
|
||||
Patient string `json:"patient"`
|
||||
Encounter string `json:"encounter"`
|
||||
// Practitioner holds a bare Practitioner FHIR ID when provided by the EHR.
|
||||
Practitioner string `json:"practitioner"`
|
||||
// User holds a relative FHIR reference to the authenticated user,
|
||||
@@ -340,6 +414,53 @@ func (c *Client) get(path string, dest interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchAllBundlePages follows pagination links in a Bundle and accumulates all entries.
|
||||
// It fetches the initial bundle and then follows 'next' links up to maxPages times.
|
||||
// Returns a slice of raw JSON entries and any error encountered.
|
||||
func (c *Client) fetchAllBundlePages(initialBundle *Bundle, maxPages int) ([]json.RawMessage, error) {
|
||||
if maxPages < 1 {
|
||||
maxPages = 1
|
||||
}
|
||||
|
||||
var allEntries []json.RawMessage
|
||||
for _, entry := range initialBundle.Entry {
|
||||
allEntries = append(allEntries, entry.Resource)
|
||||
}
|
||||
|
||||
currentBundle := initialBundle
|
||||
pageCount := 1
|
||||
|
||||
for pageCount < maxPages {
|
||||
nextURL := ""
|
||||
for _, link := range currentBundle.Link {
|
||||
if link.Relation == "next" {
|
||||
nextURL = link.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nextURL == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// Extract path from absolute URL
|
||||
var nextBundle Bundle
|
||||
if err := c.get(strings.TrimPrefix(nextURL, c.baseURL+"/"), &nextBundle); err != nil {
|
||||
// Don't fail on pagination error; return what we have so far
|
||||
break
|
||||
}
|
||||
|
||||
for _, entry := range nextBundle.Entry {
|
||||
allEntries = append(allEntries, entry.Resource)
|
||||
}
|
||||
|
||||
currentBundle = &nextBundle
|
||||
pageCount++
|
||||
}
|
||||
|
||||
return allEntries, nil
|
||||
}
|
||||
|
||||
// GetPatient fetches a Patient resource by FHIR ID.
|
||||
func (c *Client) GetPatient(id string) (*Patient, error) {
|
||||
var p Patient
|
||||
@@ -359,17 +480,26 @@ func (c *Client) GetPractitioner(id string) (*Practitioner, error) {
|
||||
}
|
||||
|
||||
// GetObservations fetches Observation resources for a specific patient.
|
||||
func (c *Client) GetObservations(patientID string) ([]Observation, error) {
|
||||
// If since is non-empty, only fetches observations modified after that timestamp (RFC3339).
|
||||
func (c *Client) GetObservations(patientID, since string) ([]Observation, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("Observation?patient=%s&_sort=-date", patientID)
|
||||
if since != "" {
|
||||
path += fmt.Sprintf("&_lastUpdated=ge%s", since)
|
||||
}
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := c.fetchAllBundlePages(&bundle, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var observations []Observation
|
||||
for _, entry := range bundle.Entry {
|
||||
for _, entry := range entries {
|
||||
var o Observation
|
||||
if err := json.Unmarshal(entry.Resource, &o); err == nil {
|
||||
if err := json.Unmarshal(entry, &o); err == nil {
|
||||
observations = append(observations, o)
|
||||
}
|
||||
}
|
||||
@@ -377,17 +507,26 @@ func (c *Client) GetObservations(patientID string) ([]Observation, error) {
|
||||
}
|
||||
|
||||
// GetConditions fetches Condition resources for a specific patient.
|
||||
func (c *Client) GetConditions(patientID string) ([]Condition, error) {
|
||||
// If since is non-empty, only fetches conditions modified after that timestamp (RFC3339).
|
||||
func (c *Client) GetConditions(patientID, since string) ([]Condition, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("Condition?patient=%s", patientID)
|
||||
if since != "" {
|
||||
path += fmt.Sprintf("&_lastUpdated=ge%s", since)
|
||||
}
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := c.fetchAllBundlePages(&bundle, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conditions []Condition
|
||||
for _, entry := range bundle.Entry {
|
||||
for _, entry := range entries {
|
||||
var cond Condition
|
||||
if err := json.Unmarshal(entry.Resource, &cond); err == nil {
|
||||
if err := json.Unmarshal(entry, &cond); err == nil {
|
||||
conditions = append(conditions, cond)
|
||||
}
|
||||
}
|
||||
@@ -396,23 +535,86 @@ func (c *Client) GetConditions(patientID string) ([]Condition, error) {
|
||||
|
||||
// GetDocumentReferences fetches DocumentReference resources for a specific patient.
|
||||
// Results are sorted newest-first by date.
|
||||
func (c *Client) GetDocumentReferences(patientID string) ([]DocumentReference, error) {
|
||||
// If since is non-empty, only fetches documents modified after that timestamp (RFC3339).
|
||||
func (c *Client) GetDocumentReferences(patientID, since string) ([]DocumentReference, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("DocumentReference?patient=%s&_sort=-date", patientID)
|
||||
if since != "" {
|
||||
path += fmt.Sprintf("&_lastUpdated=ge%s", since)
|
||||
}
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := c.fetchAllBundlePages(&bundle, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var docs []DocumentReference
|
||||
for _, entry := range bundle.Entry {
|
||||
for _, entry := range entries {
|
||||
var d DocumentReference
|
||||
if err := json.Unmarshal(entry.Resource, &d); err == nil {
|
||||
if err := json.Unmarshal(entry, &d); err == nil {
|
||||
docs = append(docs, d)
|
||||
}
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// GetMedicationRequests fetches MedicationRequest resources for a specific patient.
|
||||
// If since is non-empty, only fetches requests modified after that timestamp (RFC3339).
|
||||
func (c *Client) GetMedicationRequests(patientID, since string) ([]MedicationRequest, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("MedicationRequest?patient=%s&status=active&_sort=-date", patientID)
|
||||
if since != "" {
|
||||
path += fmt.Sprintf("&_lastUpdated=ge%s", since)
|
||||
}
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := c.fetchAllBundlePages(&bundle, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var requests []MedicationRequest
|
||||
for _, entry := range entries {
|
||||
var m MedicationRequest
|
||||
if err := json.Unmarshal(entry, &m); err == nil {
|
||||
requests = append(requests, m)
|
||||
}
|
||||
}
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
// GetAllergyIntolerances fetches AllergyIntolerance resources for a specific patient.
|
||||
// If since is non-empty, only fetches allergies modified after that timestamp (RFC3339).
|
||||
func (c *Client) GetAllergyIntolerances(patientID, since string) ([]AllergyIntolerance, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("AllergyIntolerance?patient=%s&_sort=-date", patientID)
|
||||
if since != "" {
|
||||
path += fmt.Sprintf("&_lastUpdated=ge%s", since)
|
||||
}
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := c.fetchAllBundlePages(&bundle, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allergies []AllergyIntolerance
|
||||
for _, entry := range entries {
|
||||
var a AllergyIntolerance
|
||||
if err := json.Unmarshal(entry, &a); err == nil {
|
||||
allergies = append(allergies, a)
|
||||
}
|
||||
}
|
||||
return allergies, nil
|
||||
}
|
||||
|
||||
// GetSmartConfiguration fetches and parses the SMART discovery document
|
||||
// for this FHIR server.
|
||||
func GetSmartConfiguration(issURL string) (*SmartConfiguration, error) {
|
||||
@@ -552,24 +754,78 @@ func ExtractObservation(o *Observation, patientFHIRID, ehrURL string) *models.Ob
|
||||
coding := firstCoding(o.Code)
|
||||
var qty *float64
|
||||
var unit string
|
||||
var valueStr string
|
||||
|
||||
if o.ValueQuantity != nil {
|
||||
v := o.ValueQuantity.Value
|
||||
qty = &v
|
||||
unit = o.ValueQuantity.Unit
|
||||
valueStr = o.ValueString
|
||||
} else if len(o.Component) > 0 && o.ValueQuantity == nil {
|
||||
// Handle compound observations like blood pressure (systolic/diastolic)
|
||||
// Format: "value1/value2 unit" (e.g., "120/80 mmHg")
|
||||
var values []string
|
||||
var compUnit string
|
||||
for _, comp := range o.Component {
|
||||
if comp.ValueQuantity != nil {
|
||||
values = append(values, fmt.Sprintf("%.0f", comp.ValueQuantity.Value))
|
||||
if compUnit == "" {
|
||||
compUnit = comp.ValueQuantity.Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(values) > 0 {
|
||||
valueStr = strings.Join(values, "/")
|
||||
if compUnit != "" {
|
||||
valueStr += " " + compUnit
|
||||
}
|
||||
unit = compUnit
|
||||
}
|
||||
} else {
|
||||
valueStr = o.ValueString
|
||||
}
|
||||
|
||||
// Extract interpretation (first coding display or code)
|
||||
var interpretation string
|
||||
if len(o.Interpretation) > 0 {
|
||||
interp := firstCoding(o.Interpretation[0])
|
||||
if interp.Display != "" {
|
||||
interpretation = interp.Display
|
||||
} else {
|
||||
interpretation = interp.Code
|
||||
}
|
||||
}
|
||||
|
||||
// Extract reference range (low and high from first range entry)
|
||||
var refRangeLow, refRangeHigh *float64
|
||||
if len(o.ReferenceRange) > 0 {
|
||||
refRange := o.ReferenceRange[0]
|
||||
if refRange.Low != nil {
|
||||
v := refRange.Low.Value
|
||||
refRangeLow = &v
|
||||
}
|
||||
if refRange.High != nil {
|
||||
v := refRange.High.Value
|
||||
refRangeHigh = &v
|
||||
}
|
||||
}
|
||||
|
||||
return &models.Observation{
|
||||
FHIRID: o.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
Status: o.Status,
|
||||
Category: firstCategoryText(o.Category),
|
||||
CodeText: o.Code.Text,
|
||||
CodeSystem: coding.System,
|
||||
CodeCode: coding.Code,
|
||||
EffectiveDate: o.EffectiveDateTime,
|
||||
ValueQuantity: qty,
|
||||
ValueUnit: unit,
|
||||
ValueString: o.ValueString,
|
||||
FHIRID: o.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
Status: o.Status,
|
||||
Category: firstCategoryText(o.Category),
|
||||
CodeText: o.Code.Text,
|
||||
CodeSystem: coding.System,
|
||||
CodeCode: coding.Code,
|
||||
EffectiveDate: o.EffectiveDateTime,
|
||||
ValueQuantity: qty,
|
||||
ValueUnit: unit,
|
||||
ValueString: valueStr,
|
||||
Interpretation: interpretation,
|
||||
ReferenceRangeLow: refRangeLow,
|
||||
ReferenceRangeHigh: refRangeHigh,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,6 +882,53 @@ func ExtractDocumentReference(d *DocumentReference, patientFHIRID, ehrURL string
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractMedicationRequest maps a FHIR MedicationRequest to a models.MedicationRequest ready for upsert.
|
||||
func ExtractMedicationRequest(m *MedicationRequest, patientFHIRID, ehrURL string) *models.MedicationRequest {
|
||||
medCoding := firstCoding(m.MedicationCodeableConcept)
|
||||
var dosageText string
|
||||
if len(m.DosageInstruction) > 0 {
|
||||
dosageText = m.DosageInstruction[0].Text
|
||||
}
|
||||
return &models.MedicationRequest{
|
||||
FHIRID: m.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
Status: m.Status,
|
||||
Intent: m.Intent,
|
||||
MedCodeText: m.MedicationCodeableConcept.Text,
|
||||
MedCodeSystem: medCoding.System,
|
||||
MedCodeCode: medCoding.Code,
|
||||
AuthoredOn: m.AuthoredOn,
|
||||
RequesterDisplay: m.Requester.Display,
|
||||
DosageText: dosageText,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractAllergyIntolerance maps a FHIR AllergyIntolerance to a models.AllergyIntolerance ready for upsert.
|
||||
func ExtractAllergyIntolerance(a *AllergyIntolerance, patientFHIRID, ehrURL string) *models.AllergyIntolerance {
|
||||
codeCoding := firstCoding(a.Code)
|
||||
clinicalStatus := firstCoding(a.ClinicalStatus)
|
||||
verificationStatus := firstCoding(a.VerificationStatus)
|
||||
var category string
|
||||
if len(a.Category) > 0 {
|
||||
category = a.Category[0]
|
||||
}
|
||||
return &models.AllergyIntolerance{
|
||||
FHIRID: a.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
ClinicalStatus: clinicalStatus.Code,
|
||||
VerificationStatus: verificationStatus.Code,
|
||||
Type: a.Type,
|
||||
Category: category,
|
||||
Criticality: a.Criticality,
|
||||
CodeText: a.Code.Text,
|
||||
CodeSystem: codeCoding.System,
|
||||
CodeCode: codeCoding.Code,
|
||||
RecordedDate: a.RecordedDate,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFHIRUserFromIDToken attempts to extract a FHIR resource reference
|
||||
// (e.g. "Practitioner/123" or "Patient/abc") from the id_token's fhirUser claim.
|
||||
// Returns an empty string if the claim is missing or invalid.
|
||||
|
||||
Reference in New Issue
Block a user