Skip to content

Commit

Permalink
Merge pull request #82 from fair4health/local-definitions
Browse files Browse the repository at this point in the history
Local definitions
  • Loading branch information
sinaci authored Aug 17, 2022
2 parents 3119d16 + b654df3 commit edb19bc
Show file tree
Hide file tree
Showing 10 changed files with 22,572 additions and 230 deletions.
Binary file added fhir-definitions/definitions-r4.zip
Binary file not shown.
22,530 changes: 22,376 additions & 154 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fair4health-data-curation-tool",
"productName": "FAIR4Health Data Curation Tool",
"version": "1.2.5",
"version": "1.2.6",
"private": true,
"author": "SRDC Corporation <[email protected]>",
"description": "FAIR4Health | Data Curation and Validation Tool",
Expand Down Expand Up @@ -31,6 +31,7 @@
"electron-log": "^4.2.2",
"electron-store": "^5.1.0",
"es6-promise": "^4.2.8",
"extract-zip": "^2.0.1",
"isomorphic-fetch": "^2.2.1",
"ng-fhir": "^2.3.0",
"pg": "^8.5.1",
Expand Down
1 change: 1 addition & 0 deletions src/common/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export let environment = {
},
compatibleFhirVersions: ['4.0.0', '4.0.1']
},
FHIRDefinitionsZipPath: './fhir-definitions/definitions-r4.zip',
langs: ['en'],
databaseTypes: ['postgres'],
hl7: hl7Base,
Expand Down
182 changes: 121 additions & 61 deletions src/common/utils/fhir-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import HmacSHA256 from 'crypto-js/hmac-md5'
import { FhirService } from './../services/fhir.service'
import { environment } from './../environment'
import { DataTypeFactory } from './../model/factory/data-type-factory'
import path from 'path'
import fs from 'fs'
import { remote } from 'electron'
import StructureDefinition = fhir.StructureDefinition;

export class FHIRUtil {

Expand Down Expand Up @@ -61,76 +65,132 @@ export class FHIRUtil {
}

/**
* Parses elements of a StructureDefinition resource (StructureDefinition.snapshot.element)
* Filter the StructureDefinition resources from standard profiles-resources.json and save them into
* tofhir-structure-definitions.json so that subsequent calls do not need to parse a very large file again and again.
*
* @return Return an array of #fhir.StructureDefinition objects from the base resource definition files.
*/
static readStructureDefinitionsOfBaseResources (): fhir.StructureDefinition[] {
let structureDefinitionsFilePath = localStorage.getItem('structure-definitions-file')

if (structureDefinitionsFilePath === undefined || structureDefinitionsFilePath === null || !fs.existsSync(structureDefinitionsFilePath)) {
const parsedZipPath = path.parse(environment.FHIRDefinitionsZipPath)
const resourcesFilePath = path.join(remote.app.getPath('userData'), parsedZipPath.name, 'profiles-resources.json')
structureDefinitionsFilePath = path.join(remote.app.getPath('userData'), parsedZipPath.name, 'tofhir-structure-definitions.json')

const content = fs.readFileSync(resourcesFilePath)
const bundle = JSON.parse(content.toString('utf-8')) as fhir.Bundle
const structureDefinitionResources = bundle.entry.flatMap<StructureDefinition>(e => e.resource.resourceType === 'StructureDefinition' ? e.resource as StructureDefinition : [])
fs.writeFileSync(structureDefinitionsFilePath, JSON.stringify(structureDefinitionResources))

localStorage.setItem('structure-definitions-file', structureDefinitionsFilePath)

return structureDefinitionResources
}

return JSON.parse(fs.readFileSync(structureDefinitionsFilePath).toString('utf-8')) as StructureDefinition[]
}

/**
* Retrieve the #StructureDefinition of a given resource type from the base FHIR definitions.
* @param resourceType
*/
static getStructureDefinitionFromBaseResources (resourceType: string): Promise<StructureDefinition> {
return new Promise((resolve, reject) => {
const baseResourceDefinition = this.readStructureDefinitionsOfBaseResources().find(sd => sd.url.split('/').pop() === resourceType)
if (baseResourceDefinition === undefined || baseResourceDefinition === null) {
reject(`Base resource definition file does not include the resourceType: ${resourceType}`)
}
resolve(baseResourceDefinition)
})
}

/**
* Retrieve the profile definition from the given FHIR endpoint.
* @param fhirService
* @param parameter - Search parameter
* @param profileId
* @param profileUrl
*/
static parseElementDefinitions (fhirService: FhirService, parameter: string, profileId: string): Promise<any> {
return new Promise((resolveParam, rejectParam) => {
static getStructureDefinitionOfProfile (fhirService: FhirService, profileUrl: string): Promise<StructureDefinition> {
return new Promise((resolve, reject) => {
const query = {}
query[parameter] = profileId
query['url'] = profileUrl

fhirService.search('StructureDefinition', query, true)
.then(res => {
const bundle = res.data as fhir.Bundle
if (bundle.entry?.length) {
const resource = bundle.entry[0].resource as fhir.StructureDefinition
const list: fhir.ElementTree[] = []
Promise.all(resource?.snapshot?.element.map((element: fhir.ElementDefinition) => {
return new Promise(resolveElement => {
// Mark the id field as required field
const idSplitted = element.id.split('.')
if (idSplitted.length === 2 && idSplitted[1] === 'id') element.min = 1
const parts = element.id?.split('.') || []
let tmpList = list

// Fixed code-system uri for code fields
const fixedUri = element.fixedUri

Promise.all(parts.map(part => {
return new Promise((resolveElementPart => {
let match = tmpList.findIndex(_ => _.label === part)
if (match === -1) {
match = 0
const item: fhir.ElementTree = {
value: element.id,
label: part,
definition: element.definition,
comment: element.comment,
short: element.short,
min: element.min,
max: element.max,
type: element.type.map(_ => {
const elementType: fhir.ElementTree = {value: _.code, label: _.code, type: [{value: _.code, label: _.code}], targetProfile: _.targetProfile}
if (_.code !== 'CodeableConcept' && _.code !== 'Coding' && _.code !== 'Reference' && environment.datatypes[_.code])
elementType.lazy = true
return FHIRUtil.cleanJSON(elementType)
}),
children: []
}
tmpList.push(item)
resolveElementPart()
}
if (fixedUri) tmpList[match].fixedUri = fixedUri
tmpList = tmpList[match].children as fhir.ElementTree[]
resolveElementPart()
}))
})).then(() => resolveElement()).catch(() => resolveElement())
})
}) || [])
.then(() => {
list[0].children.map(_ => {
if (_.type[0].value !== 'BackboneElement') _.children = []
else _.noTick = true
return _
})
resolveParam(list)
})
.catch(() => rejectParam([]))
} else { resolveParam([]) }
resolve(bundle.entry[0].resource as fhir.StructureDefinition)
} else {
reject(`Returned bundle from the StructureDefinition search endpoint does no include any entries for the profile: ${profileUrl}.`)
}
})
.catch(() => rejectParam([]))
.catch(() => reject(`Cannot invoke the search under StructureDefinition endpoint of the FHIR service for the profile: ${profileUrl}.`))
})
}

/**
* Parses elements of a StructureDefinition resource (StructureDefinition.snapshot.element)
* @param fhirService
* @param parameter - Search parameter
* @param profileId
*/
static parseElementDefinitions (fhirService: FhirService, parameter: string, profileId: string): Promise<any> {
return new Promise((resolveParam, rejectParam) => {
const promise = parameter === 'url' ? this.getStructureDefinitionOfProfile(fhirService, profileId) : this.getStructureDefinitionFromBaseResources(profileId)
promise.then(resource => {
const list: fhir.ElementTree[] = []
Promise.all(resource?.snapshot?.element.map((element: fhir.ElementDefinition) => {
return new Promise(resolveElement => {
// Mark the id field as required field
const idSplitted = element.id.split('.')
if (idSplitted.length === 2 && idSplitted[1] === 'id') element.min = 1
const parts = element.id?.split('.') || []
let tmpList = list

// Fixed code-system uri for code fields
const fixedUri = element.fixedUri

Promise.all(parts.map(part => {
return new Promise((resolveElementPart => {
let match = tmpList.findIndex(_ => _.label === part)
if (match === -1) {
match = 0
const item: fhir.ElementTree = {
value: element.id,
label: part,
definition: element.definition,
comment: element.comment,
short: element.short,
min: element.min,
max: element.max,
type: element.type.map(_ => {
const elementType: fhir.ElementTree = {value: _.code, label: _.code, type: [{value: _.code, label: _.code}], targetProfile: _.targetProfile}
if (_.code !== 'CodeableConcept' && _.code !== 'Coding' && _.code !== 'Reference' && environment.datatypes[_.code])
elementType.lazy = true
return FHIRUtil.cleanJSON(elementType)
}),
children: []
}
tmpList.push(item)
resolveElementPart()
}
if (fixedUri) tmpList[match].fixedUri = fixedUri
tmpList = tmpList[match].children as fhir.ElementTree[]
resolveElementPart()
}))
})).then(() => resolveElement()).catch(() => resolveElement())
})
}) || [])
.then(() => {
list[0].children.map(_ => {
if (_.type[0].value !== 'BackboneElement') _.children = []
else _.noTick = true
return _
})
resolveParam(list)
})
.catch(() => rejectParam([]))
})
})
}

Expand Down
67 changes: 57 additions & 10 deletions src/electron-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import { app, protocol, BrowserWindow, dialog, ipcMain, webContents, MessageBoxReturnValue } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import log from 'electron-log'
import log, {error} from 'electron-log'
import { IpcChannelUtil as ipcChannels } from './common/utils/ipc-channel-util'
// import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import path from 'path'
import fs from 'fs'
import extract from 'extract-zip'
import {environment} from '@/common/environment';

const isDevelopment = process.env.NODE_ENV !== 'production'

Expand All @@ -22,7 +26,7 @@ app.commandLine.appendSwitch('js-flags', '--max-old-space-size=8096')

// Logger settings
log.transports.file.fileName = 'log.txt'
log.transports.console.level = false
// log.transports.console.level = false

// Stack of available background threads
const availableThreads: webContents[] = []
Expand All @@ -39,7 +43,7 @@ function doWork () {
// win.webContents.send('status', availableThreads.length, taskQueue.length)
}

function createWindow () {
async function createWindow () {
// Create the browser window.
win = new BrowserWindow({
minWidth: 800,
Expand All @@ -62,7 +66,7 @@ function createWindow () {
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
// if (!process.env.IS_TEST) win.webContents.openDevTools()
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('fair4health')
// Load the index.html when not in development
Expand Down Expand Up @@ -144,6 +148,28 @@ function createBgWindow (id: number): BrowserWindow {

}

async function extractFHIRDefinitionZip () {
const definitionsFilePath = path.resolve(environment.FHIRDefinitionsZipPath)
const parsedPath = path.parse(definitionsFilePath)
const extractionFolderPath = path.join(app.getPath('userData'), parsedPath.name)
log.info('Location of definitions-r4.zip -> ' + definitionsFilePath)
log.info('Extracted definitions folder -> ' + extractionFolderPath)

if (fs.existsSync(path.join(extractionFolderPath, 'profiles-resources.json'))) {
// ZIP was previously extracted. Do not extract it again.
log.info('Definitions ZIP file already extracted, skipping...')
return
}

try {
await extract(definitionsFilePath, {dir: extractionFolderPath})
log.info(`Extraction of ${definitionsFilePath} completed`)
} catch (e) {
log.error(`Extraction error: ${e}`)
throw e
}
}

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS, quits the app as Cmd + Q does
Expand All @@ -164,11 +190,32 @@ app.on('activate', () => {
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
// try {
// await installExtension(VUEJS_DEVTOOLS)
// } catch (e) {
// console.error('Vue Devtools failed to install:', e.toString())
// }
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}

try {
await extractFHIRDefinitionZip()
} catch (e) {
const options = {
type: 'info',
title: 'FHIR Definitions Zip extraction failed.',
message: 'Please consult the project\'s GitHub page.',
buttons: ['Close']
}
dialog.showMessageBox(options)
.then((messageBoxReturnValue: MessageBoxReturnValue) => {
if (messageBoxReturnValue.response === 0) win?.reload()
else win?.destroy()
})
.catch(err => {
log.error(err)
win?.destroy()
})
return
}

// Create Main renderer window
Expand Down
1 change: 1 addition & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"CANNOT_GET_SAVED_MAPPINGS": "Cannot get saved mappings",
"CANNOT_LOAD_CREATED_RESOURCES": "Cannot load created resources. Try again",
"CHOOSE_A_TYPE": "Choose a type for selected items",
"COULDNT_EXTRACT_FHIR_DEFINITIONS_ZIP": "Couldn't extract FHIR Definitions zip!",
"CHUNK_SIZE_CANNOT_BE_X": "Chunk size can not be {chunkSize}.",
"DATA_COULDNT_BE_IMPORTED": "Data could't be imported",
"EMPTY_MAPPING_SHEET": "Empty mapping sheet",
Expand Down
12 changes: 8 additions & 4 deletions src/store/fhirStore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ const fhirStore = {
.then(res => {
const bundle = res.data as fhir.Bundle
if (bundle.total > 0) {
profileList.push(...(bundle.entry?.map(e => {
profileList.push(...(bundle.entry?.flatMap(e => {
const structure = e.resource as fhir.StructureDefinition
return {id: structure.id, title: structure.title, url: structure.url} as fhir.StructureDefinition
if (structure.snapshot) // Add this profile to the list only if it has the snapshot field.
return {id: structure.id, title: structure.title, url: structure.url} as fhir.StructureDefinition
else return []
}) || []))

Promise.all(profileList.map(profile => {
Expand All @@ -107,9 +109,11 @@ const fhirStore = {
.then(res => {
const bundle = res.data as fhir.Bundle
if (bundle.total > 0) {
subProfiles.push(...(bundle.entry?.map(e => {
subProfiles.push(...(bundle.entry?.flatMap(e => {
const structure = e.resource as fhir.StructureDefinition
return {id: structure.id, title: structure.title, url: structure.url} as fhir.StructureDefinition
if (structure.snapshot) // Add this profile to the list only if it has the snapshot field.
return {id: structure.id, title: structure.title, url: structure.url} as fhir.StructureDefinition
else return []
}) || []))
}
resolveSubProfile(true)
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"noImplicitAny": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"inlineSources": true,
"sourceMap": true,
"baseUrl": ".",
"typeRoots": [
Expand Down
5 changes: 5 additions & 0 deletions vue.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ module.exports = {
perMachine: true,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true
},
extraFiles: {
from: 'fhir-definitions',
to: 'fhir-definitions',
filter: ["**/*"]
}
}
}
Expand Down

0 comments on commit edb19bc

Please sign in to comment.