UNPKG

fume-fhir-converter

Version:

FHIR-Utilized Mapping Engine - Community

252 lines (238 loc) 11.9 kB
/** * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved * Project name: FUME-COMMUNITY */ import _ from 'lodash'; import uuidByString from 'uuid-by-string'; import config from '../../config'; import { getFhirPackageIndex } from '../conformance'; import expressions from '../jsonataExpression'; import { getStructureDefinition } from '../jsonataFunctions'; import { getLogger } from '../logger'; import thrower from '../thrower'; export interface CastToFhirOptions { name: string // name of the element path: string // the path of the element - needed for error messages baseType: string // the type.code from eDef. Would be a FHIR type name or a primitive's System URI kind?: string // the kind of element as defined in the type's StructureDefinition jsonPrimitiveProfile?: string // when baseType is a System primitive, the fhirtype to take contraints from fixed?: any // the fixed[x] or pattern[x] from edef vsDictionary?: any[] // codes for required bindings }; const dev = process.env.NODE_ENV === 'dev'; const primitiveParsers = {}; // cache for primitive value testing functions const getPrimitiveParser = async (typeName: string): Promise<Function | undefined> => { // 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: Function; const sDef = await getStructureDefinition(typeName, config.getFhirVersion(), getFhirPackageIndex(), getLogger()); if (sDef === undefined) return thrower.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: string = valueElementDef?.type[0]?.extension?.filter((ext: any) => ext?.url === 'http://hl7.org/fhir/StructureDefinition/regex')[0]?.valueString; if (regexStr) { // found regex, compile it const fn = new RegExp(`^${regexStr}$`); resFn = (value: string): boolean => fn.test(value); } else { // no regex - function will just test for empty strings resFn = (value: string): boolean => value.trim() !== ''; } primitiveParsers[typeName] = resFn; // cache the function return resFn; } }; const testCodingAgainstVS = async (coding: any, vs: any[]): Promise<string | string[] | undefined> => { return await expressions.testCodingAgainstVS.evaluate({}, { coding, vs }); }; export const castToFhir = async (options: CastToFhirOptions, input: any) => { // 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 }, options, input); if (typeof input === 'undefined') return {}; const baseType: string = options?.baseType; const kind: string | undefined = options?.kind; // primitive-type | complex-type | resource const elementName: string = options?.name; const fixed: any = options?.fixed; const res: { // container object for the key-value and _key/children pairs [k: string]: any } = {}; // handle arrays const isInputAnArray = Array.isArray(input); if (isInputAnArray) { const resArray: any[] = []; for (let i = 0; i < input.length; i++) { const iterationRes: any = await 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: string; // 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: string | undefined = 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.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.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 || !_.isNil(ruleValue)) { // has primitive value assigned in expression // fetch tester function const parser = await getPrimitiveParser(baseType); let passToParser: string; // 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: string = await expressions.testCodeAgainstVS.evaluate({}, { value: passToParser, vs: options.vsDictionary }); if (!vsTest) { return thrower.throwRuntimeError(`value '${passToParser}' is invalid for element ${options?.path}. This code is not in the required value set`); } } let resValue: string | number | boolean; // 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.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: string = JSON.stringify(resObj); // logger.info(seed); res.fullUrl = 'urn:uuid:' + uuidByString(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: string) => k.startsWith('coding')).length > 0) { const vsTest = await expressions.testCodeableAgainstVS.evaluate({}, { codeable: res[elementName], vs: options.vsDictionary, testCodingAgainstVS }); if (!vsTest) { return thrower.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: string = res[elementName]?.system ?? ''; const code: string = res[elementName]?.code ?? ''; return thrower.throwRuntimeError(`The code '${system}#${code}' is invalid for element ${options?.path}. This code is not in the required value set`); } } } }; return res; };