Skip to content

Commit

Permalink
Full linked Project ordering in CodeSystem lookup (#4522)
Browse files Browse the repository at this point in the history
* Full linked Project ordering in CodeSystem lookup

* Fully specify lookup order of Terminology Resources in linked projects
  • Loading branch information
mattwiller committed May 14, 2024
1 parent 20db8c7 commit 84b2ad7
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 23 deletions.
4 changes: 2 additions & 2 deletions packages/server/src/fhir/operations/codesystemlookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export async function codeSystemLookupHandler(req: FhirRequest): Promise<FhirRes
if (req.params.id) {
codeSystem = await getAuthenticatedContext().repo.readResource<CodeSystem>('CodeSystem', req.params.id);
} else if (params.system) {
codeSystem = await findTerminologyResource<CodeSystem>('CodeSystem', params.system, params.version);
codeSystem = await findTerminologyResource('CodeSystem', params.system, params.version);
} else if (params.coding?.system) {
codeSystem = await findTerminologyResource<CodeSystem>('CodeSystem', params.coding.system, params.version);
codeSystem = await findTerminologyResource('CodeSystem', params.coding.system, params.version);
} else {
return [badRequest('No code system specified')];
}
Expand Down
66 changes: 64 additions & 2 deletions packages/server/src/fhir/operations/expand.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContentType, HTTP_HL7_ORG, HTTP_TERMINOLOGY_HL7_ORG, LOINC, SNOMED } from '@medplum/core';
import { ContentType, HTTP_HL7_ORG, HTTP_TERMINOLOGY_HL7_ORG, LOINC, SNOMED, createReference } from '@medplum/core';
import {
CodeSystem,
OperationOutcome,
Expand Down Expand Up @@ -391,7 +391,6 @@ describe.each<Partial<Project>>([{ features: [] }, { features: ['terminology'] }
const res2 = await request(app)
.get(`/fhir/R4/ValueSet/$expand?url=${encodeURIComponent(url)}`)
.set('Authorization', 'Bearer ' + accessToken);
console.log(JSON.stringify(res2.body, undefined, 2));
expect(res2.status).toBe(200);
expect(res2.body.expansion.contains).toEqual(
expect.arrayContaining([
Expand Down Expand Up @@ -548,6 +547,69 @@ describe('Updated implementation', () => {
expect(coding.display).toEqual('Test SNOMED override');
});

test('Prefers CodeSystem from linked Projects in link order', async () => {
// Set up linked Projects and CodeSystem resources
const url = 'http://example.com/cs' + randomUUID();
const codeSystem: CodeSystem = {
resourceType: 'CodeSystem',
status: 'active',
content: 'complete',
url,
};

const { project: p2, accessToken: a2 } = await createTestProject({ withAccessToken: true });
const cs2 = await request(app)
.post(`/fhir/R4/CodeSystem`)
.set('Authorization', 'Bearer ' + a2)
.set('Content-Type', ContentType.FHIR_JSON)
.send({ ...codeSystem, concept: [{ code: '1', display: 'Incorrect coding' }] });
expect(cs2.status).toEqual(201);

const { project: p1, accessToken: a1 } = await createTestProject({ withAccessToken: true });
const cs1 = await request(app)
.post(`/fhir/R4/CodeSystem`)
.set('Authorization', 'Bearer ' + a1)
.set('Content-Type', ContentType.FHIR_JSON)
.send({ ...codeSystem, concept: [{ code: '1', display: 'Correct coding' }] });
expect(cs1.status).toEqual(201);

const { project: p3, accessToken: a3 } = await createTestProject({ withAccessToken: true });
const cs3 = await request(app)
.post(`/fhir/R4/CodeSystem`)
.set('Authorization', 'Bearer ' + a3)
.set('Content-Type', ContentType.FHIR_JSON)
.send({ ...codeSystem, concept: [{ code: '1', display: 'Another incorrect coding' }] });
expect(cs3.status).toEqual(201);

accessToken = await initTestAuth({
project: {
features: ['terminology'],
link: [{ project: createReference(p1) }, { project: createReference(p2) }, { project: createReference(p3) }],
},
});

const res2 = await request(app)
.post(`/fhir/R4/ValueSet`)
.set('Authorization', 'Bearer ' + accessToken)
.set('Content-Type', ContentType.FHIR_JSON)
.send({
resourceType: 'ValueSet',
status: 'active',
url: 'https://example.com/' + randomUUID(),
compose: { include: [{ system: url }] },
});
expect(res2.status).toBe(201);

const res3 = await request(app)
.get(`/fhir/R4/ValueSet/$expand?url=${encodeURIComponent(res2.body.url)}`)
.set('Authorization', 'Bearer ' + accessToken);
expect(res3.status).toBe(200);
const coding = res3.body.expansion.contains[0];
expect(coding.system).toBe(url);
expect(coding.code).toBe('1');
expect(coding.display).toEqual('Correct coding');
});

test('Returns error when property filter is invalid for CodeSystem', async () => {
const res1 = await request(app)
.post(`/fhir/R4/CodeSystem`)
Expand Down
57 changes: 38 additions & 19 deletions packages/server/src/fhir/operations/utils/terminology.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { OperationOutcomeError, Operator, badRequest } from '@medplum/core';
import { OperationOutcomeError, Operator, badRequest, createReference, resolveId } from '@medplum/core';
import { getAuthenticatedContext } from '../../../context';
import { r4ProjectId } from '../../../seed';
import { CodeSystem, CodeSystemConceptProperty, ConceptMap, ValueSet } from '@medplum/fhirtypes';
import { CodeSystem, CodeSystemConceptProperty, ConceptMap, Reference, ValueSet } from '@medplum/fhirtypes';
import { SelectQuery, Conjunction, Condition, Column, Union } from '../../sql';
import { getSystemRepo } from '../../repo';

export const parentProperty = 'http://hl7.org/fhir/concept-properties#parent';
export const childProperty = 'http://hl7.org/fhir/concept-properties#child';
Expand All @@ -15,12 +15,12 @@ export async function findTerminologyResource<T extends TerminologyResource>(
url: string,
version?: string
): Promise<T> {
const { repo } = getAuthenticatedContext();
const { repo, project } = getAuthenticatedContext();
const filters = [{ code: 'url', operator: Operator.EQUALS, value: url }];
if (version) {
filters.push({ code: 'version', operator: Operator.EQUALS, value: version });
}
const resources = await repo.searchResources<T>({
const results = await repo.searchResources<T>({
resourceType,
filters,
sortRules: [
Expand All @@ -31,24 +31,43 @@ export async function findTerminologyResource<T extends TerminologyResource>(
],
});

if (!resources.length) {
if (!results.length) {
throw new OperationOutcomeError(badRequest(`${resourceType} ${url} not found`));
} else if (resources.length === 1) {
return resources[0];
} else if (results.length === 1 || !sameTerminologyResourceVersion(results[0], results[1])) {
return results[0];
} else {
resources.sort((a: TerminologyResource, b: TerminologyResource) => {
// Select the non-base FHIR versions of resources before the base FHIR ones
// This is kind of a kludge, but is required to break ties because some CodeSystems (including SNOMED)
// don't have a version and the base spec version doesn't include a date (and so is always considered current)
if (a.meta?.project === r4ProjectId) {
return 1;
} else if (b.meta?.project === r4ProjectId) {
return -1;
const resourceReferences: Reference<T>[] = [];
for (const resource of results) {
resourceReferences.push(createReference(resource));
}
const resources = (await getSystemRepo().readReferences(resourceReferences)) as (T | Error)[];
const projectResource = resources.find((r) => r instanceof Error || r.meta?.project === project.id);
if (projectResource instanceof Error) {
throw projectResource;
} else if (projectResource) {
return projectResource;
}
if (project.link) {
for (const linkedProject of project.link) {
const linkedResource = resources.find(
(r) => !(r instanceof Error) && r.meta?.project === resolveId(linkedProject.project)
) as T | undefined;
if (linkedResource) {
return linkedResource;
}
}
return 0;
});
return resources[0];
}
throw new OperationOutcomeError(badRequest(`${resourceType} ${url} not found`));
}
}

function sameTerminologyResourceVersion(a: TerminologyResource, b: TerminologyResource): boolean {
if (a.version !== b.version) {
return false;
} else if (a.date !== b.date) {
return false;
}
return true;
}

export function addPropertyFilter(query: SelectQuery, property: string, value: string, isEqual?: boolean): SelectQuery {
Expand Down

0 comments on commit 84b2ad7

Please sign in to comment.