fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
268 lines • 13.1 kB
JavaScript
;
/**
* © Copyright Outburn Ltd. 2022-2024 All Rights Reserved
* Project name: FUME-COMMUNITY
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.castToFhir = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const uuid_by_string_1 = tslib_1.__importDefault(require("uuid-by-string"));
const config_1 = tslib_1.__importDefault(require("../../config"));
const conformance_1 = require("../conformance");
const jsonataExpression_1 = tslib_1.__importDefault(require("../jsonataExpression"));
const jsonataFunctions_1 = require("../jsonataFunctions");
const logger_1 = require("../logger");
const thrower_1 = tslib_1.__importDefault(require("../thrower"));
;
const dev = process.env.NODE_ENV === 'dev';
const primitiveParsers = {}; // cache for primitive value testing functions
const getPrimitiveParser = async (typeName) => {
// returns a function that tests primitives agaist their regex
// returned functions are cached in memory so they are only compiled once
if (primitiveParsers[typeName]) {
// exists in cache, return from there
return primitiveParsers[typeName];
}
else {
// generate and compile the function
let resFn;
const sDef = await (0, jsonataFunctions_1.getStructureDefinition)(typeName, config_1.default.getFhirVersion(), (0, conformance_1.getFhirPackageIndex)(), (0, logger_1.getLogger)());
if (sDef === undefined)
return thrower_1.default.throwRuntimeError(`error fetching structure definition for type ${typeName}`);
const valueElementDef = sDef?.snapshot?.element[3]; // 4th element in a primitive's structdef is always the actual primitive value
// get regular expression string from the standard extension
const regexStr = valueElementDef?.type[0]?.extension?.filter((ext) => ext?.url === 'http://hl7.org/fhir/StructureDefinition/regex')[0]?.valueString;
if (regexStr) {
// found regex, compile it
const fn = new RegExp(`^${regexStr}$`);
resFn = (value) => fn.test(value);
}
else {
// no regex - function will just test for empty strings
resFn = (value) => value.trim() !== '';
}
primitiveParsers[typeName] = resFn; // cache the function
return resFn;
}
};
const testCodingAgainstVS = async (coding, vs) => {
return await jsonataExpression_1.default.testCodingAgainstVS.evaluate({}, { coding, vs });
};
const castToFhir = async (options, input) => {
// this is the function that is called from parsed flash rules. it only runs
// at runtime - when the native jsonata build of the flash script is evaluated
if (dev)
console.log({ castToFhir: exports.castToFhir }, options, input);
if (typeof input === 'undefined')
return {};
const baseType = options?.baseType;
const kind = options?.kind; // primitive-type | complex-type | resource
const elementName = options?.name;
const fixed = options?.fixed;
const res = {};
// handle arrays
const isInputAnArray = Array.isArray(input);
if (isInputAnArray) {
const resArray = [];
for (let i = 0; i < input.length; i++) {
const iterationRes = await (0, exports.castToFhir)(options, input[i]);
resArray.push(iterationRes[elementName]);
}
;
res[elementName] = resArray;
return res;
}
;
if (baseType.startsWith('http://hl7.org/fhirpath/System.')) {
// this is a true (System) primitive
const value = input?.__value; // it is only assined inline in a rule - no children are possible
let testValue; // will be passed to the tester function
if (typeof value === 'string') {
testValue = value;
}
else {
testValue = JSON.stringify(value); // any value other than string is strigified
}
;
const primitiveProfile = options?.jsonPrimitiveProfile; // the fhirtype to take the regex from
// throw error if empty
if (baseType.endsWith('String') && typeof testValue === 'string' && testValue.trim() === '' && !fixed)
return thrower_1.default.throwRuntimeError(`got forbidden empty value in ${options.path}! (type: ${primitiveProfile})`);
// prepare to validate and parse the value
if (primitiveProfile) {
// fetch a tester function
const parser = await getPrimitiveParser(primitiveProfile);
// perform test
if (testValue && parser && !await parser(testValue)) {
// failed the test, throw error
return thrower_1.default.throwRuntimeError(`value '${testValue}' is invalid according to RegEx defined for type ${primitiveProfile} (${options.path})`);
}
}
;
// parse to the correct primitive type
if (['decimal', 'integer', 'positiveInt', 'integer64', 'unsignedInt'].includes(baseType)) {
// numeric primitive - cast as number.
// it should not fail since regex has already been tested
// TODO: how to retain decimal percision in js?? not sure it's possible...
res[elementName] = fixed ?? Number(value);
}
else if (baseType === 'boolean') {
if (typeof value === 'boolean')
res[elementName] = (fixed ?? value); // already boolean, take as-is
if (typeof value === 'string')
res[elementName] = (fixed ?? (value === 'true' ? true : value === 'false' ? false : undefined)); // literal string booleans are accepted
}
else {
// the base type is some specialization of string (date, uri, code etc.)
// regex test has already passed, just take the stringified version if not already a string
if (typeof value === 'string') {
res[elementName] = fixed ?? value; // already a string
}
else {
res[elementName] = fixed ?? testValue; // stringified version of value
}
}
}
else if (kind === 'primitive-type') {
// it's a fhir primitive, so it might have children
const ruleValue = input?.__value; // a value assigned as part of an assignment rule
const literalValue = input?.value; // a value that's been assigned directly to the 'value' property under a primitive's path
const id = input?.id; // an element id
let extension;
try {
// @ts-expect-error
extension = input?.extension || input[Object.keys(input).find(key => key.includes('extension'))]; // any element extensions
}
catch (e) { }
if (literalValue || !lodash_1.default.isNil(ruleValue)) { // has primitive value assigned in expression
// fetch tester function
const parser = await getPrimitiveParser(baseType);
let passToParser;
// value to pass to the tester
// values assigned explicitly to the "value" element override inline assignment values
// e.g., in:
//
// * code = 'a'
// * value = 'b'
//
// => 'b' will override 'a' as the primitive value of the 'code' element
let value = literalValue ?? ruleValue;
if (typeof value === 'string') {
if (!isNaN(new Date(value).getTime()) && options.baseType === 'date') {
value = value.split('T')[0];
}
passToParser = value; // pass as-is, remove time if Datetime
}
else {
// pass a stringified version of value
passToParser = JSON.stringify(value);
}
if (parser && await parser(passToParser)) {
// passed the test
if (options.vsDictionary) {
// it's a primitive that has binding
const vsTest = await jsonataExpression_1.default.testCodeAgainstVS.evaluate({}, { value: passToParser, vs: options.vsDictionary });
if (!vsTest) {
return thrower_1.default.throwRuntimeError(`value '${passToParser}' is invalid for element ${options?.path}. This code is not in the required value set`);
}
}
let resValue; // only possible primitive types
if (['decimal', 'integer', 'positiveInt', 'integer64', 'unsignedInt'].includes(baseType)) {
// numeric in the json
resValue = fixed ?? Number(value);
}
else if (baseType === 'boolean') {
// boolean in the json
if (typeof value === 'boolean')
resValue = fixed ?? value;
if (typeof value === 'string')
resValue = fixed ?? value === 'true';
}
else {
// string in the json
if (typeof value === 'string') {
resValue = fixed ?? value;
}
else {
resValue = fixed ?? passToParser;
}
}
;
res[elementName] = resValue; // set value to a key with the element's name
}
else {
// failed the test, throw error
return thrower_1.default.throwRuntimeError(`value '${passToParser}' failed RegEx defined for type ${baseType} (${options?.path})`);
}
}
;
if (fixed) {
// has fixed primitive value
res[elementName] = fixed;
}
if (id || extension) {
// this primitive has children, they should be placed in an
// object with the same key, but prefixed with underscore
res['_' + elementName] = {
id,
extension
};
}
}
else {
// this is a complex type (or a resource?), so it's an object
// TODO: what happens when it's an inline resource? (contained, bundle)
let resObj = { ...input }; // copy all keys
// value assined inline to the object is currently ignored
// TODO: find a way to handle objects assigned this way (they might contain illegal elements)
if (kind === 'resource' && Object.prototype.hasOwnProperty.call(resObj, '__value') && typeof resObj.__value === 'object' && !Array.isArray(resObj.__value)) {
resObj = { ...resObj.__value };
if (options?.path.startsWith('Bundle')) {
// this is a resource inside a bundle
// need to add a fullUrl
const seed = JSON.stringify(resObj);
// logger.info(seed);
res.fullUrl = 'urn:uuid:' + (0, uuid_by_string_1.default)(seed);
}
}
;
delete resObj.__value; // remove the internal __value key
if ((fixed && Object.keys(fixed).length > 0) || Object.keys(resObj).length > 0) {
if (fixed) {
res[elementName] = { ...fixed, ...resObj };
}
else {
res[elementName] = { ...resObj };
}
}
;
if (options.vsDictionary && options.baseType === 'CodeableConcept') {
// required bindings on CodeableConcept
if (Object.keys(res[elementName]).filter((k) => k.startsWith('coding')).length > 0) {
const vsTest = await jsonataExpression_1.default.testCodeableAgainstVS.evaluate({}, { codeable: res[elementName], vs: options.vsDictionary, testCodingAgainstVS });
if (!vsTest) {
return thrower_1.default.throwRuntimeError(`Element ${options?.path} is invalid since none of the codings provided are in the required value set`);
}
}
}
;
if (options.vsDictionary && (options.baseType === 'Quantity' || options.baseType === 'Coding')) {
if (dev)
console.log(`(castToFhir): Generating ${options.baseType}`, { path: options?.path, name: options?.name });
// required bindings on Quantity or Coding
if (res[elementName]?.system || res[elementName]?.code) {
const vsTest = await testCodingAgainstVS(res[elementName], options.vsDictionary);
if (!vsTest) {
const system = res[elementName]?.system ?? '';
const code = res[elementName]?.code ?? '';
return thrower_1.default.throwRuntimeError(`The code '${system}#${code}' is invalid for element ${options?.path}. This code is not in the required value set`);
}
}
}
}
;
return res;
};
exports.castToFhir = castToFhir;
//# sourceMappingURL=castToFhir.js.map