Skip to content
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
112 changes: 83 additions & 29 deletions examples/browser/cql4browsers.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/cql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Interval,
Quantity,
Ratio,
CQLValueSet,
ValueSet
} from './datatypes/datatypes';

Expand Down Expand Up @@ -56,6 +57,7 @@ export {
Interval,
Quantity,
Ratio,
CQLValueSet,
ValueSet
};

Expand All @@ -82,5 +84,6 @@ export default {
Interval,
Quantity,
Ratio,
CQLValueSet,
ValueSet
};
29 changes: 22 additions & 7 deletions src/datatypes/clinical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,33 @@ export class Concept {
}
}

export class ValueSet {
constructor(public oid: string, public version?: string, public codes: any[] = []) {
this.codes ||= [];
export abstract class Vocabulary {
constructor(public id: string, public version?: string, public name?: string) {}
}

export class CodeSystem extends Vocabulary {
constructor(public id: string, public version?: string, public name?: string) {
super(id, version, name);
}
}

export class CQLValueSet extends Vocabulary {
constructor(
public id: string,
public version?: string,
public name?: string,
public codesystems?: CodeSystem[]
) {
super(id, version, name);
}

get isValueSet() {
return true;
}
}

export class ValueSet {
constructor(public oid: string, public version?: string, public codes: any[] = []) {}

/**
* Determines if the provided code matches any code in the current set.
Expand Down Expand Up @@ -149,7 +168,3 @@ function codesInList(cl1: any, cl2: any) {
function codesMatch(code1: Code, code2: Code) {
return code1.code === code2.code && code1.system === code2.system;
}

export class CodeSystem {
constructor(public id: string, public version?: string, public name?: string) {}
}
36 changes: 21 additions & 15 deletions src/elm/clinical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@ import { Expression } from './expression';
import * as dt from '../datatypes/datatypes';
import { Context } from '../runtime/context';
import { build } from './builder';
import { resolveValueSet } from '../util/util';

export class ValueSetDef extends Expression {
name: string;
id: string;
version?: string;
codesystems?: CodeSystemRef[];

constructor(json: any) {
super(json);
this.name = json.name;
this.id = json.id;
this.version = json.version;
this.codesystems = json.codeSystem?.map((cs: any) => new CodeSystemRef(cs));
}

//todo: code systems and versions

async exec(ctx: Context) {
const valueset =
(await ctx.codeService.findValueSet(this.id, this.version)) ||
new dt.ValueSet(this.id, this.version);
ctx.rootContext().set(this.name, valueset);
let codeSystems;
if (this.codesystems) {
codeSystems = (await Promise.all(
this.codesystems.map(async csRef => csRef.exec(ctx))
)) as dt.CodeSystem[];
}
const valueset = new dt.CQLValueSet(this.id, this.version, this.name, codeSystems);
// ctx.rootContext().set(this.name, valueset); Note (2025): this seems to be unneccesary, remove completely in future if not needed
return valueset;
}
}
Expand All @@ -37,7 +42,6 @@ export class ValueSetRef extends Expression {
}

async exec(ctx: Context) {
// TODO: This calls the code service every time-- should be optimized
let valueset = ctx.getValueSet(this.name, this.libraryName);
if (valueset instanceof Expression) {
valueset = await valueset.execute(ctx);
Expand All @@ -64,11 +68,12 @@ export class AnyInValueSet extends Expression {
if (codes == null) {
return false;
}
const valueset = await this.valueset.execute(ctx);
const valueset: dt.CQLValueSet = await this.valueset.execute(ctx);
if (valueset == null || !valueset.isValueSet) {
throw new Error('ValueSet must be provided to AnyInValueSet expression');
}
return codes.some((code: any) => valueset.hasMatch(code));
const vsExpansion = await resolveValueSet(valueset, ctx);
return codes.some((code: any) => vsExpansion.hasMatch(code));
}
}

Expand All @@ -90,12 +95,13 @@ export class InValueSet extends Expression {
if (code == null) {
return false;
}
const valueset = await this.valueset.execute(ctx);
const valueset: dt.CQLValueSet = await this.valueset.execute(ctx);
if (valueset == null || !valueset.isValueSet) {
throw new Error('ValueSet must be provided to InValueSet expression');
}
// If there is a code and valueset return whether or not the valueset has the code
return valueset.hasMatch(code);
const vsExpansion = await resolveValueSet(valueset, ctx);
return vsExpansion.hasMatch(code);
}
}

Expand All @@ -108,14 +114,14 @@ export class ExpandValueSet extends Expression {
}

async exec(ctx: Context) {
const valueset = await this.valueset.execute(ctx);
const valueset: dt.CQLValueSet = await this.valueset.execute(ctx);
if (valueset == null) {
return null;
} else if (!valueset.isValueSet) {
throw new Error('ExpandValueSet function invoked on object that is not a ValueSet');
}

return valueset.expand();
const vsExpansion = await resolveValueSet(valueset, ctx);
return vsExpansion.expand();
}
}

Expand Down Expand Up @@ -148,7 +154,7 @@ export class CodeSystemRef extends Expression {

async exec(ctx: Context) {
const codeSystemDef = ctx.getCodeSystem(this.name, this.libraryName);
return codeSystemDef ? codeSystemDef.execute(ctx) : undefined;
return codeSystemDef.execute(ctx);
}
}

Expand Down
15 changes: 10 additions & 5 deletions src/elm/external.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Expression } from './expression';
import { typeIsArray } from '../util/util';
import { resolveValueSet, typeIsArray } from '../util/util';
import { Context } from '../runtime/context';
import { build } from './builder';
import { RetrieveDetails } from '../types/cql-patient.interfaces';
import { Code, ValueSet } from '../datatypes/clinical';
import { Code, CQLValueSet } from '../datatypes/clinical';

export class Retrieve extends Expression {
datatype: string;
Expand Down Expand Up @@ -33,13 +33,18 @@ export class Retrieve extends Expression {
};

if (this.codes) {
const resolvedCodes: Code[] | ValueSet | undefined = await this.codes.execute(ctx);
const executedCodes: Code[] | CQLValueSet | undefined = await this.codes.execute(ctx);

if (resolvedCodes == null) {
if (executedCodes == null) {
return [];
}

retrieveDetails.codes = resolvedCodes;
if (typeIsArray(executedCodes)) {
retrieveDetails.codes = executedCodes as Code[];
} else {
// retrieveDetails codes are expected to be expanded for external usage
retrieveDetails.codes = await resolveValueSet(executedCodes as CQLValueSet, ctx);
}
}

if (this.dateRange) {
Expand Down
17 changes: 15 additions & 2 deletions src/elm/overloaded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
import { Expression } from './expression';
import { ThreeValuedLogic } from '../datatypes/logic';
import { DateTime } from '../datatypes/datetime';
import { typeIsArray } from '../util/util';
import { resolveValueSet, typeIsArray } from '../util/util';
import { equals, equivalent } from '../util/comparison';
import * as DT from './datetime';
import * as LIST from './list';
import * as IVL from './interval';
import { Context } from '../runtime/context';
import { CQLValueSet } from '../datatypes/clinical';

export class Equal extends Expression {
constructor(json: any) {
Expand All @@ -30,12 +31,24 @@ export class Equivalent extends Expression {
}

async exec(ctx: Context) {
const [a, b] = await this.execArgs(ctx);
let [a, b] = await this.execArgs(ctx);
if (a == null && b == null) {
return true;
} else if (a == null || b == null) {
return false;
} else {
// comparison of valueset id/version -> only check expanded equivalence if these don't match
if (a.isValueSet && b.isValueSet) {
if (a.id === b.id && a.version === b.version) {
return true;
}
}
if (a.isValueSet) {
a = await resolveValueSet(a as CQLValueSet, ctx);
}
if (b.isValueSet) {
b = await resolveValueSet(b as CQLValueSet, ctx);
}
return equivalent(a, b);
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/util/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { CQLValueSet, ValueSet } from '../datatypes/clinical';
import { Context } from '../runtime/context';

export type Direction = 'asc' | 'ascending' | 'desc' | 'descending';

export function removeNulls(things: any[]) {
Expand Down Expand Up @@ -107,3 +110,14 @@ async function merge<T>(left: T[], right: T[], compareFn: SortCompareFn<T>) {
}
return [...sorted, ...left, ...right];
}

export async function resolveValueSet(vs: CQLValueSet, ctx: Context): Promise<ValueSet> {
// code service owns implementation of any valueset expansion caching
const vsExpansion = await ctx.codeService.findValueSet(vs.id, vs.version);
if (!vsExpansion) {
throw new Error(
`Unable to resolve expected valueset with id ${vs.id} and version ${vs.version}`
);
}
return vsExpansion;
}
4 changes: 2 additions & 2 deletions test/cql-exports-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ libNames.push('Context', 'Executor', 'PatientContext', 'UnfilteredContext', 'Res
// PatientSource-related classes
libNames.push('Patient', 'PatientSource');
// TerminologyService-related classes
libNames.push('CodeService');
libNames.push('CodeService', 'ValueSet');
// DataType classes
libNames.push(
'Code',
Expand All @@ -20,7 +20,7 @@ libNames.push(
'Interval',
'Quantity',
'Ratio',
'ValueSet'
'CQLValueSet'
);

describe('CQL Exports', () =>
Expand Down
17 changes: 16 additions & 1 deletion test/datatypes/clinical-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Code, CodeSystem, Concept, ValueSet } from '../../src/datatypes/clinical';
import { Code, CodeSystem, Concept, CQLValueSet, ValueSet } from '../../src/datatypes/clinical';
import should from 'should';

describe('Code', () => {
Expand Down Expand Up @@ -119,6 +119,21 @@ describe('Concept', () => {
});
});

describe('CQLValueSet', () => {
let valueSet: CQLValueSet;
beforeEach(() => {
valueSet = new CQLValueSet('1.2.3.4.5', '1', 'name', [new CodeSystem('systemId')]);
});

it('should properly represent the id, version, name, codesystems', () => {
valueSet.id.should.equal('1.2.3.4.5');
valueSet.version.should.equal('1');
valueSet.name.should.equal('name');
valueSet.codesystems.length.should.equal(1);
valueSet.codesystems[0].should.eql(new CodeSystem('systemId'));
});
});

describe('ValueSet', () => {
let valueSet: ValueSet;
beforeEach(() => {
Expand Down
8 changes: 2 additions & 6 deletions test/datatypes/uncertainty-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import should from 'should';
import { Concept, ValueSet } from '../../src/datatypes/clinical';
import { CodeSystem, Concept, CQLValueSet } from '../../src/datatypes/clinical';
import { Code, Date, DateTime } from '../../src/datatypes/datatypes';
import { Uncertainty } from '../../src/datatypes/uncertainty';
import { equals } from '../../src/util/comparison';
Expand Down Expand Up @@ -50,11 +50,7 @@ describe('Uncertainty', () => {
should(conceptHigh.low).be.null();
should(conceptHigh.high).be.null();

const valueSet = new ValueSet('1.2.3.4.5', '1', [
new Code('ABC', '5.4.3.2.1', '1'),
new Code('DEF', '5.4.3.2.1', '2'),
new Code('GHI', '5.4.3.4.5', '3')
]);
const valueSet = new CQLValueSet('1.2.3.4.5', '1', 'name', [new CodeSystem('systemId')]);
const valueSetLow = new Uncertainty(valueSet, 1);
should(valueSetLow.low).be.null();
should(valueSetLow.high).be.null();
Expand Down
Loading