UNPKG

fume-fhir-converter

Version:

FHIR-Utilized Mapping Engine - Community

268 lines 13.1 kB
"use strict"; /** * © 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