diff --git a/clients/internal/manual/client.go b/clients/internal/manual/client.go
index 845dbdd65..3a8ef3025 100644
--- a/clients/internal/manual/client.go
+++ b/clients/internal/manual/client.go
@@ -108,7 +108,73 @@ func (m ManualClient) SyncAllBundle(db models.DatabaseRepository, bundleFile *os
continue
}
}
+ case pkg.DocumentTypeFhirList:
+ list401Data := fhir401.List{}
+ err := base.UnmarshalJson(bundleFile, &list401Data)
+ if err != nil {
+ return summary, fmt.Errorf("an error occurred while parsing 4.0.1 list: %w", err)
+ }
+
+ //find the encounter reference
+ if list401Data.Encounter == nil || list401Data.Encounter.Reference == nil || *list401Data.Encounter.Reference == "" {
+ return summary, fmt.Errorf("list does not contain an encounter reference")
+ }
+ var encounterSourceId, encounterResourceType, encounterResourceId string
+ encounterSourceId, encounterResourceType, encounterResourceId, err = pkg.ParseReferenceUri(list401Data.Encounter.Reference)
+ if err != nil {
+ referenceParts := strings.Split(*list401Data.Encounter.Reference, "/")
+
+ if len(referenceParts) == 2 {
+ encounterSourceId = m.SourceCredential.GetSourceId()
+ encounterResourceType = referenceParts[0]
+ encounterResourceId = referenceParts[1]
+ } else {
+ return summary, fmt.Errorf("an error occurred while parsing encounter reference: %w", err)
+ }
+ }
+
+ //loop though all the contained resources, and process them
+ for ndx, contained := range list401Data.Contained {
+ //attempt to parse the FHIR Resource
+ apiModel, err := wrapFhir401JsonToRawResource(contained)
+ if err != nil {
+ syncErrors[fmt.Sprintf("contained index: %d", ndx)] = err
+ continue
+ }
+
+ rawResourceList = append(rawResourceList, *apiModel)
+ err = client.ProcessResource(db, *apiModel, lookupResourceReferences, internalFragmentReferenceLookup, &summary)
+ if err != nil {
+ syncErrors[apiModel.SourceResourceType] = err
+ continue
+ }
+ }
+
+ //loop though all the entries (references), and process them
+ for ndx, entry := range list401Data.Entry {
+ //parse the Reference
+
+ entrySourceId, entryResourceType, entryResourceId, err := pkg.ParseReferenceUri(entry.Item.Reference)
+ if err != nil {
+ syncErrors[fmt.Sprintf("reference (%s)", ndx)] = err
+ continue
+ }
+ err = db.UpsertRawResourceAssociation(m.Context, encounterSourceId, encounterResourceType, encounterResourceId, entrySourceId, entryResourceType, entryResourceId)
+ if err != nil {
+ syncErrors[fmt.Sprintf("reference association (%s)", ndx)] = err
+ continue
+ }
+ }
+
+ summary.TotalResources = len(rawResourceList)
+ m.Logger.Infof("Completed document processing: %d resources", summary.TotalResources)
+
+ if len(syncErrors) > 0 {
+ //TODO: ignore errors.
+ m.Logger.Errorf("%d error(s) occurred during sync. \n %v", len(syncErrors), syncErrors)
+ }
+ return summary, nil
case pkg.DocumentTypeFhirNDJSON:
d := json.NewDecoder(bundleFile)
counter := 0
@@ -125,32 +191,22 @@ func (m ManualClient) SyncAllBundle(db models.DatabaseRepository, bundleFile *os
}
}
- resourceObj, err := fhir401utils.MapToResource(resource, false)
+ //attempt to parse the FHIR Resource
+ apiModel, err := wrapFhir401JsonToRawResource(resource)
if err != nil {
syncErrors[fmt.Sprintf("index: %d", counter)] = err
continue
}
- resourceObjTyped := resourceObj.(models.ResourceInterface)
- resourceType, resourceId := resourceObjTyped.ResourceRef()
- if resourceId == nil {
- syncErrors[fmt.Sprintf("%s (index: %d)", resourceType, counter)] = fmt.Errorf("resource ID is nil, skipping")
- continue
- }
-
- apiModel := models.RawResourceFhir{
- SourceResourceID: *resourceId,
- SourceResourceType: resourceType,
- ResourceRaw: resource,
- }
- rawResourceList = append(rawResourceList, apiModel)
- err = client.ProcessResource(db, apiModel, lookupResourceReferences, internalFragmentReferenceLookup, &summary)
+ rawResourceList = append(rawResourceList, *apiModel)
+ err = client.ProcessResource(db, *apiModel, lookupResourceReferences, internalFragmentReferenceLookup, &summary)
if err != nil {
syncErrors[apiModel.SourceResourceType] = err
continue
}
counter += 1
+
}
}
summary.TotalResources = len(rawResourceList)
@@ -192,3 +248,23 @@ func GetSourceClientManual(env pkg.FastenLighthouseEnvType, ctx context.Context,
func cleanPatientIdPrefix(patientId string) string {
return strings.TrimLeft(patientId, "Patient/")
}
+
+func wrapFhir401JsonToRawResource(jsonResource json.RawMessage) (*models.RawResourceFhir, error) {
+ resourceObj, err := fhir401utils.MapToResource(jsonResource, false)
+ if err != nil {
+ return nil, err
+ }
+
+ resourceObjTyped := resourceObj.(models.ResourceInterface)
+ resourceType, resourceId := resourceObjTyped.ResourceRef()
+ if resourceId == nil {
+ return nil, fmt.Errorf("resource ID is nil, skipping")
+ }
+
+ return &models.RawResourceFhir{
+ SourceResourceID: *resourceId,
+ SourceResourceType: resourceType,
+ ResourceRaw: jsonResource,
+ }, nil
+
+}
diff --git a/clients/internal/manual/testdata/fixtures/401-R4/list/family-history.json b/clients/internal/manual/testdata/fixtures/401-R4/list/family-history.json
new file mode 100644
index 000000000..267bba798
--- /dev/null
+++ b/clients/internal/manual/testdata/fixtures/401-R4/list/family-history.json
@@ -0,0 +1,298 @@
+{
+ "resourceType": "List",
+ "id": "genetic",
+ "text": {
+ "status": "generated",
+ "div": "
Generated Narrative with Details
id: genetic
contained: , , , , , , ,
status: current
mode: snapshot
code: History of family member diseases (Details : {LOINC code '8670-2' = 'History of family member diseases', given as 'History of family member diseases'})
subject: Peter Patient
entry
item: id: 1; status: completed; name: Dave; father (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'FTH' = 'father', given as 'father'})
entry
item: id: 2; status: completed; maternal grandfather (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'MGRFTH' = 'maternal grandfather', given as 'maternal grandfather'})
entry
item: id: 3; status: completed; mother (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'MTH' = 'mother', given as 'mother'})
entry
item: id: 4; status: completed; paternal grandmother (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'PGRMTH' = 'paternal grandmother', given as 'paternal grandmother'})
entry
item: id: 5; status: completed; name: Eve; paternal aunt (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'PAUNT' = 'paternal aunt', given as 'paternal aunt'})
entry
item: id: 6; status: completed; maternal uncle (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'MUNCLE' = 'maternal uncle', given as 'maternal uncle'})
entry
item: id: 7; status: completed; natural sister (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'NSIS' = 'natural sister', given as 'natural sister'})
entry
item: id: 8; status: completed; name: Alice; maternal cousin (Details : {http://terminology.hl7.org/CodeSystem/v3-RoleCode code 'MCOUSN' = 'maternal cousin', given as 'maternal cousin'})
entry
item: Family history of cancer of colon
"
+ },
+ "contained": [
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "1",
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/family-member-history-genetics-parent",
+ "extension": [
+ {
+ "url": "type",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "FTH",
+ "display": "father"
+ }
+ ]
+ }
+ },
+ {
+ "url": "reference",
+ "valueReference": {
+ "reference": "#2",
+ "display": "maternal grandfather"
+ }
+ }
+ ]
+ }
+ ],
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "name": "Dave",
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "FTH",
+ "display": "father"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "2",
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "MGRFTH",
+ "display": "maternal grandfather"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "3",
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/family-member-history-genetics-parent",
+ "extension": [
+ {
+ "url": "type",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "MTH",
+ "display": "mother"
+ }
+ ]
+ }
+ },
+ {
+ "url": "reference",
+ "valueReference": {
+ "reference": "#2",
+ "display": "maternal grandfather"
+ }
+ }
+ ]
+ }
+ ],
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "MTH",
+ "display": "mother"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "4",
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "PGRMTH",
+ "display": "paternal grandmother"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "5",
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "name": "Eve",
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "PAUNT",
+ "display": "paternal aunt"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "6",
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "MUNCLE",
+ "display": "maternal uncle"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "7",
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "NSIS",
+ "display": "natural sister"
+ }
+ ]
+ }
+ },
+ {
+ "resourceType": "FamilyMemberHistory",
+ "id": "8",
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/family-member-history-genetics-parent",
+ "extension": [
+ {
+ "url": "type",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "MTH",
+ "display": "mother"
+ }
+ ]
+ }
+ },
+ {
+ "url": "reference",
+ "valueReference": {
+ "reference": "#2",
+ "display": "maternal grandfather"
+ }
+ }
+ ]
+ }
+ ],
+ "status": "completed",
+ "patient": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "name": "Alice",
+ "relationship": {
+ "coding": [
+ {
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode",
+ "code": "MCOUSN",
+ "display": "maternal cousin"
+ }
+ ]
+ }
+ }
+ ],
+ "status": "current",
+ "mode": "snapshot",
+ "code": {
+ "coding": [
+ {
+ "system": "http://loinc.org",
+ "code": "8670-2",
+ "display": "History of family member diseases"
+ }
+ ]
+ },
+ "subject": {
+ "reference": "Patient/example",
+ "display": "Peter Patient"
+ },
+ "entry": [
+ {
+ "item": {
+ "reference": "#1"
+ }
+ },
+ {
+ "item": {
+ "reference": "#2"
+ }
+ },
+ {
+ "item": {
+ "reference": "#3"
+ }
+ },
+ {
+ "item": {
+ "reference": "#4"
+ }
+ },
+ {
+ "item": {
+ "reference": "#5"
+ }
+ },
+ {
+ "item": {
+ "reference": "#6"
+ }
+ },
+ {
+ "item": {
+ "reference": "#7"
+ }
+ },
+ {
+ "item": {
+ "reference": "#8"
+ }
+ },
+ {
+ "item": {
+ "reference": "Condition/family-history",
+ "display": "Family history of cancer of colon"
+ }
+ }
+ ]
+}
diff --git a/clients/internal/manual/utils.go b/clients/internal/manual/utils.go
index 596b664f7..60a4988fb 100644
--- a/clients/internal/manual/utils.go
+++ b/clients/internal/manual/utils.go
@@ -87,6 +87,8 @@ func GetFileDocumentType(file *os.File) (pkg.DocumentType, error) {
primaryResourceType, _ := parsedResource.ResourceRef()
if primaryResourceType == "Bundle" {
return pkg.DocumentTypeFhirBundle, nil
+ } else if primaryResourceType == "List" {
+ return pkg.DocumentTypeFhirList, nil
} else {
return pkg.DocumentType("unknown"), fmt.Errorf("unknown FHIR Resource collection type: %s", primaryResourceType)
}
diff --git a/clients/internal/manual/utils_test.go b/clients/internal/manual/utils_test.go
index 91548e18e..8e280c18a 100644
--- a/clients/internal/manual/utils_test.go
+++ b/clients/internal/manual/utils_test.go
@@ -15,6 +15,7 @@ func TestGetFileDocumentType(t *testing.T) {
expectedDocumentType pkg.DocumentType // expected result
}{
{"testdata/fixtures/401-R4/bundle/synthea_Tania553_Harris789_545c2380-b77f-4919-ab5d-0f615f877250.json", true, pkg.DocumentTypeFhirBundle},
+ {"testdata/fixtures/401-R4/list/family-history.json", true, pkg.DocumentTypeFhirList},
{"testdata/fixtures/401-R4/ccda/MaryGrant-ClinicalSummary.xml", true, pkg.DocumentTypeCCDA},
{"testdata/fixtures/401-R4/international-patient-summary/IPS-bundle-01.json", true, pkg.DocumentTypeFhirBundle},
{"testdata/fixtures/401-R4/phr-ndjson-jsonl/JohnDoe.phr", true, pkg.DocumentTypeFhirNDJSON},
diff --git a/clients/models/database_repository.go b/clients/models/database_repository.go
index eb6dfc882..6ba74bd0e 100644
--- a/clients/models/database_repository.go
+++ b/clients/models/database_repository.go
@@ -7,5 +7,14 @@ import (
//go:generate mockgen -source=database_repository.go -destination=mock/mock_database_repository.go
type DatabaseRepository interface {
UpsertRawResource(ctx context.Context, sourceCredentials SourceCredential, rawResource RawResourceFhir) (bool, error)
+ UpsertRawResourceAssociation(
+ ctx context.Context,
+ sourceId string,
+ sourceResourceType string,
+ sourceResourceId string,
+ targetSourceId string,
+ targetResourceType string,
+ targetResourceId string,
+ ) error
BackgroundJobCheckpoint(ctx context.Context, checkpointData map[string]interface{}, errorData map[string]interface{})
}
diff --git a/clients/models/mock/mock_database_repository.go b/clients/models/mock/mock_database_repository.go
index f90bb57f9..680912f1d 100644
--- a/clients/models/mock/mock_database_repository.go
+++ b/clients/models/mock/mock_database_repository.go
@@ -61,3 +61,17 @@ func (mr *MockDatabaseRepositoryMockRecorder) UpsertRawResource(ctx, sourceCrede
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRawResource", reflect.TypeOf((*MockDatabaseRepository)(nil).UpsertRawResource), ctx, sourceCredentials, rawResource)
}
+
+// UpsertRawResourceAssociation mocks base method.
+func (m *MockDatabaseRepository) UpsertRawResourceAssociation(ctx context.Context, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpsertRawResourceAssociation", ctx, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpsertRawResourceAssociation indicates an expected call of UpsertRawResourceAssociation.
+func (mr *MockDatabaseRepositoryMockRecorder) UpsertRawResourceAssociation(ctx, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRawResourceAssociation", reflect.TypeOf((*MockDatabaseRepository)(nil).UpsertRawResourceAssociation), ctx, sourceId, sourceResourceType, sourceResourceId, targetSourceId, targetResourceType, targetResourceId)
+}
diff --git a/clients/models/mock/mock_source_credential.go b/clients/models/mock/mock_source_credential.go
index 3223eac1d..cf5200063 100644
--- a/clients/models/mock/mock_source_credential.go
+++ b/clients/models/mock/mock_source_credential.go
@@ -146,6 +146,20 @@ func (mr *MockSourceCredentialMockRecorder) GetRefreshToken() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshToken", reflect.TypeOf((*MockSourceCredential)(nil).GetRefreshToken))
}
+// GetSourceId mocks base method.
+func (m *MockSourceCredential) GetSourceId() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetSourceId")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// GetSourceId indicates an expected call of GetSourceId.
+func (mr *MockSourceCredentialMockRecorder) GetSourceId() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSourceId", reflect.TypeOf((*MockSourceCredential)(nil).GetSourceId))
+}
+
// GetSourceType mocks base method.
func (m *MockSourceCredential) GetSourceType() pkg.SourceType {
m.ctrl.T.Helper()
diff --git a/clients/models/source_credential.go b/clients/models/source_credential.go
index 222325678..e987209ec 100644
--- a/clients/models/source_credential.go
+++ b/clients/models/source_credential.go
@@ -2,9 +2,11 @@ package models
import "github.com/fastenhealth/fasten-sources/pkg"
+// this is actually an interface to a pointer receiver
+//
//go:generate mockgen -source=source_credential.go -destination=mock/mock_source_credential.go
-//this is actually an interface to a pointer receiver
type SourceCredential interface {
+ GetSourceId() string
GetSourceType() pkg.SourceType
GetClientId() string
GetPatientId() string
diff --git a/pkg/document_type.go b/pkg/document_type.go
index 36a41fe8a..706a17822 100644
--- a/pkg/document_type.go
+++ b/pkg/document_type.go
@@ -5,5 +5,6 @@ type DocumentType string
const (
DocumentTypeCCDA DocumentType = "CCDA"
DocumentTypeFhirBundle DocumentType = "FHIR_BUNDLE"
+ DocumentTypeFhirList DocumentType = "FHIR_LIST"
DocumentTypeFhirNDJSON DocumentType = "NDJSON"
)
diff --git a/pkg/resource_uri.go b/pkg/resource_uri.go
new file mode 100644
index 000000000..2f6ca2dd3
--- /dev/null
+++ b/pkg/resource_uri.go
@@ -0,0 +1,32 @@
+package pkg
+
+import (
+ "fmt"
+ "strings"
+)
+
+const FASTENHEALTH_URN_PREFIX = "urn:fastenhealth-fhir:"
+
+func ParseReferenceUri(referenceUri *string) (string, string, string, error) {
+ if referenceUri == nil || *referenceUri == "" {
+ return "", "", "", fmt.Errorf("reference cannot be empty nil")
+ }
+ if strings.HasPrefix(*referenceUri, FASTENHEALTH_URN_PREFIX) {
+ //parse referenceUri into sourceId, resourceType, resourceId
+ originalReference := strings.TrimPrefix(*referenceUri, FASTENHEALTH_URN_PREFIX)
+ urnParts := strings.Split(originalReference, ":")
+ if len(urnParts) != 2 {
+ return "", "", "", fmt.Errorf("invalid reference (%s), must have 2 parts", *referenceUri)
+ }
+ sourceId := urnParts[0]
+ resourceParts := strings.Split(urnParts[1], "/")
+ if len(resourceParts) != 2 {
+ return "", "", "", fmt.Errorf("invalid resource id (%s), must have 2 parts", *referenceUri)
+ }
+
+ return sourceId, resourceParts[0], resourceParts[1], nil
+
+ } else {
+ return "", "", "", fmt.Errorf("invalid reference (%s), must start with `%s`", *referenceUri, FASTENHEALTH_URN_PREFIX)
+ }
+}