Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full linked Project ordering in CodeSystem lookup #4522

Merged
merged 2 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
mattwiller marked this conversation as resolved.
Show resolved Hide resolved
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
Loading