From e15c48a50f2787115fd4787821ad396cfe83b0c2 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Sun, 14 Apr 2024 14:09:07 +0300 Subject: [PATCH 1/8] feat: added $capabilities() function --- package-lock.json | 4 ++-- src/helpers/fhirFunctions/capabilities.ts | 27 +++++++++++++++++++++++ src/helpers/fhirFunctions/index.ts | 4 +++- src/helpers/jsonataFunctions/transform.ts | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/helpers/fhirFunctions/capabilities.ts 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/fhirFunctions/capabilities.ts b/src/helpers/fhirFunctions/capabilities.ts new file mode 100644 index 0000000..5920a0e --- /dev/null +++ b/src/helpers/fhirFunctions/capabilities.ts @@ -0,0 +1,27 @@ +/** + * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved + * Project name: FUME-COMMUNITY + */ +import { getFhirClient } from '../fhirServer'; +import thrower from '../thrower'; + +// cached copy of the capability statement resource +let capabilityStatement: Record = {}; + +export const capabilities = async (): Promise | undefined> => { + // check if capability statement cached copy is empty + if (Object.keys(capabilityStatement).length === 0) { + // try to fetch from server + try { + capabilityStatement = await getFhirClient().read('metadata'); + } catch (error) { + return thrower.throwRuntimeError(`Failed to fetch CapabilityStatement from FHIR server. ${JSON.stringify(error)}`); + } + }; + // check that the object is actually a capability statement + if (!capabilityStatement?.resourceType || typeof capabilityStatement.resourceType !== 'string' || capabilityStatement.resourceType !== 'CapabilityStatement') { + return thrower.throwRuntimeError('Invalid response from FHIR server: The \'/metadata\' endpoint did not return a CapabilityStatement resource'); + }; + // return the cached capability statement + return capabilityStatement; +}; diff --git a/src/helpers/fhirFunctions/index.ts b/src/helpers/fhirFunctions/index.ts index 5a672eb..dbbb39d 100644 --- a/src/helpers/fhirFunctions/index.ts +++ b/src/helpers/fhirFunctions/index.ts @@ -2,6 +2,7 @@ * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved * Project name: FUME-COMMUNITY */ +import { capabilities } from './capabilities'; import { fhirVersionToMinor } from './fhirVersionToMinor'; import { literal } from './literal'; import { reference } from './reference'; @@ -21,5 +22,6 @@ export default { translateCoding, translateCode, fhirVersionToMinor, - reference + reference, + capabilities }; diff --git a/src/helpers/jsonataFunctions/transform.ts b/src/helpers/jsonataFunctions/transform.ts index d13bed2..638dfc8 100644 --- a/src/helpers/jsonataFunctions/transform.ts +++ b/src/helpers/jsonataFunctions/transform.ts @@ -90,6 +90,7 @@ export const transform = async (input, expression: string, extraBindings: Record bindings.v2parse = v2.v2parse; bindings.v2json = v2.v2json; bindings.isNumeric = stringFuncs.isNumeric; + bindings.capabilities = fhirFuncs.capabilities; const { aliases } = getCache(); // these are debug functions, should be removed in production versions From d60cb2dae7d92473c1ecc9b837657d753ae28950 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Sun, 26 May 2024 22:54:15 +0300 Subject: [PATCH 2/8] fix: package lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6dfb62..f58eb0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fume-fhir-converter", - "version": "2.0.12", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fume-fhir-converter", - "version": "2.0.12", + "version": "2.1.2", "license": "AGPL-3.0", "dependencies": { "axios": "^1.6.7", From d1d3afe80402c49e26f993cee18696336dfd16c0 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Tue, 28 May 2024 16:33:20 +0300 Subject: [PATCH 3/8] feat: support single line comments and comments inside strings --- src/helpers/jsonataFunctions/transform.ts | 2 + src/helpers/parser/index.ts | 4 +- src/helpers/parser/removeComments.ts | 73 +++++++++++++++++++ src/helpers/parser/toJsonataString.ts | 6 +- src/helpers/stringFunctions/index.ts | 2 - .../stringFunctions/stringFunctions.ts | 6 +- 6 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 src/helpers/parser/removeComments.ts diff --git a/src/helpers/jsonataFunctions/transform.ts b/src/helpers/jsonataFunctions/transform.ts index ffb9214..daa2f05 100644 --- a/src/helpers/jsonataFunctions/transform.ts +++ b/src/helpers/jsonataFunctions/transform.ts @@ -19,6 +19,7 @@ import * as v2 from '../hl7v2'; import { getLogger } from '../logger'; import * as objectFuncs from '../objectFunctions'; import compiler from '../parser'; +import { removeComments } from '../parser/removeComments'; import runtime from '../runtime'; import * as stringFuncs from '../stringFunctions'; import { getStructureDefinition } from './getStructureDefinition'; @@ -105,6 +106,7 @@ export const transform = async (input, expression: string, extraBindings: Record bindings.getMandatoriesOfStructure = compiler.getMandatoriesOfStructure; bindings.getElementDefinition = compiler.getElementDefinition; bindings.replaceColonsWithBrackets = compiler.replaceColonsWithBrackets; + bindings.removeComments = removeComments; // end of debug functions // bind all aliases from cache diff --git a/src/helpers/parser/index.ts b/src/helpers/parser/index.ts index 313af85..bd6223d 100644 --- a/src/helpers/parser/index.ts +++ b/src/helpers/parser/index.ts @@ -7,6 +7,7 @@ import { funcs } from '../jsonataFuncs'; import { replaceColonsWithBrackets } from '../stringFunctions'; import { getElementDefinition } from './getElementDefinition'; import { getSnapshot } from './getSnapshot'; +import { removeComments } from './removeComments'; import { toJsonataString } from './toJsonataString'; export default { @@ -15,5 +16,6 @@ export default { getMandatoriesOfElement: funcs.getMandatoriesOfElement, getMandatoriesOfStructure: funcs.getMandatoriesOfStructure, getElementDefinition, - replaceColonsWithBrackets + replaceColonsWithBrackets, + removeComments }; diff --git a/src/helpers/parser/removeComments.ts b/src/helpers/parser/removeComments.ts new file mode 100644 index 0000000..bd876ff --- /dev/null +++ b/src/helpers/parser/removeComments.ts @@ -0,0 +1,73 @@ +/** + * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved + * Project name: FUME-COMMUNITY + */ + +export const removeComments = (expr: string): string => { + const exprLen: number = expr.length; + if (exprLen === 0) return expr; + let accExpr: string = ''; + let currentChar: string = ''; + let nextChar: string = ''; + let prevChar: string = ''; + let prevPrevChar: string = ''; + let openedQuote: string = ''; + let openedComment: string = ''; + + for (let i = 0; i < exprLen; i++) { + currentChar = expr.charAt(i); + console.log({ currentChar }); + nextChar = i < (exprLen - 1) ? expr.charAt(i + 1) : ''; + prevChar = i > 0 ? expr.charAt(i - 1) : ''; + prevPrevChar = i > 1 ? expr.charAt(i - 2) : ''; + + if (openedComment !== '') { + // inside a comment + console.log('inside a comment'); + console.log({ openedComment }); + const twoChars: string = prevChar + currentChar; + + if (openedComment === '//' && (currentChar === '\r' || currentChar === '\n')) { + // this is the end of the // comment + console.log('end of comment'); + openedComment = ''; + accExpr += '\n'; + } else { + if (openedComment === '/*' && twoChars === '*/' && prevPrevChar !== '/') { + // this is the end of the /* comment + console.log('end of comment'); + openedComment = ''; + } + }; + continue; + }; + + // not inside a comment + if ((currentChar === '"' || currentChar === '\'') && prevChar !== '\\') { + // quote sign, unescaped + if (openedQuote === '') { + // it's an opening quote + openedQuote = currentChar; + } else { + // it's a closing quote + if (openedQuote === currentChar) { + openedQuote = ''; + } + } + } else { + // not a quote sign + const twoChars: string = currentChar + nextChar; + if ((twoChars === '/*' || twoChars === '//') && openedQuote === '') { + // opening comment, not inside quotes + console.log('opened a comment'); + openedComment = twoChars; + console.log({ openedComment }); + continue; + } + } + accExpr += currentChar; + console.log({ accExpr }); + }; + + return accExpr; +}; diff --git a/src/helpers/parser/toJsonataString.ts b/src/helpers/parser/toJsonataString.ts index 1e94ab6..a9ccd86 100644 --- a/src/helpers/parser/toJsonataString.ts +++ b/src/helpers/parser/toJsonataString.ts @@ -18,6 +18,7 @@ import { } from '../stringFunctions'; import thrower from '../thrower'; import { getElementDefinition } from './getElementDefinition'; +import { removeComments } from './removeComments'; export const toJsonataString = async (inExpr: string): Promise => { console.time('Parse to JSONATA'); @@ -417,9 +418,10 @@ export const toJsonataString = async (inExpr: string): Promise { return lines.filter((line) => line.trim() !== ''); }; -// takes out all comments from expression -// TODO: add support for end-of-line // type comments -export const removeComments = (expr: string): string => expr.replace(/(\/\*[^*]*\*\/)/g, ''); - // clean and split an expression. returns a line array -export const splitToLines = (expr: string): string[] => removeEmptyLines(removeComments(expr).split(/\r?\n/)); +export const splitToLines = (expr: string): string[] => removeEmptyLines(expr.split(/\r?\n/)); export const hashKey = (str: string): string => sha256(str + uuid(str)); From 7ff7bbfbefc606d794cc5583b57c1cda271ac31e Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Tue, 28 May 2024 16:46:23 +0300 Subject: [PATCH 4/8] fix: remove console.log --- src/app.ts | 5 ++-- src/controllers/root.ts | 36 ++++++++++++------------- src/helpers/conformance/getCachePath.ts | 4 ++- src/helpers/parser/removeComments.ts | 8 ------ 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/app.ts b/src/app.ts index 1b2365d..016dc63 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,11 +2,12 @@ * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved * Project name: FUME-COMMUNITY */ +import { getLogger } from './helpers/logger'; import { FumeServer } from './server'; const server = new FumeServer(); server.warmUp().then((res) => { - console.log('FUME is ready!'); + getLogger().info('FUME is ready!'); }).catch((err) => { - console.error('FUME failed to initialize:', err); + getLogger().error({ ERROR: 'FUME failed to initialize', details: err }); }); diff --git a/src/controllers/root.ts b/src/controllers/root.ts index 6008bb7..8408970 100644 --- a/src/controllers/root.ts +++ b/src/controllers/root.ts @@ -1,16 +1,16 @@ -/** - * © 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 type { Request, Response } from 'express'; - -import config from '../config'; -import { getCache } from '../helpers/cache'; -import { recacheFromServer } from '../helpers/conformance'; -import { v2json } from '../helpers/hl7v2'; -import { transform } from '../helpers/jsonataFunctions'; -import { getLogger } from '../helpers/logger'; +import type { Request, Response } from 'express'; + +import config from '../config'; +import { getCache } from '../helpers/cache'; +import { recacheFromServer } from '../helpers/conformance'; +import { v2json } from '../helpers/hl7v2'; +import { transform } from '../helpers/jsonataFunctions'; +import { getLogger } from '../helpers/logger'; import { parseCsv } from '../helpers/stringFunctions'; const get = async (req: Request, res: Response) => { @@ -32,16 +32,16 @@ const evaluate = async (req: Request, res: Response) => { let inputJson; if (req.body.contentType === 'x-application/hl7-v2+er7') { - console.log('Content-Type suggests HL7 V2.x message'); - console.log('Trying to parse V2 message as JSON...'); + getLogger().info('Content-Type suggests HL7 V2.x message'); + getLogger().info('Trying to parse V2 message as JSON...'); inputJson = await v2json(req.body.input); - console.log('Parsed V2 message'); + getLogger().info('Parsed V2 message'); } else { if (req.body.contentType === 'text/csv') { - console.log('Content-Type suggests CSV input'); - console.log('Trying to parse CSV to JSON...'); + getLogger().info('Content-Type suggests CSV input'); + getLogger().info('Trying to parse CSV to JSON...'); inputJson = await parseCsv(req.body.input); - console.log('Parsed CSV to JSON'); + getLogger().info('Parsed CSV to JSON'); } else { inputJson = req.body.input; } diff --git a/src/helpers/conformance/getCachePath.ts b/src/helpers/conformance/getCachePath.ts index 1632fec..946ced6 100644 --- a/src/helpers/conformance/getCachePath.ts +++ b/src/helpers/conformance/getCachePath.ts @@ -6,11 +6,13 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import { getLogger } from '../logger'; + export const getCachePath = (innerFolder = '') => { const cachePath = path.join(os.homedir(), '.fhir', innerFolder); if (!fs.existsSync(cachePath)) { fs.mkdirSync(cachePath, { recursive: true }); - console.log(`Directory '${cachePath}' created successfully.`); + getLogger().info(`Directory '${cachePath}' created successfully.`); } return cachePath; }; diff --git a/src/helpers/parser/removeComments.ts b/src/helpers/parser/removeComments.ts index bd876ff..878e839 100644 --- a/src/helpers/parser/removeComments.ts +++ b/src/helpers/parser/removeComments.ts @@ -16,26 +16,21 @@ export const removeComments = (expr: string): string => { for (let i = 0; i < exprLen; i++) { currentChar = expr.charAt(i); - console.log({ currentChar }); nextChar = i < (exprLen - 1) ? expr.charAt(i + 1) : ''; prevChar = i > 0 ? expr.charAt(i - 1) : ''; prevPrevChar = i > 1 ? expr.charAt(i - 2) : ''; if (openedComment !== '') { // inside a comment - console.log('inside a comment'); - console.log({ openedComment }); const twoChars: string = prevChar + currentChar; if (openedComment === '//' && (currentChar === '\r' || currentChar === '\n')) { // this is the end of the // comment - console.log('end of comment'); openedComment = ''; accExpr += '\n'; } else { if (openedComment === '/*' && twoChars === '*/' && prevPrevChar !== '/') { // this is the end of the /* comment - console.log('end of comment'); openedComment = ''; } }; @@ -59,14 +54,11 @@ export const removeComments = (expr: string): string => { const twoChars: string = currentChar + nextChar; if ((twoChars === '/*' || twoChars === '//') && openedQuote === '') { // opening comment, not inside quotes - console.log('opened a comment'); openedComment = twoChars; - console.log({ openedComment }); continue; } } accExpr += currentChar; - console.log({ accExpr }); }; return accExpr; From d45e7f97b95ea304e4b0040a86052017fdf22bb6 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Wed, 29 May 2024 20:33:23 +0300 Subject: [PATCH 5/8] fix: exclude double slashes that are part of a url --- src/helpers/parser/removeComments.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/helpers/parser/removeComments.ts b/src/helpers/parser/removeComments.ts index 878e839..94a9049 100644 --- a/src/helpers/parser/removeComments.ts +++ b/src/helpers/parser/removeComments.ts @@ -3,6 +3,21 @@ * Project name: FUME-COMMUNITY */ +const isUrlPart = (charIndex: number, expr: string): boolean => { + // the minimum index for a url's // part is after the 'http(s):' part + // so undex lower than 7 means it's a comment and not a url + if (charIndex < 7) return false; + const prevSevenChars: string = expr.substring(charIndex - 7, charIndex); + const prevSixChars: string = prevSevenChars.substring(1); + if ( + ['https:', '[https:'].includes(prevSevenChars.trimStart()) || ['http:', '[http:'].includes(prevSixChars.trimStart()) + ) { + return true; + } else { + return false; + } +}; + export const removeComments = (expr: string): string => { const exprLen: number = expr.length; if (exprLen === 0) return expr; @@ -52,7 +67,8 @@ export const removeComments = (expr: string): string => { } else { // not a quote sign const twoChars: string = currentChar + nextChar; - if ((twoChars === '/*' || twoChars === '//') && openedQuote === '') { + const notUrl: boolean = !isUrlPart(i, expr); + if (openedQuote === '' && (twoChars === '/*' || (twoChars === '//' && notUrl))) { // opening comment, not inside quotes openedComment = twoChars; continue; From 3f2a8760407c3827cd21aa2ff5519de390aaeba7 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Thu, 30 May 2024 02:03:44 +0300 Subject: [PATCH 6/8] fix: do $__finalize wherever there's flash --- src/helpers/parser/toJsonataString.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/parser/toJsonataString.ts b/src/helpers/parser/toJsonataString.ts index a9ccd86..14f85c8 100644 --- a/src/helpers/parser/toJsonataString.ts +++ b/src/helpers/parser/toJsonataString.ts @@ -427,7 +427,7 @@ export const toJsonataString = async (inExpr: string): Promise$__finalize'; } } catch (e) { console.timeEnd('Parse to JSONATA'); From f89036515c70062ee63ebc35edd7a5fdbc449064 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Thu, 30 May 2024 02:18:44 +0300 Subject: [PATCH 7/8] test: unskip relevant tests --- package-lock.json | 4 ++-- tests/root/index.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca49a41..dfc4572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fume-fhir-converter", - "version": "2.2.0", + "version": "2.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fume-fhir-converter", - "version": "2.2.0", + "version": "2.5.0", "license": "AGPL-3.0", "dependencies": { "axios": "^1.6.7", diff --git a/tests/root/index.test.ts b/tests/root/index.test.ts index 8e2a993..d4cfff1 100644 --- a/tests/root/index.test.ts +++ b/tests/root/index.test.ts @@ -168,7 +168,7 @@ describe('integration tests', () => { }); }); - test.skip('Case 4 - Profiled FLASH with no rules doesn\'t go through finalize', async () => { + test('Case 4 - Profiled FLASH with no rules doesn\'t go through finalize', async () => { const mapping = 'InstanceOf: bp'; const requestBody = { input: mockInput, From 8b4695f2398089def02334b38e59101668233179 Mon Sep 17 00:00:00 2001 From: Daniel Mechanik Date: Thu, 30 May 2024 02:36:10 +0300 Subject: [PATCH 8/8] docs: readme version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa297a6..9a9f0bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FUME Community 2.0 - FHIR Utilized Mapping Engine +# FUME Community 2.6 - FHIR Utilized Mapping Engine FUME is a sophisticated FHIR convertion tool. Made by Outburn with :heart: