diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09f7e9c..9405d78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,77 +1,77 @@ -# Contributing to FUME Community - -## Bug reports - -If you think you have found a bug in FUME Community, first make sure that you are testing against the latest version - your issue may already have been fixed. If not, search our [issues list on GitHub](https://github.com/Outburn-IL/fume-community/issues) in case a similar issue has already been opened. - -It is very helpful if you can prepare a reproduction of the bug. In other words, provide a small test case which we can run to confirm your bug. It makes it easier to find the problem and to fix it. For example: - -``` -EXAMPLE CODE -``` - -## Feature requests - -If you find yourself wishing for a feature that doesn't exist in FUME Community, please let us know. Check the [issues list on GitHub](https://github.com/Outburn-IL/fume-community/issues) to see if a similar feature has already been requested. If no,open an issue on our issues list on GitHub which describes the feature you would like to see, why you need it, and how it should work. - -## Contributing code and documentation changes - -If you would like to contribute a new feature or a bug fix to FUME Community, please **discuss your idea first on the GitHub issue**. If there is no GitHub issue for your idea, please open one. It may be that somebody is already working on it, or that there are particular complexities that you should know about before starting the implementation. There are often a number of ways to fix a problem and it is important to find the right approach before spending time on a PR that cannot be merged. - -### Fork and clone the repository - -To make changes, you will need to fork this repository and clone it to your local machine. See [GitHub help page](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) for help. - -Once you forked the repository, clone it to your local work station and open it in your preferred editor. - -### Running the server - -1. Install the recommended version as listed in the `.nvmrc` file. (Or use `nvm` or an equivalent version management tool). -2. Go to the `fume-community` folder -3. Install all dependencies - ``` - npm install - ``` -4. Run the development server - ``` - npm run dev - ``` -This will start the server in development mode on `localhost:42420` (the default port). - -5. To change the default port or any other environment variable, you should create a file named `.env`. You can find two sample templates in the repository ([Stateful](https://github.com/Outburn-IL/fume-community/blob/main/.env.example.stateful) \ [Stateless](https://github.com/Outburn-IL/fume-community/blob/main/.env.example.stateless)). - -### Testing your changes - -1. Run unit tests by running the `npm run test:unit` command. -2. Run integration tests by running the `npm run test:integration` command. This will start a local FHIR service - -### Submitting your changes - -1. Comitting changes - - Code goes through a linting check every time you commit. It will automatically fix any issues it can and fail if there are issues that can't be resolved. Please refrain from using `eslint-disable` and if you absolutely must, explain the reasoning behind doing so. - - Commit messages in the repository pass `commitlint` validation. We use the default `Angular` style commit messages. You can read about them [here](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional#type-enum). - -2. Test your changes - - Make sure to add unit and integration tests (where applicable) to verify your changes. - -3. Rebase your changes - - Update your local repository with the most recent code from the main Outburn-IL repository, and rebase your branch on top of the latest main branch. - -3. Push your changes - - Push changes to the repository you forked and create a pull requst to the `Outburn-IL/fume-community` repo. - - The first time you make a PR, one of our maintainers will have to approve tests to run on your PR. After that tests are automatically run for every PR. PRs can be merged only if all tests pass. - -4. Sign the Contributor License Agreement - - If you haven't done so before, our `CLAassistant` will ask you to sign the CLA. - -At this stage, we might ask you for clarifications or changes and once all are resolved we will gladly merge your contribution! - ---- -© 2022-2024 Outburn Ltd. All Rights Reserved. +# Contributing to FUME Community + +## Bug reports + +If you think you have found a bug in FUME Community, first make sure that you are testing against the latest version - your issue may already have been fixed. If not, search our [issues list on GitHub](https://github.com/Outburn-IL/fume-community/issues) in case a similar issue has already been opened. + +It is very helpful if you can prepare a reproduction of the bug. In other words, provide a small test case which we can run to confirm your bug. It makes it easier to find the problem and to fix it. For example: + +``` +EXAMPLE CODE +``` + +## Feature requests + +If you find yourself wishing for a feature that doesn't exist in FUME Community, please let us know. Check the [issues list on GitHub](https://github.com/Outburn-IL/fume-community/issues) to see if a similar feature has already been requested. If no,open an issue on our issues list on GitHub which describes the feature you would like to see, why you need it, and how it should work. + +## Contributing code and documentation changes + +If you would like to contribute a new feature or a bug fix to FUME Community, please **discuss your idea first on the GitHub issue**. If there is no GitHub issue for your idea, please open one. It may be that somebody is already working on it, or that there are particular complexities that you should know about before starting the implementation. There are often a number of ways to fix a problem and it is important to find the right approach before spending time on a PR that cannot be merged. + +### Fork and clone the repository + +To make changes, you will need to fork this repository and clone it to your local machine. See [GitHub help page](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) for help. + +Once you forked the repository, clone it to your local work station and open it in your preferred editor. + +### Running the server + +1. Install the recommended version as listed in the `.nvmrc` file. (Or use `nvm` or an equivalent version management tool). +2. Go to the `fume-community` folder +3. Install all dependencies + ``` + npm install + ``` +4. Run the development server + ``` + npm run dev + ``` +This will start the server in development mode on `localhost:42420` (the default port). + +5. To change the default port or any other environment variable, you should create a file named `.env`. You can find two sample templates in the repository ([Stateful](https://github.com/Outburn-IL/fume-community/blob/main/.env.example.stateful) \ [Stateless](https://github.com/Outburn-IL/fume-community/blob/main/.env.example.stateless)). + +### Testing your changes + +1. Run unit tests by running the `npm run test:unit` command. +2. Run integration tests by running the `npm run test:integration` command. This will start a local FHIR service + +### Submitting your changes + +1. Comitting changes + + Code goes through a linting check every time you commit. It will automatically fix any issues it can and fail if there are issues that can't be resolved. Please refrain from using `eslint-disable` and if you absolutely must, explain the reasoning behind doing so. + + Commit messages in the repository pass `commitlint` validation. We use the default `Angular` style commit messages. You can read about them [here](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional#type-enum). + +2. Test your changes + + Make sure to add unit and integration tests (where applicable) to verify your changes. + +3. Rebase your changes + + Update your local repository with the most recent code from the main Outburn-IL repository, and rebase your branch on top of the latest main branch. + +3. Push your changes + + Push changes to the repository you forked and create a pull requst to the `Outburn-IL/fume-community` repo. + + The first time you make a PR, one of our maintainers will have to approve tests to run on your PR. After that tests are automatically run for every PR. PRs can be merged only if all tests pass. + +4. Sign the Contributor License Agreement + + If you haven't done so before, our `CLAassistant` will ask you to sign the CLA. + +At this stage, we might ask you for clarifications or changes and once all are resolved we will gladly merge your contribution! + +--- +© 2022-2024 Outburn Ltd. All Rights Reserved. diff --git a/package-lock.json b/package-lock.json index 5ba7510..a6dfb62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fume-fhir-converter", - "version": "2.0.11", + "version": "2.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fume-fhir-converter", - "version": "2.0.11", + "version": "2.0.12", "license": "AGPL-3.0", "dependencies": { "axios": "^1.6.7", diff --git a/src/helpers/conformance/loadFhirPackageIndex.ts b/src/helpers/conformance/loadFhirPackageIndex.ts index 02b6075..ebd5cc8 100644 --- a/src/helpers/conformance/loadFhirPackageIndex.ts +++ b/src/helpers/conformance/loadFhirPackageIndex.ts @@ -5,9 +5,9 @@ import fs from 'fs-extra'; import path from 'path'; +import { expressions as expressionsNew } from '../jsonataExpr'; import expressions from '../jsonataExpression'; import { getLogger } from '../logger'; -import { omitKeys } from '../objectFunctions'; import { isNumeric } from '../stringFunctions'; import { getCachedPackageDirs, getCachePackagesPath, getFumeIndexFilePath } from './getCachePath'; @@ -68,7 +68,7 @@ const buildFhirCacheIndex = async () => { }); const bindings = { - omitKeys, + omitKeys: expressionsNew.omitKeys, pathJoin: path.join, require: (filePath: string) => { try { @@ -82,9 +82,10 @@ const buildFhirCacheIndex = async () => { }, isNumeric }; + const packageIndexObject = await expressions.createRawPackageIndexObject.evaluate(packageIndexArray, bindings); - const fixedIndex = await expressions.fixPackageIndexObject.evaluate(packageIndexObject, { isNumeric }); + const fixedIndex = expressionsNew.fixPackageIndexObject(packageIndexObject); return fixedIndex; }; @@ -95,8 +96,8 @@ const parseFhirPackageIndex = async (): Promise => { const dirList: string[] = getCachedPackageDirs(); // eslint-disable-next-line @typescript-eslint/no-var-requires const currentIndex = require(fumeIndexPath); - const currentPackages = await expressions.extractCurrentPackagesFromIndex.evaluate(currentIndex); - const diff: string[] = await expressions.checkPackagesMissingFromIndex.evaluate({ dirList, packages: currentPackages }); + const currentPackages = Object.keys(currentIndex?.packages); + const diff: string[] = expressionsNew.checkPackagesMissingFromIndex(dirList, currentPackages); if (diff.length === 0) { getLogger().info('Global package index file is up-to-date'); return require(fumeIndexPath); diff --git a/src/helpers/conformance/recacheFromServer.ts b/src/helpers/conformance/recacheFromServer.ts index 0fc173e..50e4fb3 100644 --- a/src/helpers/conformance/recacheFromServer.ts +++ b/src/helpers/conformance/recacheFromServer.ts @@ -5,7 +5,7 @@ import config from '../../config'; import { getCache } from '../cache'; import { getFhirClient } from '../fhirServer'; -import expressions from '../jsonataExpression'; +import { expressions } from '../jsonataExpr'; import { transform } from '../jsonataFunctions'; import { getLogger } from '../logger'; @@ -36,7 +36,7 @@ const getNextBundle = async (bundle: Record) => { throw new Error('FUME running in stateless mode. Cannot get next page of search results bundle.'); }; let nextBundle; - const nextLink = await expressions.extractNextLink.evaluate(bundle); + const nextLink = expressions.extractNextLink(bundle); if (typeof nextLink === 'string' && nextLink > '') { nextBundle = await getFhirClient().read(nextLink); } @@ -54,7 +54,7 @@ const fullSearch = async (query: string, params?: Record) => { bundleArray.push(page); page = await getNextBundle(page); }; - const resourceArray = await expressions.bundleToArrayOfResources.evaluate({}, { bundleArray }); + const resourceArray = expressions.bundleToArrayOfResources(bundleArray); return resourceArray; }; @@ -100,12 +100,12 @@ const getAliases = async (createFunc?: Function) => { let aliasResource = await getAliasResource(); if (typeof aliasResource === 'object') { if (typeof aliasResource.resourceType === 'string' && aliasResource.resourceType === 'ConceptMap') { - aliasObject = await expressions.aliasResourceToObject.evaluate(aliasResource); + aliasObject = expressions.aliasResourceToObject(aliasResource); } else { if (createFunc !== undefined) { logger.info('Creating new alias resource...'); aliasResource = await createFunc(); - aliasObject = await expressions.aliasResourceToObject.evaluate(aliasResource); + aliasObject = expressions.aliasResourceToObject(aliasResource); } } }; @@ -120,7 +120,7 @@ const getAllMappings = async (): Promise> => { return {}; }; const allStructureMaps = await fullSearch('StructureMap/', { context: 'http://codes.fume.health|fume' }); - const mappingDict: Record = await expressions.structureMapsToMappingObject.evaluate(allStructureMaps); + const mappingDict: Record = expressions.structureMapsToMappingObject(allStructureMaps); if (Object.keys(mappingDict).length > 0) { logger.info('Loaded the following mappings from server: ' + Object.keys(mappingDict).join(', ')); }; diff --git a/src/helpers/fhirFunctions/literal.ts b/src/helpers/fhirFunctions/literal.ts index a6722f2..4a5c6af 100644 --- a/src/helpers/fhirFunctions/literal.ts +++ b/src/helpers/fhirFunctions/literal.ts @@ -1,11 +1,6 @@ -/** - * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved - * Project name: FUME-COMMUNITY - */ -import expressions from '../jsonataExpression'; -import { searchSingle } from './searchSingle'; +import { expressions } from '../jsonataExpr'; export const literal = async (query: string, params?: Record): Promise => { - const res = await expressions.literal.evaluate({}, { query, searchSingle, params }); + const res = await expressions.literal(query); return res; }; diff --git a/src/helpers/fhirFunctions/resourceId.ts b/src/helpers/fhirFunctions/resourceId.ts index 8dbfb35..26ea6a9 100644 --- a/src/helpers/fhirFunctions/resourceId.ts +++ b/src/helpers/fhirFunctions/resourceId.ts @@ -4,9 +4,9 @@ */ import { searchSingle } from './searchSingle'; -export const resourceId = async (query: string, params?: Record): Promise => { +export const resourceId = (query: string, params?: Record): string | undefined => { // fork: os - const resource = await searchSingle(query, params); + const resource: any = searchSingle(query, params); let resourceId: string | undefined; if (resource === undefined) { resourceId = undefined; diff --git a/src/helpers/fhirFunctions/searchSingle.ts b/src/helpers/fhirFunctions/searchSingle.ts index 0bfc7f9..b4b1349 100644 --- a/src/helpers/fhirFunctions/searchSingle.ts +++ b/src/helpers/fhirFunctions/searchSingle.ts @@ -3,7 +3,7 @@ * Project name: FUME-COMMUNITY */ import { getFhirClient } from '../fhirServer'; -import expressions from '../jsonataExpression'; +import { expressions } from '../jsonataExpr'; export const searchSingle = async (query: string, params?: Record): Promise => { const url: string = encodeURI(query); @@ -18,6 +18,6 @@ export const searchSingle = async (query: string, params?: Record): }; const bundle = await getFhirClient().search(url, options); - const res = await expressions.searchSingle.evaluate({}, { bundle }); + const res = expressions.searchSingle(bundle); return res; }; diff --git a/src/helpers/fhirFunctions/translateCode.ts b/src/helpers/fhirFunctions/translateCode.ts index e496304..857b62b 100644 --- a/src/helpers/fhirFunctions/translateCode.ts +++ b/src/helpers/fhirFunctions/translateCode.ts @@ -4,7 +4,7 @@ */ import { getCache } from '../cache'; import { getTable } from '../conformance'; -import expressions from '../jsonataExpression'; +import { expressions } from '../jsonataExpr'; import { getLogger } from '../logger'; export const translateCode = async (input: string, tableId: string) => { @@ -27,7 +27,7 @@ export const translateCode = async (input: string, tableId: string) => { if (mapFiltered.length === 1) { result = mapFiltered[0].code; } else { - result = await expressions.translateCodeExtract.evaluate({}, { mapFiltered }); + result = expressions.translateCodeExtract(mapFiltered); } } return result; diff --git a/src/helpers/fhirFunctions/translateCoding.ts b/src/helpers/fhirFunctions/translateCoding.ts index 8f5df97..cb83e63 100644 --- a/src/helpers/fhirFunctions/translateCoding.ts +++ b/src/helpers/fhirFunctions/translateCoding.ts @@ -3,7 +3,7 @@ * Project name: FUME-COMMUNITY */ import { getCache } from '../cache'; -import expressions from '../jsonataExpression'; +import { expressions } from '../jsonataExpr'; import { getLogger } from '../logger'; export const translateCoding = async (input, tableId) => { @@ -22,7 +22,7 @@ export const translateCoding = async (input, tableId) => { } } - const coding = await expressions.translateCodingExtract.evaluate({}, { result, input }); + const coding = expressions.translateCodingExtract(result, input); return coding; } catch (error) { getLogger().error({ error }); diff --git a/src/helpers/hl7v2/v2normalizeKey.test.ts b/src/helpers/hl7v2/v2normalizeKey.test.ts index 2d209cb..701acc3 100644 --- a/src/helpers/hl7v2/v2normalizeKey.test.ts +++ b/src/helpers/hl7v2/v2normalizeKey.test.ts @@ -17,18 +17,18 @@ describe('v2normalizeKey', () => { }); test('normalizes key', async () => { - const res = await v2normalizeKey('hello'); + const res = v2normalizeKey('hello'); expect(res).toBe('Hello'); }); test('stores normalized key', async () => { - await v2normalizeKey('hello'); + v2normalizeKey('hello'); expect(v2keyMap.get('hello')).toBe('Hello'); }); test('returns from cache, if exists in cache.v2keyMap', async () => { v2keyMap.set('hello', 'Hello2'); - const res = await v2normalizeKey('hello'); + const res = v2normalizeKey('hello'); expect(res).toBe('Hello2'); }); }); diff --git a/src/helpers/hl7v2/v2normalizeKey.ts b/src/helpers/hl7v2/v2normalizeKey.ts index 1b6233d..c31ea2e 100644 --- a/src/helpers/hl7v2/v2normalizeKey.ts +++ b/src/helpers/hl7v2/v2normalizeKey.ts @@ -1,18 +1,6 @@ -/** - * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved - * Project name: FUME-COMMUNITY - */ -import { getCache } from '../cache'; -import expressions from '../jsonataExpression'; -import { initCap } from '../stringFunctions'; -import { registerV2key } from './registerV2key'; +import { expressions } from '../jsonataExpr'; -export const v2normalizeKey = async (key: string) => { - const bindings = { - initCap, - keyMap: getCache().v2keyMap.getDict(), - registerV2key - }; - const res = await expressions.v2normalizeKey.evaluate(key, bindings); +export const v2normalizeKey = (key: string) => { + const res = expressions.v2normalizeKey(key); return res; }; diff --git a/src/helpers/jsonataExpr/aliasResourceToObject.ts b/src/helpers/jsonataExpr/aliasResourceToObject.ts new file mode 100644 index 0000000..183d412 --- /dev/null +++ b/src/helpers/jsonataExpr/aliasResourceToObject.ts @@ -0,0 +1,13 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +// returns the diff between opening and closing parenthesis in a single line +export const aliasResourceToObject = (aliasResource: any): object => { + const finalObj = {}; + aliasResource?.group[0]?.element.forEach(e => { + finalObj[e.code] = e.target[0].code; + }); + return finalObj; +}; diff --git a/src/helpers/jsonataExpr/bundleToArrayOfResources.ts b/src/helpers/jsonataExpr/bundleToArrayOfResources.ts new file mode 100644 index 0000000..89b79b4 --- /dev/null +++ b/src/helpers/jsonataExpr/bundleToArrayOfResources.ts @@ -0,0 +1,12 @@ +/** + * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved + * Project name: FUME-COMMUNITY + */ + +import _ from 'lodash'; + +// returns the diff between opening and closing parenthesis in a single line +export const bundleToArrayOfResources = (bundleArray): any => { + const arr = bundleArray.map(entry => _.get(entry, ['entry', '0', 'resource'])); + return !arr.filter(Boolean).length ? [] : arr; +}; diff --git a/src/helpers/jsonataExpr/checkPackagesMissingFromIndex.ts b/src/helpers/jsonataExpr/checkPackagesMissingFromIndex.ts new file mode 100644 index 0000000..64cec69 --- /dev/null +++ b/src/helpers/jsonataExpr/checkPackagesMissingFromIndex.ts @@ -0,0 +1,13 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const checkPackagesMissingFromIndex = (dirList: any, currentPackages: any): any => { + const fixedDirList = dirList.map(d => d.replace('#', '@')); + + const missingFromIndex = currentPackages.filter(pkg => !fixedDirList.includes(pkg)); + const missingFromCache = fixedDirList.filter(pkg => !currentPackages.includes(pkg)); + + return [...missingFromIndex, ...missingFromCache]; +}; diff --git a/src/helpers/jsonataExpr/conceptMapToTable.ts b/src/helpers/jsonataExpr/conceptMapToTable.ts new file mode 100644 index 0000000..50c72a1 --- /dev/null +++ b/src/helpers/jsonataExpr/conceptMapToTable.ts @@ -0,0 +1,28 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const conceptMapToTable = (map: any): any => { + const cm = (map.resourceType === 'Bundle' ? [map.entry[0].resource] : [map]); + + return cm.flatMap((item, i) => { + const codes = cm[i].group.element.map(element => element.code); + const distinctCodes = [...new Set(codes)]; + + return distinctCodes.map((code: any) => { + const targets = cm[i].group.element.filter(element => element.code === code) + .flatMap(element => element.target) + .filter(target => ['equivalent', 'equal', 'wider', 'subsumes', 'relatedto'].includes(target?.equivalence)) + .map(target => ({ + code: target?.code, + source: target?.source?.source?.source, + target: target?.target?.source?.source + })); + + return { + [code]: targets + }; + }); + }); +}; diff --git a/src/helpers/jsonataExpr/constructLineIterator.ts b/src/helpers/jsonataExpr/constructLineIterator.ts new file mode 100644 index 0000000..bfc1e87 --- /dev/null +++ b/src/helpers/jsonataExpr/constructLineIterator.ts @@ -0,0 +1,17 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +// returns the diff between opening and closing parenthesis in a single line +export const constructLineIterator = async (nodes: string[], + constructLine: Function, + prefix: string, + value: string = '', + context: string = ''): Promise => { + const first = await constructLine(prefix, nodes[0], '', context, true); + const middle = await Promise.all(nodes.slice(1, -1).map(node => constructLine('', node, '', '', true))); + const last = await constructLine('', nodes[nodes.length - 1], value, '', false); + + return [first, ...middle, last].join(''); +}; diff --git a/src/helpers/jsonataExpr/createRawPackageIndexObject.ts b/src/helpers/jsonataExpr/createRawPackageIndexObject.ts new file mode 100644 index 0000000..4ea6ebe --- /dev/null +++ b/src/helpers/jsonataExpr/createRawPackageIndexObject.ts @@ -0,0 +1,97 @@ +/** + * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved + * Project name: FUME-COMMUNITY + */ + +import fs from 'fs'; +import _ from 'lodash'; +import path from 'path'; + +import { omitKeys } from '../objectFunctions'; + +const packageReplace = (pkg) => pkg.replace('#', '@'); + +export const createRawPackageIndexObject = (packageIndexArray: any): any => { + const packages = packageIndexArray.map(async (pkg) => omitKeys(pkg, ['package', 'packageIndex'])); + + const files = packageIndexArray.flatMap((pkg) => + pkg.packageIndex.files.filter( + (file) => + ['StructureDefinition', 'ValueSet', 'CodeSystem', 'ConceptMap'].includes(file.resourceType) + ).map((file) => { + const fullPath = path.join(pkg.path, file.filename); + const actualFile = JSON.parse(fs.readFileSync(fullPath, 'utf8')); + const fhirVersion = actualFile.fhirVersion || pkg.packageManifest.fhirVersions || pkg.packageManifest['fhir-version-list']; + const resourceName = typeof actualFile.name === 'string' ? actualFile.name : undefined; + + return { + packageId: packageReplace(pkg.package), + packageName: pkg.packageManifest.name, + packageVersion: pkg.packageManifest.version, + filename: file.filename, + path: fullPath, + fhirVersion, + resourceType: file.resourceType, + id: file.id, + url: file.url, + name: resourceName, + version: file.version, + kind: file.kind, + type: file.type, + baseDefinition: actualFile.baseDefinition, + derivation: actualFile.derivation, + date: actualFile.date + }; + }) + ); + + const structureDefinitions = files.filter((file) => file.resourceType === 'StructureDefinition'); + const valueSets = files.filter((file) => file.resourceType === 'ValueSet'); + const codeSystems = files.filter((file) => file.resourceType === 'CodeSystem'); + const conceptMaps = files.filter((file) => file.resourceType === 'ConceptMap'); + + const fhirVersions = [...new Set(files.map((file) => file ? file.fhirVersion : undefined))]; + + const minorVersions = [...new Set(fhirVersions.map((version: any) => `${_.split(version, '.')[0]}.${_.split(version, '.')[1]}`))]; + + return minorVersions.map((mv) => { + const filteredFiles = files.filter((file) => `${_.split(file.fhirVersion, '.')[0]}.${_.split(file.fhirVersion, '.')[1] === mv}`); + + return { + packages, + files: filteredFiles, + structureDefinitions: { + byUrl: structureDefinitions.filter((def) => `${_.split(def.fhirVersion, '.')[0]}.${_.split(def.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.url]: curr.path, [`${curr.url}|${curr.version}`]: curr.path }), {}), + byId: structureDefinitions.filter((def) => `${_.split(def.fhirVersion, '.')[0]}.${_.split(def.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.id]: curr.path, [`${curr.id}|${curr.version}`]: curr.path }), {}), + byName: structureDefinitions.filter((def) => `${_.split(def.fhirVersion, '.')[0]}.${_.split(def.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.name]: curr.path, [`${curr.name}|${curr.version}`]: curr.path }), {}) + }, + codeSystems: { + byUrl: codeSystems.filter((cs) => `${_.split(cs.fhirVersion, '.')[0]}.${_.split(cs.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.url]: curr.path, [`${curr.url}|${curr.version}`]: curr.path }), {}), + byId: codeSystems.filter((cs) => `${_.split(cs.fhirVersion, '.')[0]}.${_.split(cs.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.id]: curr.path, [`${curr.id}|${curr.version}`]: curr.path }), {}), + byName: codeSystems.filter((cs) => `${_.split(cs.fhirVersion, '.')[0]}.${_.split(cs.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.name]: curr.path, [`${curr.name}|${curr.version}`]: curr.path }), {}) + }, + valueSets: { + byUrl: valueSets.filter((vs) => `${_.split(vs.fhirVersion, '.')[0]}.${_.split(vs.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.url]: curr.path, [`${curr.url}|${curr.version}`]: curr.path }), {}), + byId: valueSets.filter((vs) => `${_.split(vs.fhirVersion, '.')[0]}.${_.split(vs.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.id]: curr.path, [`${curr.id}|${curr.version}`]: curr.path }), {}), + byName: valueSets.filter((vs) => `${_.split(vs.fhirVersion, '.')[0]}.${_.split(vs.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.name]: curr.path, [`${curr.name}|${curr.version}`]: curr.path }), {}) + }, + conceptMaps: { + byUrl: conceptMaps.filter((cm) => `${_.split(cm.fhirVersion, '.')[0]}.${_.split(cm.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.url]: curr.path, [`${curr.url}|${curr.version}`]: curr.path }), {}), + byId: conceptMaps.filter((cm) => `${_.split(cm.fhirVersion, '.')[0]}.${_.split(cm.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.id]: curr.path, [`${curr.id}|${curr.version}`]: curr.path }), {}), + byName: conceptMaps.filter((cm) => `${_.split(cm.fhirVersion, '.')[0]}.${_.split(cm.fhirVersion, '.')[1]}` === mv) + .reduce((acc, curr) => ({ ...acc, [curr.name]: curr.path, [`${curr.name}|${curr.version}`]: curr.path }), {}) + } + }; + }); +}; diff --git a/src/helpers/jsonataExpr/duplicate.ts b/src/helpers/jsonataExpr/duplicate.ts new file mode 100644 index 0000000..217eea8 --- /dev/null +++ b/src/helpers/jsonataExpr/duplicate.ts @@ -0,0 +1,8 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const duplicate = (times: number, str: string): string => { + return str.repeat(times); +}; diff --git a/src/helpers/jsonataExpr/extractNextLink.ts b/src/helpers/jsonataExpr/extractNextLink.ts new file mode 100644 index 0000000..9ace372 --- /dev/null +++ b/src/helpers/jsonataExpr/extractNextLink.ts @@ -0,0 +1,8 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const extractNextLink = (bundle: any): any => { + return bundle?.link.find(link => link.relation === 'next')?.url; +}; diff --git a/src/helpers/jsonataExpr/fixPackageIndexObject.ts b/src/helpers/jsonataExpr/fixPackageIndexObject.ts new file mode 100644 index 0000000..ea4f55c --- /dev/null +++ b/src/helpers/jsonataExpr/fixPackageIndexObject.ts @@ -0,0 +1,108 @@ +/** + * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved + * Project name: FUME-COMMUNITY + */ + +// returns the diff between opening and closing parenthesis in a single line +export const fixPackageIndexObject = (packageIndexObject: any): any => { + const splitVersionId = (versionId) => { + const parts = (versionId ?? '').split('.'); + const major = parts[0]; + const minor = parts[1]; + const patch = (parts[2] ?? '').split('-')[0]; + const label = (parts[2] ?? '').split('-')[1] || ''; + return { + id: versionId, + major: !isNaN(major) ? parseInt(major) : 0, + minor: !isNaN(minor) ? parseInt(minor) : 0, + patch: !isNaN(patch) ? parseInt(patch) : 0, + label: isNaN(major) ? major : (label !== patch ? label : '') + }; + }; + + const splitPackageVersion = (filesEntry) => { + return { + ...filesEntry, + packageVersion: splitVersionId(filesEntry?.packageVersion) + }; + }; + + const bestFileByUrl = (filePaths) => { + const filesEntries = filePaths.map(fp => splitPackageVersion(packageIndexObject[fp])); + const sortedEntries = filesEntries.sort((a, b) => { + if (a?.date > b?.date) return -1; + if (a?.date < b?.date) return 1; + if (a?.packageName !== b?.packageName) return a?.packageName.localeCompare(b?.packageName); + if (a?.packageVersion.major !== b?.packageVersion.major) return b?.packageVersion.major - a?.packageVersion.major; + if (a?.packageVersion.minor !== b?.packageVersion.minor) return b?.packageVersion.minor - a?.packageVersion.minor; + if (a?.packageVersion.patch !== b?.packageVersion.patch) return b?.packageVersion.patch - a?.packageVersion.patch; + return a?.packageVersion.label.localeCompare(b?.packageVersion.label); + }); + return sortedEntries[0].path; + }; + + const bestFileById = (filePaths) => { + const urls = Array.from(new Set(filePaths.map(fp => packageIndexObject[fp]?.url))); + return urls.map(url => bestFileByUrl(filePaths.filter(fp => packageIndexObject[fp]?.url === url))); + }; + + const bestFileByName = (filePaths) => { + return bestFileByUrl(filePaths); + }; + + const getDuplicates = (obj) => { + return Object.keys(obj).filter(key => Array.isArray(obj[key])); + }; + + const fixTypeByUrl = (byUrl) => { + const dups = getDuplicates(byUrl); + const dupsFixed = {}; + dups.forEach(url => { + dupsFixed[url] = bestFileByUrl(dups[url]); + }); + return { ...byUrl, ...dupsFixed }; + }; + + const fixTypeById = (byId) => { + const dups = getDuplicates(byId); + const dupsFixed = {}; + dups.forEach(id => { + dupsFixed[id] = bestFileById(byId[id]); + }); + return { ...byId, ...dupsFixed }; + }; + + const fixTypeByName = (byName) => { + const dups = getDuplicates(byName); + const dupsFixed = {}; + dups.forEach(name => { + dupsFixed[name] = bestFileByName(byName[name]); + }); + return { ...byName, ...dupsFixed }; + }; + + const fixType = (typeObj) => { + return { + byUrl: fixTypeByUrl(typeObj?.byUrl), + byId: fixTypeById(typeObj?.byId), + byName: fixTypeByName(typeObj?.byName) + }; + }; + + const fixVersion = (versionObj) => { + const types = Object.keys(versionObj); + const fixedVersion = {}; + types.forEach(type => { + fixedVersion[type] = fixType(versionObj[type]); + }); + return fixedVersion; + }; + + return Object.keys(packageIndexObject).filter(key => !['packages', 'files'].includes(key)).map(version => ({ + [version]: { + packages: packageIndexObject?.packages, + files: packageIndexObject?.files, + ...fixVersion(packageIndexObject[version]) + } + })); +}; diff --git a/src/helpers/jsonataExpr/index.ts b/src/helpers/jsonataExpr/index.ts new file mode 100644 index 0000000..356bbac --- /dev/null +++ b/src/helpers/jsonataExpr/index.ts @@ -0,0 +1,48 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +import { aliasResourceToObject } from './aliasResourceToObject'; +import { bundleToArrayOfResources } from './bundleToArrayOfResources'; +import { checkPackagesMissingFromIndex } from './checkPackagesMissingFromIndex'; +import { conceptMapToTable } from './conceptMapToTable'; +import { constructLineIterator } from './constructLineIterator'; +import { createRawPackageIndexObject } from './createRawPackageIndexObject'; +import { duplicate } from './duplicate'; +import { extractNextLink } from './extractNextLink'; +import { fixPackageIndexObject } from './fixPackageIndexObject'; +import { initCap } from './initCap'; +import { literal } from './literal'; +import { omitKeys } from './omitKeys'; +import { parseFumeExpression } from './parseFumeExpression'; +import { searchSingle } from './searchSingle'; +import { selectKeys } from './selectKeys'; +import { structureMapsToMappingObject } from './structureMapsToMappingObject'; +import { translateCodeExtract } from './translateCodeExtract'; +import { translateCodingExtract } from './translateCodingExtract'; +import { v2json } from './v2json'; +import { v2normalizeKey } from './v2normalizeKey'; + +export const expressions = { + translateCodeExtract, + translateCodingExtract, + searchSingle, + literal, + initCap, + duplicate, + selectKeys, + omitKeys, + v2normalizeKey, + v2json, + parseFumeExpression, + constructLineIterator, + extractNextLink, + bundleToArrayOfResources, + structureMapsToMappingObject, + aliasResourceToObject, + conceptMapToTable, + createRawPackageIndexObject, + fixPackageIndexObject, + checkPackagesMissingFromIndex +}; diff --git a/src/helpers/jsonataExpr/initCap.ts b/src/helpers/jsonataExpr/initCap.ts new file mode 100644 index 0000000..687cd0f --- /dev/null +++ b/src/helpers/jsonataExpr/initCap.ts @@ -0,0 +1,11 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const initCap = (string: string): string => { + if (!string) return ''; + const words = string.trim().split(' '); + const capitalizedWords = words.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`); + return capitalizedWords.join(' '); +}; diff --git a/src/helpers/jsonataExpr/literal.ts b/src/helpers/jsonataExpr/literal.ts new file mode 100644 index 0000000..8dcc5ce --- /dev/null +++ b/src/helpers/jsonataExpr/literal.ts @@ -0,0 +1,18 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ +import { searchSingle } from '../fhirFunctions/searchSingle'; + +// returns the diff between opening and closing parenthesis in a single line +export const literal = async (query: string): Promise => { + const r: any = await searchSingle(query); + if (r.resourceType === 'OperationOutcome') { + throw new Error(String(r)); + } + if (r?.resourceType) { + return `${r.resourceType}/${r.id}`; + } else { + return undefined; + } +}; diff --git a/src/helpers/jsonataExpr/omitKeys.ts b/src/helpers/jsonataExpr/omitKeys.ts new file mode 100644 index 0000000..b226e53 --- /dev/null +++ b/src/helpers/jsonataExpr/omitKeys.ts @@ -0,0 +1,13 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const omitKeys = (obj, skeys): object => { + return Object.keys(obj) + .filter(key => !skeys.includes(key)) + .reduce((resObj, key) => { + resObj[key] = obj[key]; + return resObj; + }, {}); +}; diff --git a/src/helpers/jsonataExpr/parseFumeExpression.ts b/src/helpers/jsonataExpr/parseFumeExpression.ts new file mode 100644 index 0000000..4428240 --- /dev/null +++ b/src/helpers/jsonataExpr/parseFumeExpression.ts @@ -0,0 +1,21 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +import { splitToLines } from '../stringFunctions'; + +// returns the diff between opening and closing parenthesis in a single line +export const parseFumeExpression = async (expression: string, lineParser: Function): Promise => { + let lines = splitToLines(expression); + lines = lines.filter(line => !(line.trim().startsWith('*') && line.trim().endsWith('undefined '))); + lines.push(''); // Add an empty line at the end + const parsedLines: any = []; + for (let i = 0; i < lines.length; i++) { + const parsedLine = await lineParser(lines[i], i, lines); + parsedLines.push(parsedLine); + } + const result = parsedLines.join('\r\n'); + + return result; +}; diff --git a/src/helpers/jsonataExpr/searchSingle.ts b/src/helpers/jsonataExpr/searchSingle.ts new file mode 100644 index 0000000..411c654 --- /dev/null +++ b/src/helpers/jsonataExpr/searchSingle.ts @@ -0,0 +1,12 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +// returns the diff between opening and closing parenthesis in a single line +export const searchSingle = (bundle: any): any => { + if (!(bundle.total <= 1)) { + throw new Error(`The search ${bundle?.link().find(link => link?.relation === 'self')?.url} returned multiple matches - criteria is not selective enough`); + } + return bundle.entry.find(entry => entry.search.mode === 'match')?.resource; +}; diff --git a/src/helpers/jsonataExpr/selectKeys.ts b/src/helpers/jsonataExpr/selectKeys.ts new file mode 100644 index 0000000..27f8927 --- /dev/null +++ b/src/helpers/jsonataExpr/selectKeys.ts @@ -0,0 +1,13 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const selectKeys = (obj, skeys): object => { + return Object.keys(obj) + .filter(key => skeys.includes(key)) + .reduce((resObj, key) => { + resObj[key] = obj[key]; + return resObj; + }, {}); +}; diff --git a/src/helpers/jsonataExpr/structureMapsToMappingObject.ts b/src/helpers/jsonataExpr/structureMapsToMappingObject.ts new file mode 100644 index 0000000..fd513b0 --- /dev/null +++ b/src/helpers/jsonataExpr/structureMapsToMappingObject.ts @@ -0,0 +1,29 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +export const structureMapsToMappingObject = (data: any): any => { + const filteredData = data.filter(item => + item.resourceType === 'StructureMap' && + item.status === 'active' && + item.useContext.some(context => + context.code.system === 'http://snomed.info/sct' && + context.code.code === '706594005' && + context.valueCodeableConcept.coding.some(coding => + coding.system === 'http://codes.fume.health' && + coding.code === 'fume' + ) + ) + ); + + return filteredData.map(item => ({ + [item.id]: item.group.find(group => + group.name === 'fumeMapping' + ).rule.find(rule => + rule.name === 'evaluate' + ).extension.find(extension => + extension.url === 'http://fhir.fume.health/StructureDefinition/mapping-expression' + ).valueExpression.expression + }))[0]; +}; diff --git a/src/helpers/jsonataExpr/translateCodeExtract.ts b/src/helpers/jsonataExpr/translateCodeExtract.ts new file mode 100644 index 0000000..919293a --- /dev/null +++ b/src/helpers/jsonataExpr/translateCodeExtract.ts @@ -0,0 +1,9 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +// returns the diff between opening and closing parenthesis in a single line +export const translateCodeExtract = (mapFiltered: any): string => { + return mapFiltered.code; +}; diff --git a/src/helpers/jsonataExpr/translateCodingExtract.ts b/src/helpers/jsonataExpr/translateCodingExtract.ts new file mode 100644 index 0000000..e3f1348 --- /dev/null +++ b/src/helpers/jsonataExpr/translateCodingExtract.ts @@ -0,0 +1,15 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +// returns the diff between opening and closing parenthesis in a single line +export const translateCodingExtract = (result: any, input: any): string => { + return result.map(item => [{ + system: item.target, + code: item.code + }, { + system: item.source, + code: input + }]); +}; diff --git a/src/helpers/jsonataExpr/v2json.ts b/src/helpers/jsonataExpr/v2json.ts new file mode 100644 index 0000000..06f9789 --- /dev/null +++ b/src/helpers/jsonataExpr/v2json.ts @@ -0,0 +1,113 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ + +import HL7Dictionary from 'hl7-dictionary'; +import _ from 'lodash'; + +import { getV2DatatypeDef } from '../hl7v2/getV2DatatypeDef'; +import { v2parse } from '../hl7v2/v2parse'; +import { v2normalizeKey } from './v2normalizeKey'; + +export const v2json = (rawString: string): string => { + const rawJson: any = v2parse(rawString); + const v2version: any = rawJson?.segments[0][12]; + + const getV2SegmentDef = (segmentId: string, v2version: string) => { + const segDef = HL7Dictionary.definitions[v2version].segments[segmentId]; + return { segmentId, ...segDef }; + }; + + const dtToIso = (dt) => { + const y = dt.substring(0, 4); + const m = dt.substring(4, 6); + const d = dt.substring(6, 8); + return `${y}-${m}-${d}`; + }; + + const dtmToIso = (dtm) => { + const dt = dtToIso(dtm); + const hh = dtm.substring(8, 10); + const mm = dtm.substring(10, 12); + const ss = dtm.substring(12, 14); + const tm = `${hh !== '' ? hh : '00'}:${mm !== '' ? mm : '00'}:${ss !== '' ? ss : '00'}`; + return `${dt}${tm !== '00:00:00' ? 'T' + tm : ''}`; + }; + + const parseValue = (value, datatype) => { + if (value === '') { + return undefined; + } else { + return datatype === 'DT' ? dtToIso(value) : (datatype === 'DTM' ? dtmToIso(value) : value); + } + }; + + const translateSubfield = (subfield, datatypeDef, sfi) => { + const subfieldDef = datatypeDef.subfields[sfi]; + const subfieldDesc = subfieldDef.desc; + const subfieldDatatype = subfieldDef.datatype; + const sfDataTypeDef = getV2DatatypeDef(subfieldDatatype, v2version); + const isComplex = sfDataTypeDef.subfields.length > 0; + const hasChildren = subfield.fields.length > 0; + + let value; + if (!isComplex) { + value = parseValue(subfield.value, subfieldDatatype); + } else { + if (hasChildren) { + value = subfield.fields.map(subsubfield => translateSubfield(subsubfield, sfDataTypeDef, sfi)) + .reduce((acc, curr) => ({ ...acc, [v2normalizeKey(curr.name)]: curr.value }), {}); + } else { + value = translateSubfield({ value: subfield.value }, sfDataTypeDef, 0); + } + } + + return { + name: subfieldDesc, + value: !_.isEmpty(value) ? value : undefined + }; + }; + + function translateField (field, segDef, fieldIndex) { + const fieldDef = segDef.fields[fieldIndex]; + let fieldDesc = fieldDef.desc; + fieldDesc = typeof fieldDesc === 'string' && fieldDesc.startsWith('Set ID - ') && fieldIndex === 0 ? 'SetID' : fieldDesc; + const fieldDatatype = fieldDef.datatype; + const datatypeDef = getV2DatatypeDef(fieldDatatype, v2version); + const isEnc = segDef.segmentId === 'MSH' && fieldIndex === 1; + const isComplex = datatypeDef.subfields.length > 0; + const hasChildren = field.fields.length > 0; + + let value; + if (isEnc) { + value = field.value; + } else { + if (!isComplex) { + value = parseValue(field.value, fieldDatatype); + } else { + if (hasChildren) { + value = field.fields.map(subfield => translateSubfield(subfield, datatypeDef, 0)) + .reduce((acc, curr) => ({ ...acc, [v2normalizeKey(curr.name)]: curr.value }), {}); + } else { + value = translateSubfield({ value: field.value }, datatypeDef, 0); + } + } + } + value = !_.isEmpty(value) ? value : undefined; + + return { + name: fieldDesc, + value + }; + } + + function translateSegment (segment) { + const segId = segment[0]; + const segDef = getV2SegmentDef(segId, v2version); + return segment.slice(1).map((field, fieldIndex) => translateField(field, segDef, fieldIndex)) + .reduce((acc, curr) => ({ ...acc, [v2normalizeKey(curr.name)]: curr.value, SegmentDescription: segDef.desc }), {}); + } + + return rawJson.segments.map(translateSegment); +}; diff --git a/src/helpers/jsonataExpr/v2normalizeKey.ts b/src/helpers/jsonataExpr/v2normalizeKey.ts new file mode 100644 index 0000000..fda5562 --- /dev/null +++ b/src/helpers/jsonataExpr/v2normalizeKey.ts @@ -0,0 +1,19 @@ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ +import { getCache } from '../cache'; +import { registerV2key } from '../hl7v2/registerV2key'; + +export const v2normalizeKey = (key: string): string => { + const cached = getCache().v2keyMap.get(key); + if (!cached) { + const titleCased = ((key ?? '').replace('\'', '').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('')); + const dtmFixed = (titleCased ?? '').replace('Date/Time', 'DateTime').replace('Date / Time', 'DateTime'); + const underscored = (dtmFixed ?? '').replace(/[-+".()\\//]/g, '_'); + registerV2key(key, underscored); + return underscored; + } else { + return cached; + } +}; diff --git a/src/helpers/jsonataExpression.ts b/src/helpers/jsonataExpression.ts index c8a2a99..99b0c61 100644 --- a/src/helpers/jsonataExpression.ts +++ b/src/helpers/jsonataExpression.ts @@ -1,84 +1,17 @@ -/** - * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved - * Project name: FUME-COMMUNITY - */ +/** + * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved + * Project name: FUME-COMMUNITY + */ import jsonata from 'jsonata'; export interface InternalJsonataExpression { - translateCodeExtract: jsonata.Expression - translateCodingExtract: jsonata.Expression - searchSingle: jsonata.Expression - literal: jsonata.Expression - initCap: jsonata.Expression - duplicate: jsonata.Expression - selectKeys: jsonata.Expression - omitKeys: jsonata.Expression - v2normalizeKey: jsonata.Expression v2json: jsonata.Expression - parseFumeExpression: jsonata.Expression - constructLineIterator: jsonata.Expression - extractNextLink: jsonata.Expression - bundleToArrayOfResources: jsonata.Expression - structureMapsToMappingObject: jsonata.Expression - aliasResourceToObject: jsonata.Expression conceptMapToTable: jsonata.Expression createRawPackageIndexObject: jsonata.Expression - fixPackageIndexObject: jsonata.Expression - extractCurrentPackagesFromIndex: jsonata.Expression - checkPackagesMissingFromIndex: jsonata.Expression - isEmpty: jsonata.Expression }; const expressions: InternalJsonataExpression = { - translateCodeExtract: jsonata('$mapFiltered.code'), - translateCodingExtract: jsonata(`$result.[ - { - 'system': target, - 'code': code - }, - { - 'system': source, - 'code': $input - } - ]`), - searchSingle: jsonata(`( - $assert( - $bundle.total <= 1, - 'The search ' - & $bundle.link[relation='self'].url - & ' returned multiple matches - criteria is not selective enough' - ); - $bundle.entry[search.mode='match'][0].resource - )`), - literal: jsonata(`( - $r := $searchSingle($query, $params); - $r.resourceType = 'OperationOutcome' ? $error($string($r)); - $exists($r.resourceType) ? $r.resourceType & '/' & $r.id : undefined - )`), - initCap: jsonata(`( - $words := $trim($)~>$split(" "); - ($words.$initCapOnce($))~>$join(' ') - )`), - duplicate: jsonata('$join([1..$times].($str))'), - selectKeys: jsonata('$in.$sift($, function($v, $k) {$k in $skeys})'), - omitKeys: jsonata('$in.$sift($, function($v, $k) {($k in $okeys)=false})'), - v2normalizeKey: jsonata(`( - $cached := $lookup($keyMap, $); - $exists($cached) = false - ? ( - $titleCased := ($split($initCap($replace($,"'", '')), ' ')~>$join); - $dtmFixed := $titleCased.$replace('Date/Time', 'DateTime') ~> $replace('Date / Time', 'DateTime'); - `.concat( - // eslint-disable-next-line @typescript-eslint/indent - // eslint-disable-next-line no-useless-escape, @typescript-eslint/indent - `$underscored := $replace($dtmFixed, /[-\+".()\\//]/, '_'); - $registerV2key($, $underscored); - $underscored; - ) - : ($cached); - - )`)), v2json: jsonata(`( $rawJson := $v2parse($); $v2version := $rawJson.segments[0].\`12\`; @@ -196,48 +129,6 @@ const expressions: InternalJsonataExpression = { $s.\`0\`: $ }; )`), - parseFumeExpression: jsonata(` - ( - $lines:=[$splitLineFunc($expr)]; - $lines:=$lines[$not($startsWith($trim($),"*") and $endsWith($, "undefined "))]; - $lines:=$append($lines,""); - $join( - ( - ($lines)#$i.$lineParser($, $i, $lines) - ), - "\r\n" - ) - )` - ), - constructLineIterator: jsonata(` - ( - $first := $nodes[0].$construct($prefix, $, '', $context, true); - $middle := $nodes#$i[$i>0 and $i<($count($nodes)-1)].$construct('', $, '', '', true); - $last := $nodes[-1].$construct('', $, $value, '', false); - $join([ - $first, - $middle, - $last - ]) - )` - ), - extractNextLink: jsonata('link[relation=\'next\'].url'), - bundleToArrayOfResources: jsonata('[$bundleArray.entry.resource]'), - structureMapsToMappingObject: jsonata(` - ($[ - resourceType='StructureMap' - and status='active' - and useContext[ - code.system = 'http://snomed.info/sct' - and code.code = '706594005' - ].valueCodeableConcept.coding[ - system = 'http://codes.fume.health' - ].code = 'fume' - ]){ - id: group[name = 'fumeMapping'].rule[name='evaluate'].extension[url = 'http://fhir.fume.health/StructureDefinition/mapping-expression'].valueExpression.expression - }` - ), - aliasResourceToObject: jsonata('group.element{code: target.code}'), conceptMapToTable: jsonata(`( $cm := (resourceType='Bundle' ? [entry[0].resource] : [$]); @@ -406,141 +297,6 @@ const expressions: InternalJsonataExpression = { ) } ) - )`), - fixPackageIndexObject: jsonata(`( - $splitVersionId := function($versionId) {( - $parts := $split($versionId, '.'); - $major := $parts[0]; - $minor := $parts[1]; - $patch := $substringBefore($parts[2], '-'); - $label := $substringAfter($parts[2], '-'); - { - 'id': $versionId, - 'major': $isNumeric($major) ? $number($major) : 0, - 'minor': $isNumeric($minor) ? $number($minor) : 0, - 'patch': $isNumeric($patch) ? $number($patch) : 0, - 'label': $isNumeric($major)=false ? $major : $label != $patch ? $label : '' - } - )}; - - $splitPackageVersion := function($filesEntry) { - ( - $merge([$filesEntry, {'packageVersion': $splitVersionId($filesEntry.packageVersion)}]) - ) - }; - - $bestFileByUrl := function($filePaths) { - ( - $filesEntries := $filePaths@$fp.$splitPackageVersion($lookup($$.files, $fp)); - $sortedEntries := $filesEntries^(>date, packageName, >packageVersion.major, >packageVersion.minor, >packageVersion.patch, packageVersion.label); - $sortedEntries[0].path; - ) - }; - - $bestFileById := function($filePaths) { - ( - $filesEntries := $filePaths@$fp.$splitPackageVersion($lookup($$.files, $fp)); - $urls := $distinct($filesEntries.url); - $urls@$url.$bestFileByUrl($filesEntries[url=$url].path); - ) - }; - - $bestFileByName := function($filePaths) { - ( - $filesEntries := $filePaths@$fp.$splitPackageVersion($lookup($$.files, $fp)); - $urls := $distinct($filesEntries.url); - $urls@$url.$bestFileByUrl($filesEntries[url=$url].path); - ) - }; - - $getDuplicates := function($obj) { - $sift($obj, function($value) {$type($value) = 'array'}) - }; - - $fixTypeByUrl := function($byUrl) { - ( - $dups := $getDuplicates($byUrl); - $dupsFixed := $keys($dups){ - $: $bestFileByUrl($lookup($dups, $)) - }; - $merge([$byUrl,$dupsFixed]) - ) - }; - - $fixTypeById := function($byId) { - ( - $dups := $getDuplicates($byId); - $dupsFixed := $keys($dups){ - $: $bestFileById($lookup($dups, $)) - }; - $merge([$byId,$dupsFixed]) - ) - }; - - $fixTypeByName := function($byName) { - ( - $dups := $getDuplicates($byName); - $dupsFixed := $keys($dups){ - $: $bestFileByName($lookup($dups, $)) - }; - $merge([$byName,$dupsFixed]) - ) - }; - - $fixType := function($typeObj) { - ( - { - 'byUrl': $fixTypeByUrl($typeObj.byUrl), - 'byId': $fixTypeById($typeObj.byId), - 'byName': $fixTypeByName($typeObj.byName) - } - ) - }; - - $fixVersion := function($versionObj) { - ( - $types := $keys($versionObj); - $types{ - $: $fixType($lookup($versionObj, $)) - } - ) - }; - - $versions := $keys($)[$not($ in ["packages", "files"])]; - - $versions{ - 'packages': $$.packages, - 'files': $$.files, - $: $fixVersion($lookup($$, $)) - }; -)`), - extractCurrentPackagesFromIndex: jsonata('$keys(packages)'), - checkPackagesMissingFromIndex: jsonata(`( - $dirList := dirList.$replace('#', '@'); - - $missingFromIndex := packages[$not($ in $dirList)]; - $missingFromCache := $dirList[$not($ in $$.packages)]; - - [$append($missingFromIndex,$missingFromCache)]; - )`), - isEmpty: jsonata(`( - $_isEmpty := function($input) {( - $exists($input) ? ($input in ['',null] - ? true - : ( - $type($input) = 'object' - ? ( - $count($keys($input)) = 0 ? true - : $count(($keys($input).($lookup($input,$)).$not($_isEmpty($)))[$])=0 - ) - : $type($input) = 'array' - ? ( - $count($input[$_isEmpty($)=false]) = 0 - ) - : false - )) : true - )}; - $_isEmpty($value) )`) }; diff --git a/src/helpers/jsonataFunctions/isEmpty.ts b/src/helpers/jsonataFunctions/isEmpty.ts index 76c5ad7..9c8f576 100644 --- a/src/helpers/jsonataFunctions/isEmpty.ts +++ b/src/helpers/jsonataFunctions/isEmpty.ts @@ -4,7 +4,7 @@ */ import _ from 'lodash'; -export const isEmpty = async (value) => { +export const isEmpty = (value) => { if (value) { const type = typeof value; if (type) { diff --git a/src/helpers/objectFunctions.ts b/src/helpers/objectFunctions.ts index 622fce9..92b2a26 100644 --- a/src/helpers/objectFunctions.ts +++ b/src/helpers/objectFunctions.ts @@ -1,23 +1,23 @@ -/** - * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved - * Project name: FUME-COMMUNITY - */ +/** + * © Copyright Outburn Ltd. 2022-2023 All Rights Reserved + * Project name: FUME + */ +import { expressions } from './jsonataExpr'; +import { isEmpty as isEmp } from './jsonataFunctions/isEmpty'; -import expressions from './jsonataExpression'; - -const selectKeys = async (obj: object, skeys: string[]): Promise => { - const res = await expressions.selectKeys.evaluate({}, { in: obj, skeys }); +const selectKeys = (obj: object, skeys: string[]): object => { + const res = expressions.selectKeys(obj, skeys); return res; }; -const omitKeys = async (obj: object, okeys: string[]): Promise => { - const res = await expressions.omitKeys.evaluate({}, { in: obj, okeys }); +const omitKeys = (obj: object, okeys: string[]): object => { + const res = expressions.omitKeys(obj, okeys); return res; }; -const isEmpty = async (value: any): Promise => { +const isEmpty = (value: any): boolean => { if (value === undefined || value === null) return true; - const res = await expressions.isEmpty.evaluate({}, { value }); + const res = isEmp(value); return res; }; diff --git a/src/helpers/parser/toJsonataString.ts b/src/helpers/parser/toJsonataString.ts index 1e94ab6..05ae99a 100644 --- a/src/helpers/parser/toJsonataString.ts +++ b/src/helpers/parser/toJsonataString.ts @@ -3,16 +3,13 @@ * Project name: FUME-COMMUNITY */ -import expressions from '../jsonataExpression'; +import { expressions } from '../jsonataExpr'; import { funcs } from '../jsonataFuncs'; import { getStructureDefinition } from '../jsonataFunctions'; import { CastToFhirOptions, FlashMergeOptions } from '../runtime'; import { duplicate, - endsWith, initCapOnce, - splitToLines, - startsWith, substringAfter, substringBefore } from '../stringFunctions'; @@ -278,14 +275,7 @@ export const toJsonataString = async (inExpr: string): Promise => { - const bindings = { - expr, - splitLineFunc: splitToLines, - lineParser, - startsWith, - endsWith - }; - const parsed = await expressions.parseFumeExpression.evaluate({}, bindings); + const parsed = await expressions.parseFumeExpression(expr, lineParser); // logger.info({ parsed }); return parsed; }; @@ -402,14 +392,7 @@ export const toJsonataString = async (inExpr: string): Promise 1) { // chained path, iterate through each one prevRuleNodes.push(0); - const bindings = { - nodes, - construct: constructLine, - prefix, - value: value ?? '', - context: context ?? '' - }; - const line = await expressions.constructLineIterator.evaluate({}, bindings); + const line = await expressions.constructLineIterator(nodes, constructLine, prefix, value ?? '', context ?? ''); return line; }; // didn't find any edef diff --git a/src/helpers/stringFunctions/duplicate.ts b/src/helpers/stringFunctions/duplicate.ts index 4e56e61..3f08932 100644 --- a/src/helpers/stringFunctions/duplicate.ts +++ b/src/helpers/stringFunctions/duplicate.ts @@ -3,10 +3,10 @@ * Project name: FUME-COMMUNITY */ -import expressions from '../jsonataExpression'; +import { expressions } from '../jsonataExpr'; -export const duplicate = async (str: string, times: number): Promise => { +export const duplicate = (str: string, times: number): string => { if (times === 1) return str; if (times === 0) return ''; - return await expressions.duplicate.evaluate({}, { times, str }); + return expressions.duplicate(times, str); }; diff --git a/src/helpers/stringFunctions/initCap.ts b/src/helpers/stringFunctions/initCap.ts index e1dd677..532fca4 100644 --- a/src/helpers/stringFunctions/initCap.ts +++ b/src/helpers/stringFunctions/initCap.ts @@ -3,11 +3,10 @@ * Project name: FUME-COMMUNITY */ -import expressions from '../jsonataExpression'; -import { initCapOnce } from './stringFunctions'; +import { expressions } from '../jsonataExpr'; -export const initCap = async (str: string): Promise => { +export const initCap = (str: string): string | undefined => { // fork: os - const res = await expressions.initCap.evaluate(str, { initCapOnce }); + const res = expressions.initCap(str); return res; };