cql-execution
Version:
An execution framework for the Clinical Quality Language (CQL)
439 lines • 19 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnfilteredContext = exports.PatientContext = exports.Context = void 0;
const dt = __importStar(require("../datatypes/datatypes"));
const exception_1 = require("../datatypes/exception");
const util_1 = require("../util/util");
const messageListeners_1 = require("./messageListeners");
class Context {
constructor(parent, _codeService, _parameters, executionDateTime, messageListener) {
this.parent = parent;
this._codeService = _codeService;
this.context_values = {};
this.library_context = {};
this.localId_context = {};
this.evaluatedRecords = [];
// TODO: If there is an issue with number of parameters look into cql4browsers fix: 387ea77538182833283af65e6341e7a05192304c
this.checkParameters(_parameters !== null && _parameters !== void 0 ? _parameters : {}); // not crazy about possibly throwing an error in a constructor, but...
this._parameters = _parameters || {};
this.executionDateTime = executionDateTime;
this.messageListener = messageListener;
}
get parameters() {
return this._parameters || (this.parent && this.parent.parameters);
}
set parameters(params) {
this.checkParameters(params);
this._parameters = params;
}
get codeService() {
return this._codeService || (this.parent && this.parent.codeService);
}
set codeService(cs) {
this._codeService = cs;
}
withParameters(params) {
this.parameters = params || {};
return this;
}
withCodeService(cs) {
this.codeService = cs;
return this;
}
rootContext() {
if (this.parent) {
return this.parent.rootContext();
}
else {
return this;
}
}
async findRecords(profile, retrieveDetails) {
return this.parent && this.parent.findRecords(profile, retrieveDetails);
}
childContext(context_values) {
const ctx = new Context(this);
ctx.context_values = context_values !== null && context_values !== void 0 ? context_values : {};
return ctx;
}
getLibraryContext(library) {
return this.parent && this.parent.getLibraryContext(library);
}
getLocalIdContext(localId) {
return this.parent && this.parent.getLocalIdContext(localId);
}
getParameter(name) {
return this.parent && this.parent.getParameter(name);
}
getParentParameter(name) {
if (this.parent) {
if (this.parent.parameters[name] != null) {
return this.parent.parameters[name];
}
else {
return this.parent.getParentParameter(name);
}
}
}
getTimezoneOffset() {
if (this.executionDateTime != null) {
return this.executionDateTime.timezoneOffset;
}
else if (this.parent && this.parent.getTimezoneOffset != null) {
return this.parent.getTimezoneOffset();
}
else {
throw new exception_1.Exception('No Timezone Offset has been set');
}
}
getExecutionDateTime() {
if (this.executionDateTime != null) {
return this.executionDateTime;
}
else if (this.parent && this.parent.getExecutionDateTime != null) {
return this.parent.getExecutionDateTime();
}
else {
throw new exception_1.Exception('No Execution DateTime has been set');
}
}
getMessageListener() {
if (this.messageListener != null) {
return this.messageListener;
}
else if (this.parent && this.parent.getMessageListener != null) {
return this.parent.getMessageListener();
}
else {
return new messageListeners_1.NullMessageListener();
}
}
getValueSet(name, library) {
return this.parent && this.parent.getValueSet(name, library);
}
getCodeSystem(name, libraryName) {
return this.parent && this.parent.getCodeSystem(name, libraryName);
}
getCode(name) {
return this.parent && this.parent.getCode(name);
}
getConcept(name) {
return this.parent && this.parent.getConcept(name);
}
getFunction(name) {
return this.parent && this.parent.getFunction(name);
}
get(identifier) {
// Check for undefined because if its null, we actually *do* want to return null (rather than
// looking at parent), but if it's really undefined, *then* look at the parent
if (typeof this.context_values[identifier] !== 'undefined') {
return this.context_values[identifier];
}
else if (identifier === '$this') {
return this.context_values;
}
else {
return this.parent != null && this.parent.get(identifier);
}
}
set(identifier, value) {
this.context_values[identifier] = value;
}
setLocalIdWithResult(localId, value) {
// Temporary fix. Real fix will be to return a list of all result values for a given localId.
const ctx = this.localId_context[localId];
if (ctx === false || ctx === null || ctx === undefined || ctx.length === 0) {
this.localId_context[localId] = value;
}
}
getLocalIdResult(localId) {
return this.localId_context[localId];
}
// Returns an object of objects containing each library name
// with the localIds and result values
getAllLocalIds() {
const localIdResults = {};
// Add the localIds and result values from the main library
localIdResults[this.parent.source.library.identifier.id] = {};
localIdResults[this.parent.source.library.identifier.id] = this.localId_context;
// Iterate over support libraries and store localIds
for (const libName in this.library_context) {
const lib = this.library_context[libName];
this.supportLibraryLocalIds(lib, localIdResults);
}
return localIdResults;
}
// Recursive function that will grab nested support library localId results
supportLibraryLocalIds(lib, localIdResults) {
// Set library identifier name as the key and the object of localIds with their results as the value
// if it already exists then we need to merge the results instead of overwriting
if (localIdResults[lib.library.source.library.identifier.id] != null) {
this.mergeLibraryLocalIdResults(localIdResults, lib.library.source.library.identifier.id, lib.localId_context);
}
else {
localIdResults[lib.library.source.library.identifier.id] = lib.localId_context;
}
// Iterate over any support libraries in the current support library
Object.values(lib.library_context).forEach(supportLib => {
this.supportLibraryLocalIds(supportLib, localIdResults);
});
}
// Merges the localId results for a library into the already collected results. The logic used for which result
// to keep is the same as the logic used above in setLocalIdWithResult, "falsey" results are always replaced.
mergeLibraryLocalIdResults(localIdResults, libraryId, libraryResults) {
for (const localId in libraryResults) {
const localIdResult = libraryResults[localId];
const existingResult = localIdResults[libraryId][localId];
// overwrite this localid result if the existing result is "falsey". future work could track all results for each localid
if (existingResult === false ||
existingResult === null ||
existingResult === undefined ||
existingResult.length === 0) {
localIdResults[libraryId][localId] = localIdResult;
}
}
}
checkParameters(params) {
for (const pName in params) {
const pVal = params[pName];
const pDef = this.getParameter(pName);
if (pVal == null) {
return; // Null can theoretically be any type
}
if (typeof pDef === 'undefined') {
return; // This will happen if the parameter is declared in a different (included) library
}
else if (pDef.parameterTypeSpecifier != null &&
!this.matchesTypeSpecifier(pVal, pDef.parameterTypeSpecifier)) {
throw new Error(`Passed in parameter '${pName}' is wrong type`);
}
else if (pDef['default'] != null && !this.matchesInstanceType(pVal, pDef['default'])) {
throw new Error(`Passed in parameter '${pName}' is wrong type`);
}
}
return true;
}
matchesTypeSpecifier(val, spec) {
switch (spec.type) {
case 'NamedTypeSpecifier':
return this.matchesNamedTypeSpecifier(val, spec);
case 'ListTypeSpecifier':
return this.matchesListTypeSpecifier(val, spec);
case 'TupleTypeSpecifier':
return this.matchesTupleTypeSpecifier(val, spec);
case 'IntervalTypeSpecifier':
return this.matchesIntervalTypeSpecifier(val, spec);
case 'ChoiceTypeSpecifier':
return this.matchesChoiceTypeSpecifier(val, spec);
default:
return true; // default to true when we don't know
}
}
matchesListTypeSpecifier(val, spec) {
return ((0, util_1.typeIsArray)(val) && val.every(x => this.matchesTypeSpecifier(x, spec.elementType)));
}
matchesTupleTypeSpecifier(val, spec) {
// TODO: Spec is not clear about exactly how tuples should be matched
return (val != null &&
typeof val === 'object' &&
!(0, util_1.typeIsArray)(val) &&
!val.isInterval &&
!val.isConcept &&
!val.isCode &&
!val.isDateTime &&
!val.isDate &&
!val.isQuantity &&
spec.element.every((x) => typeof val[x.name] === 'undefined' ||
this.matchesTypeSpecifier(val[x.name], x.elementType)));
}
matchesIntervalTypeSpecifier(val, spec) {
return (val.isInterval &&
(val.low == null || this.matchesTypeSpecifier(val.low, spec.pointType)) &&
(val.high == null || this.matchesTypeSpecifier(val.high, spec.pointType)));
}
matchesChoiceTypeSpecifier(val, spec) {
return spec.choice.some((c) => this.matchesTypeSpecifier(val, c));
}
matchesNamedTypeSpecifier(val, spec) {
if (val == null) {
return true;
}
switch (spec.name) {
case '{urn:hl7-org:elm-types:r1}Boolean':
return typeof val === 'boolean';
case '{urn:hl7-org:elm-types:r1}Decimal':
return typeof val === 'number';
case '{urn:hl7-org:elm-types:r1}Integer':
return typeof val === 'number' && Math.floor(val) === val;
case '{urn:hl7-org:elm-types:r1}String':
return typeof val === 'string';
case '{urn:hl7-org:elm-types:r1}Concept':
return val && val.isConcept;
case '{urn:hl7-org:elm-types:r1}Code':
return val && val.isCode;
case '{urn:hl7-org:elm-types:r1}DateTime':
return val && val.isDateTime;
case '{urn:hl7-org:elm-types:r1}Date':
return val && val.isDate;
case '{urn:hl7-org:elm-types:r1}Quantity':
return val && val.isQuantity;
case '{urn:hl7-org:elm-types:r1}Time':
return val && val.isTime && val.isTime();
default:
// Use the data model's implementation of _is, if it is available
if (typeof val._is === 'function') {
return val._is(spec);
}
// If the value is an array or interval, then we assume it cannot be cast to a
// named type. Technically, this is not 100% true because a modelinfo can define
// a named type whose base type is a list or interval. But none of our models
// (FHIR, QDM, QICore) do that, so for those models, this approach will always be
// correct.
if (Array.isArray(val) || val.isInterval) {
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;
}
}
matchesInstanceType(val, inst) {
if (inst.isBooleanLiteral) {
return typeof val === 'boolean';
}
else if (inst.isDecimalLiteral) {
return typeof val === 'number';
}
else if (inst.isIntegerLiteral) {
return typeof val === 'number' && Math.floor(val) === val;
}
else if (inst.isStringLiteral) {
return typeof val === 'string';
}
else if (inst.isCode) {
return val && val.isCode;
}
else if (inst.isConcept) {
return val && val.isConcept;
}
else if (inst.isTime && inst.isTime()) {
return val && val.isTime && val.isTime();
}
else if (inst.isDate) {
return val && val.isDate;
}
else if (inst.isDateTime) {
return val && val.isDateTime;
}
else if (inst.isQuantity) {
return val && val.isQuantity;
}
else if (inst.isList) {
return this.matchesListInstanceType(val, inst);
}
else if (inst.isTuple) {
return this.matchesTupleInstanceType(val, inst);
}
else if (inst.isInterval) {
return this.matchesIntervalInstanceType(val, inst);
}
return true; // default to true when we don't know for sure
}
matchesListInstanceType(val, list) {
return ((0, util_1.typeIsArray)(val) && val.every(x => this.matchesInstanceType(x, list.elements[0])));
}
matchesTupleInstanceType(val, tpl) {
return (typeof val === 'object' &&
!(0, util_1.typeIsArray)(val) &&
tpl.elements.every((x) => typeof val[x.name] === 'undefined' || this.matchesInstanceType(val[x.name], x.value)));
}
matchesIntervalInstanceType(val, ivl) {
const pointType = ivl.low != null ? ivl.low : ivl.high;
return (val.isInterval &&
(val.low == null || this.matchesInstanceType(val.low, pointType)) &&
(val.high == null || this.matchesInstanceType(val.high, pointType)));
}
}
exports.Context = Context;
class PatientContext extends Context {
constructor(library, patient, codeService, parameters, executionDateTime = dt.DateTime.fromJSDate(new Date()), messageListener = new messageListeners_1.NullMessageListener()) {
super(library, codeService, parameters, executionDateTime, messageListener);
this.library = library;
this.patient = patient;
}
rootContext() {
return this;
}
getLibraryContext(library) {
if (this.library_context[library] == null) {
this.library_context[library] = new PatientContext(this.get(library), this.patient, this.codeService, this.parameters, this.executionDateTime, this.messageListener);
}
return this.library_context[library];
}
getLocalIdContext(localId) {
if (this.localId_context[localId] == null) {
this.localId_context[localId] = new PatientContext(this.get(localId), this.patient, this.codeService, this.parameters, this.executionDateTime, this.messageListener);
}
return this.localId_context[localId];
}
async findRecords(profile, retrieveDetails) {
return this.patient && this.patient.findRecords(profile, retrieveDetails);
}
}
exports.PatientContext = PatientContext;
class UnfilteredContext extends Context {
constructor(library, results, codeService, parameters, executionDateTime = dt.DateTime.fromJSDate(new Date()), messageListener = new messageListeners_1.NullMessageListener()) {
super(library, codeService, parameters, executionDateTime, messageListener);
this.library = library;
this.results = results;
}
rootContext() {
return this;
}
async findRecords(_template) {
throw new exception_1.Exception('Retrieves are not currently supported in Unfiltered Context');
}
getLibraryContext(_library) {
throw new exception_1.Exception('Library expressions are not currently supported in Unfiltered Context');
}
get(identifier) {
//First check to see if the identifier is a unfiltered context expression that has already been cached
if (this.context_values[identifier]) {
return this.context_values[identifier];
}
//if not look to see if the library has a unfiltered expression of that identifier
if (this.library.expressions[identifier] &&
this.library.expressions[identifier].context === 'Unfiltered') {
return this.library.expressions[identifier];
}
//lastly attempt to gather all patient level results that have that identifier
// should this compact null values before return ?
return Object.values(this.results.patientResults).map((pr) => pr[identifier]);
}
}
exports.UnfilteredContext = UnfilteredContext;
//# sourceMappingURL=context.js.map