Skip to content

Commit 6ef99d0

Browse files
committed
initial valueset class definition
Create internal vs class Export correct types and delay expansion of vs
1 parent 7d967de commit 6ef99d0

14 files changed

Lines changed: 166 additions & 61 deletions

src/cql-code-service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Code, ValueSet } from './datatypes/datatypes';
1+
import { Code, ValueSetExpansion } from './datatypes/datatypes';
22
import { TerminologyProvider, ValueSetDictionary, ValueSetObject } from './types';
33

44
export class CodeService implements TerminologyProvider {
@@ -12,16 +12,16 @@ export class CodeService implements TerminologyProvider {
1212
const codes = valueSetsJson[oid][version].map(
1313
(code: any) => new Code(code.code, code.system, code.version)
1414
);
15-
this.valueSets[oid][version] = new ValueSet(oid, version, codes);
15+
this.valueSets[oid][version] = new ValueSetExpansion(oid, version, codes);
1616
}
1717
}
1818
}
1919

20-
findValueSetsByOid(oid: string): ValueSet[] {
20+
findValueSetsByOid(oid: string): ValueSetExpansion[] {
2121
return this.valueSets[oid] ? Object.values(this.valueSets[oid]) : [];
2222
}
2323

24-
findValueSet(oid: string, version?: string): ValueSet | null {
24+
findValueSet(oid: string, version?: string): ValueSetExpansion | null {
2525
if (version != null) {
2626
return this.valueSets[oid] != null ? this.valueSets[oid][version] : null;
2727
} else {

src/datatypes/clinical.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,35 @@ export class Concept {
3535
return codesInList(toCodeList(code), this.codes);
3636
}
3737
}
38+
export abstract class Vocabulary {
39+
constructor(public id: string, public version?: string, public name?: string) {}
40+
}
3841

39-
export class ValueSet {
40-
constructor(public oid: string, public version?: string, public codes: any[] = []) {
41-
this.codes ||= [];
42+
export class CodeSystem extends Vocabulary {
43+
constructor(public id: string, public version?: string, public name?: string) {
44+
super(id, version, name);
45+
}
46+
}
47+
48+
export class ValueSet extends Vocabulary {
49+
constructor(
50+
public id: string,
51+
public version?: string,
52+
public name?: string,
53+
public codesystems?: CodeSystem[]
54+
) {
55+
super(id, version, name);
4256
}
4357

4458
get isValueSet() {
4559
return true;
4660
}
61+
}
62+
63+
export class ValueSetExpansion {
64+
constructor(public oid: string, public version?: string, public codes: any[] = []) {
65+
this.codes ||= [];
66+
}
4767

4868
/**
4969
* Determines if the provided code matches any code in the current set.
@@ -149,7 +169,3 @@ function codesInList(cl1: any, cl2: any) {
149169
function codesMatch(code1: Code, code2: Code) {
150170
return code1.code === code2.code && code1.system === code2.system;
151171
}
152-
153-
export class CodeSystem {
154-
constructor(public id: string, public version?: string, public name?: string) {}
155-
}

src/elm/clinical.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,58 @@ import { Expression } from './expression';
22
import * as dt from '../datatypes/datatypes';
33
import { Context } from '../runtime/context';
44
import { build } from './builder';
5+
import { resolveValueSet } from '../util/util';
56

67
export class ValueSetDef extends Expression {
78
name: string;
89
id: string;
910
version?: string;
11+
codesystems?: CodeSystemRef[];
1012

1113
constructor(json: any) {
1214
super(json);
1315
this.name = json.name;
1416
this.id = json.id;
1517
this.version = json.version;
18+
this.codesystems = json.codesystems; // TODO: fix this as needed. See what the elm json passes through to populate this. Will be a CodeSystemRef (name, libraryname)
1619
}
1720

1821
//todo: code systems and versions
1922

2023
async exec(ctx: Context) {
21-
const valueset =
22-
(await ctx.codeService.findValueSet(this.id, this.version)) ||
23-
new dt.ValueSet(this.id, this.version);
24-
ctx.rootContext().set(this.name, valueset);
24+
// TODO: use the context to resolve the CodeSystemRef to a CodeSystem
25+
// valueset is loaded as part of initial library load ... check if in library vs list??? if not, add
26+
if (!ctx) {
27+
throw Error('no context');
28+
} // dumb placeholder
29+
const valueset = new dt.ValueSet(this.id, this.version, this.name, this.codesystems);
30+
// ctx.rootContext().set(this.name, valueset); Note (2025): this seems to be unneccesary, remove completely in future if not needed
2531
return valueset;
2632
}
33+
34+
// Recommendations:
35+
// Resolve when we need it -> if not resolved, throw error
36+
37+
// TODO: ?? other places that a valueset could be created in patient data (in the middle of execution)
38+
// ^ talk to Chris Moesel about valueset that's defined on the fly
39+
// ... resources could have contained valuesets -> that would be a FHIR valueset (we wouldn't have any automatic conversion of that)
40+
// probably not a problem we need to solve at this juncture
41+
// this could influence how we define the terminology or data provider interfaces, but that's a future issue
42+
43+
// Update: initially store ValueSet to context... then, when we need the expansion, call the code service to expand and (also?) store the expansion to the context
44+
// This rootContext call is kind of hinky ... why are we doing this since the getValueSet function pulls from the root library vs list?
45+
// Recommendations:
46+
// Cache both valueset references and valueset resolutions? (could also be responsibility of the terminology provider? implementation class can choose how to cache or not cache)
47+
// For simplicity, push expanded caching to terminology provider
48+
49+
// async exec(ctx: Context) {
50+
// const valuesetExpanded = await ctx.codeService.findValueSet(this.id, this.version);
51+
// if(!valuesetExpanded){
52+
// throw Error('TODO: Make this better');
53+
// }
54+
// ctx.rootContext().set(this.name, valuesetExpanded);
55+
// return new dt.ValueSet(this.id, this.version);
56+
// }
2757
}
2858

2959
export class ValueSetRef extends Expression {
@@ -37,7 +67,6 @@ export class ValueSetRef extends Expression {
3767
}
3868

3969
async exec(ctx: Context) {
40-
// TODO: This calls the code service every time-- should be optimized
4170
let valueset = ctx.getValueSet(this.name, this.libraryName);
4271
if (valueset instanceof Expression) {
4372
valueset = await valueset.execute(ctx);
@@ -64,11 +93,12 @@ export class AnyInValueSet extends Expression {
6493
if (codes == null) {
6594
return false;
6695
}
67-
const valueset = await this.valueset.execute(ctx);
96+
const valueset: dt.ValueSet = await this.valueset.execute(ctx);
6897
if (valueset == null || !valueset.isValueSet) {
6998
throw new Error('ValueSet must be provided to AnyInValueSet expression');
7099
}
71-
return codes.some((code: any) => valueset.hasMatch(code));
100+
const vsExpansion = await resolveValueSet(valueset, ctx);
101+
return codes.some((code: any) => vsExpansion.hasMatch(code));
72102
}
73103
}
74104

@@ -90,12 +120,13 @@ export class InValueSet extends Expression {
90120
if (code == null) {
91121
return false;
92122
}
93-
const valueset = await this.valueset.execute(ctx);
123+
const valueset: dt.ValueSet = await this.valueset.execute(ctx);
94124
if (valueset == null || !valueset.isValueSet) {
95125
throw new Error('ValueSet must be provided to InValueSet expression');
96126
}
97127
// If there is a code and valueset return whether or not the valueset has the code
98-
return valueset.hasMatch(code);
128+
const vsExpansion = await resolveValueSet(valueset, ctx);
129+
return vsExpansion.hasMatch(code);
99130
}
100131
}
101132

@@ -108,14 +139,14 @@ export class ExpandValueSet extends Expression {
108139
}
109140

110141
async exec(ctx: Context) {
111-
const valueset = await this.valueset.execute(ctx);
142+
const valueset: dt.ValueSet = await this.valueset.execute(ctx);
112143
if (valueset == null) {
113144
return null;
114145
} else if (!valueset.isValueSet) {
115146
throw new Error('ExpandValueSet function invoked on object that is not a ValueSet');
116147
}
117-
118-
return valueset.expand();
148+
const vsExpansion = await resolveValueSet(valueset, ctx);
149+
return vsExpansion.expand();
119150
}
120151
}
121152

src/elm/external.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Expression } from './expression';
2-
import { typeIsArray } from '../util/util';
2+
import { resolveValueSet, typeIsArray } from '../util/util';
33
import { Context } from '../runtime/context';
44
import { build } from './builder';
55
import { RetrieveDetails } from '../types/cql-patient.interfaces';
@@ -33,13 +33,20 @@ export class Retrieve extends Expression {
3333
};
3434

3535
if (this.codes) {
36-
const resolvedCodes: Code[] | ValueSet | undefined = await this.codes.execute(ctx);
36+
const executedCodes: Code[] | ValueSet | undefined = await this.codes.execute(ctx);
3737

38-
if (resolvedCodes == null) {
38+
if (executedCodes == null) {
3939
return [];
4040
}
4141

42-
retrieveDetails.codes = resolvedCodes;
42+
if (typeIsArray(executedCodes)) {
43+
retrieveDetails.codes = executedCodes;
44+
} else if (executedCodes?.isValueSet) {
45+
// retrieveDetails codes are expected to be expanded for external usage
46+
retrieveDetails.codes = await resolveValueSet(executedCodes, ctx);
47+
} else {
48+
retrieveDetails.codes = undefined;
49+
}
4350
}
4451

4552
if (this.dateRange) {

src/elm/overloaded.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import { Expression } from './expression';
33
import { ThreeValuedLogic } from '../datatypes/logic';
44
import { DateTime } from '../datatypes/datetime';
5-
import { typeIsArray } from '../util/util';
5+
import { resolveValueSet, typeIsArray } from '../util/util';
66
import { equals, equivalent } from '../util/comparison';
77
import * as DT from './datetime';
88
import * as LIST from './list';
99
import * as IVL from './interval';
1010
import { Context } from '../runtime/context';
11+
import { ValueSet } from '../datatypes/clinical';
1112

1213
export class Equal extends Expression {
1314
constructor(json: any) {
@@ -30,12 +31,24 @@ export class Equivalent extends Expression {
3031
}
3132

3233
async exec(ctx: Context) {
33-
const [a, b] = await this.execArgs(ctx);
34+
let [a, b] = await this.execArgs(ctx);
3435
if (a == null && b == null) {
3536
return true;
3637
} else if (a == null || b == null) {
3738
return false;
3839
} else {
40+
// comparison of valueset id/version -> only check expanded equivalence if these don't match
41+
if (a.isValueSet && b.isValueSet) {
42+
if (a.id === b.id && a.version === b.version) {
43+
return true;
44+
}
45+
}
46+
if (a.isValueSet) {
47+
a = await resolveValueSet(a as ValueSet, ctx);
48+
}
49+
if (b.isValueSet) {
50+
b = await resolveValueSet(b as ValueSet, ctx);
51+
}
3952
return equivalent(a, b);
4053
}
4154
}

src/types/cql-code-service.interfaces.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ValueSet } from '../datatypes/datatypes';
1+
import { ValueSetExpansion } from '../datatypes/datatypes';
22

33
/*
44
* Lookup of all codes used based on their ValueSet
@@ -19,14 +19,17 @@ export interface ValueSetDictionary {
1919
*/
2020
export interface ValueSetObject {
2121
[oid: string]: {
22-
[version: string]: ValueSet;
22+
[version: string]: ValueSetExpansion;
2323
};
2424
}
2525

2626
/*
2727
* Structure of an implementation to look up ValueSets based on oid and version
2828
*/
2929
export interface TerminologyProvider {
30-
findValueSetsByOid: (oid: string) => ValueSet[] | Promise<ValueSet[]>;
31-
findValueSet: (oid: string, version?: string) => ValueSet | Promise<ValueSet> | null;
30+
findValueSetsByOid: (oid: string) => ValueSetExpansion[] | Promise<ValueSetExpansion[]>;
31+
findValueSet: (
32+
oid: string,
33+
version?: string
34+
) => ValueSetExpansion | Promise<ValueSetExpansion> | null;
3235
}

src/types/cql-patient.interfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Code, ValueSet } from '../datatypes/clinical';
1+
import { Code, ValueSetExpansion } from '../datatypes/clinical';
22
import { Interval } from '../datatypes/interval';
33
import { AnyTypeSpecifier } from './type-specifiers.interfaces';
44

@@ -31,7 +31,7 @@ export interface RetrieveDetails {
3131
datatype: string;
3232
templateId?: string;
3333
codeProperty?: string;
34-
codes?: Code[] | ValueSet;
34+
codes?: Code[] | ValueSetExpansion;
3535
dateProperty?: string;
3636
dateRange?: Interval;
3737
}

src/util/util.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { ValueSet, ValueSetExpansion } from '../datatypes/clinical';
2+
import { Context } from '../runtime/context';
3+
14
export type Direction = 'asc' | 'ascending' | 'desc' | 'descending';
25

36
export function removeNulls(things: any[]) {
@@ -107,3 +110,14 @@ async function merge<T>(left: T[], right: T[], compareFn: SortCompareFn<T>) {
107110
}
108111
return [...sorted, ...left, ...right];
109112
}
113+
114+
export async function resolveValueSet(vs: ValueSet, ctx: Context): Promise<ValueSetExpansion> {
115+
// code service owns implementation of any valueset expansion caching
116+
const vsExpansion = await ctx.codeService.findValueSet(vs.id, vs.version);
117+
if (!vsExpansion) {
118+
throw new Error(
119+
`Unable to resolve expected valueset with id ${vs.id} and version ${vs.version}`
120+
);
121+
}
122+
return vsExpansion;
123+
}

test/cql-code-service-test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import should from 'should';
22
import { CodeService } from '../src/cql-code-service';
3-
import { Code, ValueSet } from '../src/datatypes/datatypes';
3+
import { Code, ValueSetExpansion } from '../src/datatypes/datatypes';
44

55
describe('CodeService', () => {
6-
let svc: CodeService, vsOne: ValueSet, vsTwo: ValueSet, vsThree: ValueSet;
6+
let svc: CodeService,
7+
vsOne: ValueSetExpansion,
8+
vsTwo: ValueSetExpansion,
9+
vsThree: ValueSetExpansion;
710
beforeEach(() => {
811
svc = new CodeService({
912
'1.2.3.4.5': {
@@ -26,17 +29,17 @@ describe('CodeService', () => {
2629
]
2730
}
2831
});
29-
vsOne = new ValueSet('1.2.3.4.5', '1', [
32+
vsOne = new ValueSetExpansion('1.2.3.4.5', '1', [
3033
new Code('ABC', '5.4.3.2.1', '1'),
3134
new Code('DEF', '5.4.3.2.1', '2'),
3235
new Code('GHI', '5.4.3.4.5', '3')
3336
]);
34-
vsTwo = new ValueSet('1.2.3.4.5', '2', [
37+
vsTwo = new ValueSetExpansion('1.2.3.4.5', '2', [
3538
new Code('ABC', '5.4.3.2.1', '1'),
3639
new Code('DEF', '5.4.3.2.1', '2'),
3740
new Code('JKL', '5.4.3.2.1', '3')
3841
]);
39-
vsThree = new ValueSet('6.7.8.9.0', 'A', [
42+
vsThree = new ValueSetExpansion('6.7.8.9.0', 'A', [
4043
new Code('MNO', '2.4.6.8.0', '3'),
4144
new Code('PQR', '2.4.6.8.0', '2'),
4245
new Code('STU', '2.4.6.8.0', '1')

0 commit comments

Comments
 (0)