Skip to content

Commit 0af13d0

Browse files
committed
validate resources on update
1 parent 229f0d3 commit 0af13d0

File tree

6 files changed

+912
-43
lines changed

6 files changed

+912
-43
lines changed

component/mcsd/component.go

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ var _ component.Lifecycle = &Component{}
2626
var rootDirectoryResourceTypes = []string{"Organization", "Endpoint"}
2727
var defaultDirectoryResourceTypes = []string{"Organization", "Endpoint", "Location", "HealthcareService", "PractitionerRole", "Practitioner"}
2828

29+
// ParentOrganizationMap maps parent organizations (with URA identifier) to their linked child organizations
30+
type ParentOrganizationMap map[*fhir.Organization][]*fhir.Organization
31+
2932
// clockSkewBuffer is subtracted from local time when Bundle meta.lastUpdated is not available
3033
// to account for potential clock differences between client and FHIR server
3134
var clockSkewBuffer = 2 * time.Second
@@ -278,6 +281,13 @@ func (c *Component) updateFromDirectory(ctx context.Context, fhirBaseURLRaw stri
278281
c.processEndpointDeletes(ctx, deduplicatedEntries)
279282
}
280283

284+
// Find parent organizations with URA identifier and all organizations linked to them
285+
// This is used when validating organizations that don't have their own URA identifier
286+
parentOrganizationsMap, err := findParentOrganizationWithURA(ctx, deduplicatedEntries)
287+
if err != nil {
288+
return DirectoryUpdateReport{}, fmt.Errorf("failed to find parent organization: %w", err)
289+
}
290+
281291
// Build transaction with deterministic conditional references
282292
tx := fhir.Bundle{
283293
Type: fhir.BundleTypeTransaction,
@@ -292,7 +302,7 @@ func (c *Component) updateFromDirectory(ctx context.Context, fhirBaseURLRaw stri
292302
continue
293303
}
294304
log.Ctx(ctx).Trace().Str("fhir_server", fhirBaseURLRaw).Msgf("Processing entry: %s", entry.Request.Url)
295-
resourceType, err := buildUpdateTransaction(ctx, &tx, entry, ValidationRules{AllowedResourceTypes: allowedResourceTypes}, allowDiscovery, fhirBaseURLRaw)
305+
resourceType, err := buildUpdateTransaction(ctx, &tx, entry, ValidationRules{AllowedResourceTypes: allowedResourceTypes}, parentOrganizationsMap, allowDiscovery, fhirBaseURLRaw)
296306
if err != nil {
297307
report.Warnings = append(report.Warnings, fmt.Sprintf("entry #%d: %s", i, err.Error()))
298308
continue
@@ -473,3 +483,138 @@ func extractResourceIDFromURL(entry fhir.BundleEntry) string {
473483

474484
return ""
475485
}
486+
487+
// buildOrganizationKey creates a composite key from organization ID and VersionId for deduplication.
488+
// The key includes both the ID and VersionId (if available) to differentiate between versions.
489+
func buildOrganizationKey(org *fhir.Organization) string {
490+
if org == nil || org.Id == nil {
491+
return ""
492+
}
493+
key := *org.Id
494+
if org.Meta != nil && org.Meta.VersionId != nil {
495+
key = key + ":" + *org.Meta.VersionId
496+
}
497+
return key
498+
}
499+
500+
// findOrganizationKeyByID builds a map of organization keys by their ID to help with partOf reference lookups.
501+
// This is needed because partOf references typically only contain the ID, not the VersionId.
502+
func findOrganizationKeyByID(orgMap map[string]*fhir.Organization, orgID string) string {
503+
for key, org := range orgMap {
504+
if org.Id != nil && *org.Id == orgID {
505+
return key
506+
}
507+
}
508+
return ""
509+
}
510+
511+
// findParentOrganizationWithURA finds a parent organization with a URA identifier and all organizations linked to it.
512+
// It loops through ALL entries to find organizations with URA identifiers, then selects the one with the most
513+
// organizations linked to it (directly or indirectly through their partOf chain).
514+
// If no organization with URA is found directly, it traverses each organization's partOf chain to find a parent with URA.
515+
// Returns the parent organization with the most linked organizations and a slice of all organizations whose
516+
// partOf chain leads to the parent.
517+
// Returns (nil, nil, nil) if no organization with URA identifier is found (not an error condition).
518+
func findParentOrganizationWithURA(ctx context.Context, entries []fhir.BundleEntry) (ParentOrganizationMap, error) {
519+
result := make(ParentOrganizationMap)
520+
521+
// Build a map of all organizations for efficient lookup using ID and VersionId as composite key
522+
orgMap := make(map[string]*fhir.Organization)
523+
for _, entry := range entries {
524+
if entry.Resource == nil {
525+
continue
526+
}
527+
var org fhir.Organization
528+
if err := json.Unmarshal(entry.Resource, &org); err != nil {
529+
continue
530+
}
531+
if org.Id != nil {
532+
key := buildOrganizationKey(&org)
533+
orgMap[key] = &org
534+
}
535+
}
536+
537+
// Loop through all organizations to find all with URA identifier
538+
for _, org := range orgMap {
539+
uraIdentifiers := libfhir.FilterIdentifiersBySystem(org.Identifier, coding.URANamingSystem)
540+
if len(uraIdentifiers) > 0 {
541+
// Found an organization with URA, find all organizations linked to it
542+
linkedOrgs := findOrganizationsLinkedToParent(orgMap, org)
543+
result[org] = linkedOrgs
544+
}
545+
}
546+
547+
return result, nil
548+
}
549+
550+
// findOrganizationsLinkedToParent returns all organizations whose partOf chain leads to the parent organization.
551+
// It excludes the parent organization itself from the returned slice.
552+
// Returns an empty slice (not nil) if no organizations are linked to the parent.
553+
func findOrganizationsLinkedToParent(orgMap map[string]*fhir.Organization, parentOrg *fhir.Organization) []*fhir.Organization {
554+
linked := make([]*fhir.Organization, 0)
555+
556+
for _, org := range orgMap {
557+
// Skip the parent organization itself
558+
if org.Id != nil && parentOrg.Id != nil && *org.Id == *parentOrg.Id {
559+
continue
560+
}
561+
562+
// Check if this organization's partOf chain leads to the parent
563+
if organizationLinksToParent(orgMap, org, parentOrg) {
564+
linked = append(linked, org)
565+
}
566+
}
567+
568+
return linked
569+
}
570+
571+
// organizationLinksToParent checks if an organization's partOf chain eventually leads to the parent organization.
572+
// It handles circular references by tracking visited organizations.
573+
func organizationLinksToParent(orgMap map[string]*fhir.Organization, org *fhir.Organization, parentOrg *fhir.Organization) bool {
574+
const maxDepth = 10
575+
visited := make(map[string]bool)
576+
return organizationLinksToParentRecursive(orgMap, org, parentOrg, visited, 0, maxDepth)
577+
}
578+
579+
// organizationLinksToParentRecursive is the recursive helper for organizationLinksToParent.
580+
func organizationLinksToParentRecursive(orgMap map[string]*fhir.Organization, org *fhir.Organization, parentOrg *fhir.Organization, visited map[string]bool, depth int, maxDepth int) bool {
581+
if depth > maxDepth {
582+
return false // Depth exceeded
583+
}
584+
585+
if org.Id != nil {
586+
if visited[*org.Id] {
587+
return false // Circular reference detected
588+
}
589+
visited[*org.Id] = true
590+
591+
// Check if we found the parent
592+
if parentOrg.Id != nil && *org.Id == *parentOrg.Id {
593+
return true
594+
}
595+
}
596+
597+
// Check if this organization has a partOf reference
598+
if org.PartOf == nil || org.PartOf.Reference == nil {
599+
return false // No more parents in the chain
600+
}
601+
602+
// Extract the parent ID from the reference
603+
ref := *org.PartOf.Reference
604+
var parentID string
605+
if strings.Contains(ref, "/") {
606+
parts := strings.Split(ref, "/")
607+
parentID = parts[len(parts)-1]
608+
} else {
609+
parentID = ref
610+
}
611+
612+
// Look up the parent organization
613+
nextOrg, exists := orgMap[parentID]
614+
if !exists {
615+
return false // Parent not found in map
616+
}
617+
618+
// Recursively check the parent's chain
619+
return organizationLinksToParentRecursive(orgMap, nextOrg, parentOrg, visited, depth+1, maxDepth)
620+
}

0 commit comments

Comments
 (0)