@phema/cql-execution
Version:
An execution framework for the Clinical Quality Language (CQL)
573 lines (488 loc) • 22.2 kB
JavaScript
"use strict";
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var _require = require('../datatypes/exception'),
Exception = _require.Exception;
var _require2 = require('../util/util'),
typeIsArray = _require2.typeIsArray;
var dt = require('../datatypes/datatypes');
var Context = /*#__PURE__*/function () {
function Context(parent) {
var _codeService = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
var _parameters = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
_classCallCheck(this, Context);
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); // not crazy about possibly throwing an error in a constructor, but...
this._parameters = _parameters;
}
_createClass(Context, [{
key: "parameters",
get: function get() {
return this._parameters || this.parent && this.parent.parameters;
},
set: function set(params) {
this.checkParameters(params);
this._parameters = params;
}
}, {
key: "codeService",
get: function get() {
return this._codeService || this.parent && this.parent.codeService;
},
set: function set(cs) {
this._codeService = cs;
}
}, {
key: "withParameters",
value: function withParameters(params) {
this.parameters = params || {};
return this;
}
}, {
key: "withCodeService",
value: function withCodeService(cs) {
this.codeService = cs;
return this;
}
}, {
key: "rootContext",
value: function rootContext() {
if (this.parent) {
return this.parent.rootContext();
} else {
return this;
}
}
}, {
key: "findRecords",
value: function findRecords(profile) {
return this.parent && this.parent.findRecords(profile);
}
}, {
key: "childContext",
value: function childContext() {
var context_values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var ctx = new Context(this);
ctx.context_values = context_values;
return ctx;
}
}, {
key: "getLibraryContext",
value: function getLibraryContext(library) {
return this.parent && this.parent.getLibraryContext(library);
}
}, {
key: "getLocalIdContext",
value: function getLocalIdContext(localId) {
return this.parent && this.parent.getLocalIdContext(localId);
}
}, {
key: "getParameter",
value: function getParameter(name) {
return this.parent && this.parent.getParameter(name);
}
}, {
key: "getParentParameter",
value: function getParentParameter(name) {
if (this.parent) {
if (this.parent.parameters[name] != null) {
return this.parent.parameters[name];
} else {
return this.parent.getParentParameter(name);
}
}
}
}, {
key: "getTimezoneOffset",
value: function 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('No Timezone Offset has been set');
}
}
}, {
key: "getExecutionDateTime",
value: function getExecutionDateTime() {
if (this.executionDateTime != null) {
return this.executionDateTime;
} else if (this.parent && this.parent.getExecutionDateTime != null) {
return this.parent.getExecutionDateTime();
} else {
throw new Exception('No Execution DateTime has been set');
}
}
}, {
key: "getValueSet",
value: function getValueSet(name, library) {
return this.parent && this.parent.getValueSet(name, library);
}
}, {
key: "getCodeSystem",
value: function getCodeSystem(name) {
return this.parent && this.parent.getCodeSystem(name);
}
}, {
key: "getCode",
value: function getCode(name) {
return this.parent && this.parent.getCode(name);
}
}, {
key: "getConcept",
value: function getConcept(name) {
return this.parent && this.parent.getConcept(name);
}
}, {
key: "get",
value: function 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);
}
}
}, {
key: "set",
value: function set(identifier, value) {
this.context_values[identifier] = value;
}
}, {
key: "setLocalIdWithResult",
value: function setLocalIdWithResult(localId, value) {
// Temporary fix. Real fix will be to return a list of all result values for a given localId.
var ctx = this.localId_context[localId];
if (ctx === false || ctx === null || ctx === undefined || ctx.length === 0) {
this.localId_context[localId] = value;
}
}
}, {
key: "getLocalIdResult",
value: function getLocalIdResult(localId) {
return this.localId_context[localId];
} // Returns an object of objects containing each library name
// with the localIds and result values
}, {
key: "getAllLocalIds",
value: function getAllLocalIds() {
var 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 (var libName in this.library_context) {
var lib = this.library_context[libName];
this.supportLibraryLocalIds(lib, localIdResults);
}
return localIdResults;
} // Recursive function that will grab nested support library localId results
}, {
key: "supportLibraryLocalIds",
value: function supportLibraryLocalIds(lib, localIdResults) {
var _this = this;
// 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(function (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.
}, {
key: "mergeLibraryLocalIdResults",
value: function mergeLibraryLocalIdResults(localIdResults, libraryId, libraryResults) {
for (var localId in libraryResults) {
var localIdResult = libraryResults[localId];
var existingResult = localIdResults[libraryId][localId]; // overwite 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;
}
}
}
}, {
key: "checkParameters",
value: function checkParameters(params) {
for (var pName in params) {
var pVal = params[pName];
var 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 '".concat(pName, "' is wrong type"));
} else if (pDef['default'] != null && !this.matchesInstanceType(pVal, pDef['default'])) {
throw new Error("Passed in parameter '".concat(pName, "' is wrong type"));
}
}
return true;
}
}, {
key: "matchesTypeSpecifier",
value: function 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
}
}
}, {
key: "matchesListTypeSpecifier",
value: function matchesListTypeSpecifier(val, spec) {
var _this2 = this;
return typeIsArray(val) && val.every(function (x) {
return _this2.matchesTypeSpecifier(x, spec.elementType);
});
}
}, {
key: "matchesTupleTypeSpecifier",
value: function matchesTupleTypeSpecifier(val, spec) {
var _this3 = this;
// TODO: Spec is not clear about exactly how tuples should be matched
return val != null && _typeof(val) === 'object' && !typeIsArray(val) && !val.isInterval && !val.isConcept && !val.isCode && !val.isDateTime && !val.isDate && !val.isQuantity && spec.element.every(function (x) {
return typeof val[x.name] === 'undefined' || _this3.matchesTypeSpecifier(val[x.name], x.elementType);
});
}
}, {
key: "matchesIntervalTypeSpecifier",
value: function matchesIntervalTypeSpecifier(val, spec) {
return val.isInterval && (val.low == null || this.matchesTypeSpecifier(val.low, spec.pointType)) && (val.high == null || this.matchesTypeSpecifier(val.high, spec.pointType));
}
}, {
key: "matchesChoiceTypeSpecifier",
value: function matchesChoiceTypeSpecifier(val, spec) {
var _this4 = this;
return spec.choice.some(function (c) {
return _this4.matchesTypeSpecifier(val, c);
});
}
}, {
key: "matchesNamedTypeSpecifier",
value: function 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;
}
}
}, {
key: "matchesInstanceType",
value: function 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
}
}, {
key: "matchesListInstanceType",
value: function matchesListInstanceType(val, list) {
var _this5 = this;
return typeIsArray(val) && val.every(function (x) {
return _this5.matchesInstanceType(x, list.elements[0]);
});
}
}, {
key: "matchesTupleInstanceType",
value: function matchesTupleInstanceType(val, tpl) {
var _this6 = this;
return _typeof(val) === 'object' && !typeIsArray(val) && tpl.elements.every(function (x) {
return typeof val[x.name] === 'undefined' || _this6.matchesInstanceType(val[x.name], x.value);
});
}
}, {
key: "matchesIntervalInstanceType",
value: function matchesIntervalInstanceType(val, ivl) {
var 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));
}
}]);
return Context;
}();
var PatientContext = /*#__PURE__*/function (_Context) {
_inherits(PatientContext, _Context);
var _super = _createSuper(PatientContext);
function PatientContext(library, patient, codeService, parameters) {
var _this7;
var executionDateTime = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : dt.DateTime.fromJSDate(new Date());
_classCallCheck(this, PatientContext);
_this7 = _super.call(this, library, codeService, parameters);
_this7.library = library;
_this7.patient = patient;
_this7.executionDateTime = executionDateTime;
return _this7;
}
_createClass(PatientContext, [{
key: "rootContext",
value: function rootContext() {
return this;
}
}, {
key: "getLibraryContext",
value: function 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);
}
return this.library_context[library];
}
}, {
key: "getLocalIdContext",
value: function 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);
}
return this.localId_context[localId];
}
}, {
key: "findRecords",
value: function findRecords(profile) {
return this.patient && this.patient.findRecords(profile);
}
}]);
return PatientContext;
}(Context);
var UnfilteredContext = /*#__PURE__*/function (_Context2) {
_inherits(UnfilteredContext, _Context2);
var _super2 = _createSuper(UnfilteredContext);
function UnfilteredContext(library, results, codeService, parameters) {
var _this8;
var executionDateTime = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : dt.DateTime.fromJSDate(new Date());
_classCallCheck(this, UnfilteredContext);
_this8 = _super2.call(this, library, codeService, parameters);
_this8.library = library;
_this8.results = results;
_this8.executionDateTime = executionDateTime;
return _this8;
}
_createClass(UnfilteredContext, [{
key: "rootContext",
value: function rootContext() {
return this;
}
}, {
key: "findRecords",
value: function findRecords(template) {
throw new Exception('Retreives are not currently supported in Unfiltered Context');
}
}, {
key: "getLibraryContext",
value: function getLibraryContext(library) {
throw new Exception('Library expressions are not currently supported in Unfiltered Context');
}
}, {
key: "get",
value: function 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[identifier] && this.library[identifier].context === 'Unfiltered') {
return this.library.expressions[identifier];
} //lastley 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(function (pr) {
return pr[identifier];
});
}
}]);
return UnfilteredContext;
}(Context);
module.exports = {
Context: Context,
PatientContext: PatientContext,
UnfilteredContext: UnfilteredContext
};