diff --git a/tests/ocl/ocl-cm-provider.test.js b/tests/ocl/ocl-cm-provider.test.js index a1e2fc4a..796898c3 100644 --- a/tests/ocl/ocl-cm-provider.test.js +++ b/tests/ocl/ocl-cm-provider.test.js @@ -133,6 +133,38 @@ describe('OCLConceptMapProvider', () => { expect(getMock).toHaveBeenCalledWith('/mappings/map-1/'); }); + it.each([ + ['SAME-AS', 'equivalent'], + ['NARROWER-THAN', 'source-is-narrower-than-target'], + ['BROADER-THAN', 'source-is-broader-than-target'], + ['NOT-SAME-AS', 'not-related-to'], + ['NOT-EQUIVALENT', 'not-related-to'], + ['narrower than', 'source-is-narrower-than-target'], + ['Q-AND-A', 'related-to'], + [undefined, 'related-to'] + ])('should translate OCL map_type %s to FHIR relationship %s', async (mapType, expected) => { + const getMock = jest.fn().mockResolvedValue({ data: makeMapping({ map_type: mapType }) }); + const provider = createProvider({ get: getMock }); + + const result = await provider.fetchConceptMapById('map-1'); + expect(result.jsonObj.group[0].element[0].target[0].relationship).toBe(expected); + }); + + it('should not declare a source/target scope from OCL repo paths', async () => { + const mapping = makeMapping({ + from_collection_url: '/orgs/TestOrg/collections/ColA/', + to_collection_url: '/orgs/TestOrg/collections/ColB/' + }); + const getMock = jest.fn().mockResolvedValue({ data: mapping }); + const provider = createProvider({ get: getMock }); + + const result = await provider.fetchConceptMapById('map-1'); + // $translate resolves a scope as a ValueSet; an OCL path would fail with not-found + expect(result.sourceScope).toBeUndefined(); + expect(result.targetScope).toBeUndefined(); + expect(result.jsonObj.group[0].source).toBe('/orgs/TestOrg/sources/SourceA/'); + }); + it('should return null on HTTP error', async () => { const getMock = jest.fn().mockRejectedValue(new Error('404')); const provider = createProvider({ get: getMock }); @@ -255,6 +287,9 @@ describe('OCLConceptMapProvider', () => { expect(cm.jsonObj.group[0].element).toHaveLength(2); expect(cm.jsonObj.group[0].source).toBe('http://example.org/SourceA'); expect(cm.jsonObj.group[0].target).toBe('http://example.org/SourceB'); + // a CodeSystem canonical is not a ValueSet, so it must not be declared as the scope + expect(cm.sourceScope).toBeUndefined(); + expect(cm.targetScope).toBeUndefined(); }); it('should handle target-system parameter', async () => { diff --git a/tx/ocl/cm-ocl.cjs b/tx/ocl/cm-ocl.cjs index d726ef0a..db528186 100644 --- a/tx/ocl/cm-ocl.cjs +++ b/tx/ocl/cm-ocl.cjs @@ -245,8 +245,8 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider { name: id, title: `${sourceId} to ${targetId}`, status: 'active', - sourceScopeUri: group.sourceCanonical, - targetScopeUri: group.targetCanonical, + // No sourceScope/targetScope: a scope is a ValueSet, and $translate resolves it and + // checks the code against it. The systems are carried by the group instead. group: [{ source: group.sourceCanonical, target: group.targetCanonical, @@ -422,8 +422,10 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider { name: `mapping-${id}`, title: mapping.name || `Mapping ${id}`, status: 'active', - sourceScopeUri: mapping.from_collection_url || mapping.fromCollectionUrl || source, - targetScopeUri: mapping.to_collection_url || mapping.toCollectionUrl || target, + // No sourceScope/targetScope: a scope is a ValueSet canonical, which $translate resolves + // and checks the code against. OCL only offers repo paths here (from_source_url or + // from_collection_url), which resolve to no ValueSet and made every translation fail + // with not-found. The systems are carried by the group instead. group: [ { source: sourceCanonical, @@ -453,15 +455,18 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider { return new ConceptMap(json, 'R5'); } + // OCL map_type expresses "from concept to concept", which lines up + // with the R5 ConceptMapRelationship codes (source target). #toRelationship(mapType) { - switch ((mapType || '').toUpperCase()) { + switch (String(mapType || '').trim().toUpperCase().replace(/[\s_]+/g, '-')) { case 'SAME-AS': return 'equivalent'; case 'NARROWER-THAN': - return 'narrower-than'; + return 'source-is-narrower-than-target'; case 'BROADER-THAN': - return 'broader-than'; + return 'source-is-broader-than-target'; case 'NOT-EQUIVALENT': + case 'NOT-SAME-AS': return 'not-related-to'; default: return 'related-to';