Skip to content
Open
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
13 changes: 12 additions & 1 deletion src/cql-patient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as DT from './datatypes/datatypes';
import { DataProvider, NamedTypeSpecifier, PatientObject, RecordObject } from './types';
import { SystemDataModelProvider } from './runtime/systemDataModelProvider';
import {
DataModelProvider,
DataProvider,
NamedTypeSpecifier,
PatientObject,
RecordObject
} from './types';
import { ELM_ANY_TYPE, ELM_NAMED_TYPE_SPECIFIER } from './util/elmTypes';

export class Record implements RecordObject {
Expand Down Expand Up @@ -159,4 +166,8 @@ export class PatientSource implements DataProvider {
this.current = currentJSON ? new Patient(currentJSON) : undefined;
return this.current;
}

getDataModelProvider(): DataModelProvider {
return new SystemDataModelProvider();
}
}
4 changes: 4 additions & 0 deletions src/datatypes/clinical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export class CodeSystem extends Vocabulary {
super(id, version, name);
}

get isCodeSystem() {
return true;
}

hasMatch(_code: any) {
throw new Error('In CodeSystem operation not yet supported.');
}
Expand Down
14 changes: 1 addition & 13 deletions src/elm/instance.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Expression } from './expression';
import { Quantity } from '../datatypes/quantity';
import { Code, Concept } from '../datatypes/datatypes';
import { Context } from '../runtime/context';
import { build } from './builder';
import { ELM_QUANTITY_TYPE, ELM_CONCEPT_TYPE } from '../util/elmTypes';

class Element {
name: string;
Expand Down Expand Up @@ -33,15 +30,6 @@ export class Instance extends Expression {
for (const el of this.element) {
obj[el.name] = await el.exec(ctx);
}
switch (this.classType) {
case ELM_QUANTITY_TYPE:
return new Quantity(obj.value, obj.unit);
case '{urn:hl7-org:elm-types:r1}Code':
return new Code(obj.code, obj.system, obj.version, obj.display);
case ELM_CONCEPT_TYPE:
return new Concept(obj.codes, obj.display);
default:
return obj;
}
return ctx.createInstance(this.classType, obj);
}
}
92 changes: 69 additions & 23 deletions src/runtime/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as dt from '../datatypes/datatypes';
import { Exception } from '../datatypes/exception';
import { Library } from '../elm/library';
import { PatientObject, RetrieveDetails, TerminologyProvider } from '../types';
import { DataModelProvider, PatientObject, RetrieveDetails, TerminologyProvider } from '../types';
import { Parameter } from '../types/runtime.types';
import {
ELM_NAMED_TYPE_SPECIFIER,
Expand All @@ -18,16 +18,24 @@ import {
ELM_DATETIME_TYPE,
ELM_DATE_TYPE,
ELM_QUANTITY_TYPE,
ELM_TIME_TYPE
ELM_TIME_TYPE,
ELM_CODE_TYPE,
ELM_CODESYSTEM_TYPE,
ELM_VALUESET_TYPE,
ELM_RATIO_TYPE,
ELM_ANY_TYPE,
ELM_VOCABULARY_TYPE
} from '../util/elmTypes';
import { typeIsArray } from '../util/util';
import { MessageListener, NullMessageListener } from './messageListeners';
import { SystemDataModelProvider } from './systemDataModelProvider';

export class Context {
// Public Constructor args
parent: any;
executionDateTime?: dt.DateTime;
messageListener?: MessageListener;
dataModelProviders?: DataModelProvider[];

// Private Constructor args
private _codeService?: TerminologyProvider;
Expand All @@ -44,7 +52,8 @@ export class Context {
_codeService?: TerminologyProvider,
_parameters?: Parameter,
executionDateTime?: dt.DateTime,
messageListener?: MessageListener
messageListener?: MessageListener,
dataModelProviders?: DataModelProvider[]
) {
this.parent = parent;
this._codeService = _codeService;
Expand All @@ -57,6 +66,7 @@ export class Context {
this._parameters = _parameters || {};
this.executionDateTime = executionDateTime;
this.messageListener = messageListener;
this.dataModelProviders = dataModelProviders;
}

get parameters(): any {
Expand Down Expand Up @@ -156,6 +166,17 @@ export class Context {
}
}

getDataModelProviders(): DataModelProvider[] {
if (this.dataModelProviders == null) {
if (this.parent instanceof Context) {
this.dataModelProviders = this.parent.getDataModelProviders();
} else {
this.dataModelProviders = [new SystemDataModelProvider()];
}
}
return this.dataModelProviders;
}

getValueSet(name: string, library: any) {
return this.parent && this.parent.getValueSet(name, library);
}
Expand Down Expand Up @@ -341,25 +362,33 @@ export class Context {
case ELM_BOOLEAN_TYPE:
return typeof val === 'boolean';
case ELM_DECIMAL_TYPE:
return val && val.isDecimal;
return val.isDecimal;
case ELM_INTEGER_TYPE:
return typeof val === 'number' && Number.isInteger(val);
case ELM_LONG_TYPE:
return typeof val === 'bigint';
case ELM_STRING_TYPE:
return typeof val === 'string';
case ELM_CONCEPT_TYPE:
return val && val.isConcept;
case '{urn:hl7-org:elm-types:r1}Code':
return val && val.isCode;
return val.isConcept;
case ELM_CODE_TYPE:
return val.isCode;
case ELM_CODESYSTEM_TYPE:
return val.isCodeSystem;
case ELM_VALUESET_TYPE:
return val.isValueSet;
case ELM_VOCABULARY_TYPE:
return val.isCodeSystem ?? val.isValueSet;
case ELM_RATIO_TYPE:
return val.isRatio;
case ELM_DATETIME_TYPE:
return val && val.isDateTime;
return val.isDateTime;
case ELM_DATE_TYPE:
return val && val.isDate;
return val.isDate;
case ELM_QUANTITY_TYPE:
return val && val.isQuantity;
return val.isQuantity;
case ELM_TIME_TYPE:
return val && val.isTime && val.isTime();
return val.isTime && val.isTime();
default:
// Use the data model's implementation of _is, if it is available
if (typeof val._is === 'function') {
Expand All @@ -374,12 +403,6 @@ export class Context {
return false;
}
// Otherwise just default to true to match legacy behavior.
//
// NOTE: This is also where arbitrary tuples land because they will not have
// an "is" function and we don't encode the type information into the runtime
// objects so we can't easily determine their type. We can't reject them,
// else things like `Encounter{ id: "1" } is Encounter` would return false.
// So for now we allow false positives in order to avoid false negatives.
return true;
}
}
Expand Down Expand Up @@ -442,6 +465,25 @@ export class Context {
(val.high == null || this.matchesInstanceType(val.high, pointType))
);
}

createInstance(classType: string, obj: any) {
for (const dataModel of this.getDataModelProviders()) {
const instance = dataModel.createInstance(classType, obj);
if (instance != null) {
return instance;
}
}

// None of the DataModelProviders know how to instantiate this as anything better, so return the plain object.
// Tag it with an _is method that lets us identify what it is.
// TODO / KNOWN LIMITATION: we have no way of knowing what the full type hierarchy of this type is here,
// so `Is` expressions with a supertype will incorrectly return false.
// `defineProperty` means it will still be callable but won’t appear in enumeration or object spread.
Object.defineProperty(obj, '_is', {
value: (spec: any) => spec.name === classType || spec.name === ELM_ANY_TYPE
});
return obj;
}
}

export class PatientContext extends Context {
Expand All @@ -451,9 +493,10 @@ export class PatientContext extends Context {
codeService?: TerminologyProvider,
parameters?: Parameter,
executionDateTime: dt.DateTime = dt.DateTime.fromJSDate(new Date()),
messageListener: MessageListener = new NullMessageListener()
messageListener: MessageListener = new NullMessageListener(),
dataModelProviders?: DataModelProvider[]
) {
super(library, codeService, parameters, executionDateTime, messageListener);
super(library, codeService, parameters, executionDateTime, messageListener, dataModelProviders);
}

rootContext() {
Expand All @@ -468,7 +511,8 @@ export class PatientContext extends Context {
this.codeService,
this.parameters,
this.executionDateTime,
this.messageListener
this.messageListener,
this.getDataModelProviders()
);
}
return this.library_context[library];
Expand All @@ -482,7 +526,8 @@ export class PatientContext extends Context {
this.codeService,
this.parameters,
this.executionDateTime,
this.messageListener
this.messageListener,
this.getDataModelProviders()
);
}
return this.localId_context[localId];
Expand All @@ -500,9 +545,10 @@ export class UnfilteredContext extends Context {
codeService?: TerminologyProvider,
parameters?: Parameter,
executionDateTime: dt.DateTime = dt.DateTime.fromJSDate(new Date()),
messageListener: MessageListener = new NullMessageListener()
messageListener: MessageListener = new NullMessageListener(),
dataModelProviders?: DataModelProvider[]
) {
super(library, codeService, parameters, executionDateTime, messageListener);
super(library, codeService, parameters, executionDateTime, messageListener, dataModelProviders);
}

rootContext() {
Expand Down
29 changes: 24 additions & 5 deletions src/runtime/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { Results } from './results';
import { UnfilteredContext, PatientContext } from './context';
import { DateTime } from '../datatypes/datetime';
import { Parameter } from '../types/runtime.types';
import { DataProvider, TerminologyProvider } from '../types';
import { DataModelProvider, DataProvider, TerminologyProvider } from '../types';
import { SystemDataModelProvider } from './systemDataModelProvider';

export class Executor {
constructor(
public library: any,
public codeService?: TerminologyProvider,
public parameters?: Parameter,
public messageListener: MessageListener = new NullMessageListener()
public messageListener: MessageListener = new NullMessageListener(),
public dataModelProviders: DataModelProvider[] = []
) {}

withLibrary(lib: any) {
Expand All @@ -33,6 +35,11 @@ export class Executor {
return this;
}

withDataModelProviders(dmps: DataModelProvider[]) {
this.dataModelProviders = dmps;
return this;
}

async exec_expression(expression: any, patientSource: DataProvider, executionDateTime: DateTime) {
const r = new Results();
const expr = this.library.expressions[expression];
Expand All @@ -47,7 +54,8 @@ export class Executor {
this.codeService,
this.parameters,
executionDateTime,
this.messageListener
this.messageListener,
this.getDataModelProviders(patientSource)
);
r.recordPatientResults(patient_ctx, { [expression]: await expr.execute(patient_ctx) });
currentPatient = await patientSource.nextPatient();
Expand All @@ -64,7 +72,8 @@ export class Executor {
this.codeService,
this.parameters,
executionDateTime,
this.messageListener
this.messageListener,
this.getDataModelProviders(patientSource)
);
const resultMap: any = {};
for (const key in this.library.expressions) {
Expand All @@ -90,7 +99,8 @@ export class Executor {
this.codeService,
this.parameters,
executionDateTime,
this.messageListener
this.messageListener,
this.getDataModelProviders(patientSource)
);
const resultMap: any = {};
for (const key in this.library.expressions) {
Expand All @@ -104,4 +114,13 @@ export class Executor {
}
return r;
}

private getDataModelProviders(patientSource: DataProvider) {
const dataModelProviders = new Set(this.dataModelProviders);
dataModelProviders.add(new SystemDataModelProvider());
if (typeof patientSource.getDataModelProvider === 'function') {
dataModelProviders.add(patientSource.getDataModelProvider());
}
return Array.from(dataModelProviders);
}
}
34 changes: 34 additions & 0 deletions src/runtime/systemDataModelProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Code, CodeSystem, Concept, CQLValueSet, Quantity, Ratio } from '../datatypes/datatypes';
import { DataModelProvider } from '../types/cql-patient.interfaces';
import {
ELM_QUANTITY_TYPE,
ELM_CODE_TYPE,
ELM_CONCEPT_TYPE,
ELM_CODESYSTEM_TYPE,
ELM_VALUESET_TYPE,
ELM_RATIO_TYPE
} from '../util/elmTypes';

export class SystemDataModelProvider implements DataModelProvider {
createInstance(classType: string, obj: any) {
if (!classType.startsWith('{urn:hl7-org:elm-types:r1}')) {
return null;
}
switch (classType) {
case ELM_QUANTITY_TYPE:
return new Quantity(obj.value, obj.unit);
case ELM_CODE_TYPE:
return new Code(obj.code, obj.system, obj.version, obj.display);
case ELM_CONCEPT_TYPE:
return new Concept(obj.codes, obj.display);
case ELM_CODESYSTEM_TYPE:
return new CodeSystem(obj.id, obj.version, obj.name);
case ELM_VALUESET_TYPE:
return new CQLValueSet(obj.id, obj.version, obj.name, obj.codesystems);
case ELM_RATIO_TYPE:
return new Ratio(obj.numerator, obj.denominator);
default:
return obj;
}
}
}
5 changes: 5 additions & 0 deletions src/types/cql-patient.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { AnyTypeSpecifier } from './type-specifiers.interfaces';
export interface DataProvider {
currentPatient(): PatientObject | undefined | Promise<PatientObject | undefined>;
nextPatient(): PatientObject | undefined | Promise<PatientObject | undefined>;
getDataModelProvider?(): DataModelProvider;
}

export interface DataModelProvider {
createInstance(classType: string, obj: any): any;
}

/*
Expand Down
Loading
Loading