-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add new method for
$validate
API
- add `fhir-validator` - add `refreshResourceResolver` to call C# API to reload profiles - add `storeValidationFile` to store `StructureDefinition`, `ValueSet`, `CodeSystem` - add `fetchValueSet` to get `ValueSet` resource by URL - add `fetchCodeSystem` to get `CodeSystem` resource by URL - add `validateByProfile` to validate resource by specific profile URL in URL path - add `validateByMetaProfile` to validate resource by profiles URL from meta.profile - add `FHIRValidationFiles` MongoDB schema to store data about validation files - add schedule to update validation files in MongoDB - The validation workflow note can retrieve from https://imgur.com/a/SjklQTR > feat: 新增新的方法處理 `$validate` API > - 新增 `fhir-validator` > - 新增`refreshResourceResolver`呼叫 C# API讓Server重新讀取profiles > - 新增`storedValidationFile` 儲存`StructureDefinition`, `ValueSet`, `CodeSystem` > - 新增 `fetchValueSet` 將指定 URL 的 `ValueSet` 爬下來並轉為JSON > - 新增 `fetchCodeSystem` 將指定 URL 的 `CodeSystem` 爬下來並轉為JSON > - 新增 `validateByProfile` 使用在URL指定的 profile 驗證 resource > - 新增 `validateByMetaProfile` 使用 resource 當中的 meta.profile 進行驗證 > - 新增 `FHIRValidationFiles` MongoDB schema 儲存與驗證相關的資料 > - 驗證流程 https://imgur.com/a/SjklQTR
- Loading branch information
1 parent
9c59674
commit 5b82908
Showing
8 changed files
with
436 additions
and
8,053 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
const _ = require('lodash'); | ||
const fetch = require('node-fetch'); | ||
const mongodb = require('../mongodb'); | ||
const { handleError, issue, OperationOutcome } = require('./httpMessage'); | ||
const flatten = require('flat'); | ||
const hash = require('object-hash'); | ||
const fs = require('fs'); | ||
const nodeUrl = require('url'); | ||
const https = require('https'); | ||
const { logger } = require('../../utils/log'); | ||
const path = require('path'); | ||
const PWD_FILENAME = path.relative(process.cwd(), __filename); | ||
const VALIDATION_API_URL = process.env.VALIDATION_API_URL; | ||
|
||
/** | ||
* @param {string} profileUrl | ||
*/ | ||
async function validateByProfile(profileUrl, resourceContent) { | ||
try { | ||
await storeValidationFile(profileUrl); | ||
await refreshResourceResolver(); | ||
let validation = await validate([profileUrl], resourceContent); | ||
if (validation.status) { | ||
return JSON.parse(validation.data); | ||
} else { | ||
throw new Error(validation.data); | ||
} | ||
} catch(e) { | ||
throw e; | ||
} | ||
} | ||
|
||
async function validateByMetaProfile(resourceContent) { | ||
try { | ||
let metaProfile = _.get(resourceContent, "meta.profile"); | ||
if (metaProfile) { | ||
for (let i = 0 ; i< metaProfile.length; i++) { | ||
let profileUrl = metaProfile[i]; | ||
await storeValidationFile(profileUrl); | ||
} | ||
await refreshResourceResolver(); | ||
let validation = await validate(metaProfile, resourceContent); | ||
if (validation.status) { | ||
return JSON.parse(validation.data); | ||
} else { | ||
throw new Error(validation.data); | ||
} | ||
} | ||
} catch(e) { | ||
throw e; | ||
} | ||
} | ||
|
||
/** | ||
* Store StructureDefinition, ValueSet, CodeSystem json when not exists in MongoDB. | ||
* @param {string} url | ||
*/ | ||
async function storeValidationFile(url) { | ||
try { | ||
let validationFile = await mongodb.FHIRValidationFiles.findOne({ | ||
url: url | ||
}); | ||
if (!validationFile) { | ||
logger.info(`[Info: Fetch Profile From URL: ${url}]`); | ||
let fetchRes = await fetch(url, { | ||
headers: { | ||
"accept": "application/fhir+json" | ||
} | ||
}); | ||
let resJson = await fetchRes.json(); | ||
if (resJson.resourceType === "StructureDefinition") { | ||
await fetchValueSet(resJson); | ||
} else if (resJson.resourceType === "ValueSet") { | ||
await fetchCodeSystem(resJson); | ||
} | ||
let contentHash = hash(resJson); | ||
let storePath = path.join(process.env.VALIDATION_FILES_ROOT_PATH, hash({url:url}) + ".json"); | ||
fs.writeFile(path.resolve(storePath), JSON.stringify(resJson), ()=> {}); | ||
let validationFileObj = { | ||
url: url, | ||
hash: contentHash, | ||
path: storePath, | ||
id: resJson.id | ||
}; | ||
await mongodb.FHIRValidationFiles.findOneAndUpdate({ | ||
url: url | ||
}, { $set: validationFileObj}, { | ||
upsert: true | ||
}); | ||
} | ||
} catch(e) { | ||
throw e; | ||
} | ||
} | ||
|
||
/** | ||
* Store value set json when not exists in MongoDB. | ||
* @param {JSON} content | ||
*/ | ||
async function fetchValueSet(content) { | ||
let flattenContent = flatten(content); | ||
let valueSetKeys = Object.keys(flattenContent).filter( | ||
key=> key.includes("binding.valueSet") | ||
); | ||
for (let key of valueSetKeys) { | ||
let valueSetUri = _.get(content, key); | ||
await storeValidationFile(valueSetUri); | ||
} | ||
} | ||
|
||
/** | ||
* Store code system json when not exists in MongoDB. | ||
* @param {JSON} valueSet | ||
*/ | ||
async function fetchCodeSystem(valueSet) { | ||
let flattenValueSet= flatten(valueSet); | ||
let codeSystemKeys = Object.keys(flattenValueSet).filter( | ||
key=> key.endsWith(".system") | ||
); | ||
for (let key of codeSystemKeys) { | ||
let codeSystemUri = _.get(valueSet, key); | ||
if (codeSystemUri.includes("CodeSystem")) | ||
await storeValidationFile(codeSystemUri); | ||
} | ||
} | ||
|
||
/** | ||
* Call C# validator API server to reload profiles. | ||
*/ | ||
async function refreshResourceResolver() { | ||
try { | ||
const httpsAgent = new https.Agent({ | ||
rejectUnauthorized: false | ||
}); | ||
let APIUrl = new nodeUrl.URL("/api/refreshresourceresolver" , VALIDATION_API_URL).href; | ||
let fetchRes = await fetch(APIUrl, { | ||
method: "POST", | ||
agent: httpsAgent | ||
}); | ||
logger.info(`[Info: Refresh C# Validator Resource Resolver] [Content: ${JSON.stringify(await fetchRes.json())}]`); | ||
} catch(e) { | ||
throw e; | ||
} | ||
} | ||
|
||
/** | ||
* Call C# API server to validate with profiles. | ||
* @param {Array<string>} profile The string array of profiles URL. | ||
* @param {JSON} resourceContent The FHIR resource JSON object. | ||
* @return {JSON} | ||
*/ | ||
async function validate(profile, resourceContent) { | ||
try { | ||
const httpsAgent = new https.Agent({ | ||
rejectUnauthorized: false | ||
}); | ||
let APIUrl = new nodeUrl.URL("/api/validate" , VALIDATION_API_URL).href; | ||
let body = { | ||
profile: profile, | ||
resourceJson: JSON.stringify(resourceContent) | ||
}; | ||
let fetchRes = await fetch(APIUrl, { | ||
method: "POST", | ||
body: JSON.stringify(body), | ||
agent: httpsAgent, | ||
headers: { | ||
'content-type': 'application/json' | ||
} | ||
}); | ||
let fetchResJson = await fetchRes.json(); | ||
logger.info(`[Info: Call Validation function from C# successfully] [URL: ${APIUrl}]`); | ||
return fetchResJson; | ||
} catch(e) { | ||
throw e; | ||
} | ||
} | ||
|
||
module.exports.validateByProfile = validateByProfile; | ||
module.exports.validateByMetaProfile = validateByMetaProfile; | ||
module.exports.refreshResourceResolver = refreshResourceResolver; | ||
module.exports.fetchValueSet = fetchValueSet; | ||
module.exports.fetchCodeSystem = fetchCodeSystem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
require('dotenv').config({ | ||
path: `${process.cwd()}/.env` | ||
}); | ||
const _ = require('lodash'); | ||
const fetch = require('node-fetch'); | ||
const mongodb = require('../mongodb'); | ||
const hash = require('object-hash'); | ||
const fs = require('fs'); | ||
const { fetchCodeSystem, fetchValueSet, refreshResourceResolver } = require('./fhir-validator'); | ||
const { logger } = require('../../utils/log'); | ||
const path = require('path'); | ||
const PWD_FILENAME = path.relative(process.cwd(), __filename); | ||
const schedule = require('node-schedule'); | ||
|
||
/** | ||
* Store StructureDefinition, ValueSet, CodeSystem json when not exists in MongoDB. | ||
* @param {JSON} structureDefinition | ||
*/ | ||
async function updateValidationFile(url, resJson, contentHash) { | ||
try { | ||
if (resJson.resourceType === "StructureDefinition") { | ||
await fetchValueSet(resJson); | ||
} else if (resJson.resourceType === "ValueSet") { | ||
await fetchCodeSystem(resJson); | ||
} | ||
let storePath = path.join(process.env.VALIDATION_FILES_ROOT_PATH, hash({url:url}) + ".json"); | ||
fs.writeFile(path.resolve(storePath), JSON.stringify(resJson), ()=> {}); | ||
let validationFileObj = { | ||
url: url, | ||
hash: contentHash, | ||
path: storePath, | ||
id: resJson.id | ||
}; | ||
await mongodb.FHIRValidationFiles.findOneAndUpdate({ | ||
url: url | ||
}, { $set: validationFileObj}, { | ||
upsert: true | ||
}); | ||
} catch(e) { | ||
throw e; | ||
} | ||
} | ||
|
||
async function iterateUpdateValidationFiles() { | ||
try { | ||
let validationFiles = await mongodb.FHIRValidationFiles.find({}).limit(10); | ||
while(validationFiles.length > 0) { | ||
for(let validationFileObj of validationFiles) { | ||
let url = validationFileObj.url; | ||
logger.info(`[Info: Fetch Profile From URL: ${url}]`); | ||
let fetchRes = await fetch(url, { | ||
headers: { | ||
"accept": "application/fhir+json" | ||
} | ||
}); | ||
let resJson = await fetchRes.json(); | ||
let contentHash = hash(resJson); | ||
if (validationFileObj.hash != contentHash) { | ||
await updateValidationFile(url, resJson, contentHash); | ||
} | ||
} | ||
let lastId = validationFiles[validationFiles.length - 1]._id; | ||
validationFiles = await mongodb.FHIRValidationFiles.find({ | ||
_id: { | ||
$gt: lastId | ||
} | ||
}) | ||
.limit(10); | ||
} | ||
} catch(e) { | ||
logger.error(e); | ||
} | ||
} | ||
|
||
const updateValidationFileJob = schedule.scheduleJob('0 0 0 * *', async function(){ | ||
await iterateUpdateValidationFiles(); | ||
await refreshResourceResolver(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
module.exports = function (mongodb) { | ||
let FHIRValidationFilesSchema = mongodb.Schema({ | ||
url: { | ||
type: String, | ||
default: void 0 | ||
}, | ||
hash: { | ||
type: String , | ||
default : void 0 | ||
}, | ||
path: { | ||
type: String, | ||
default : void 0 | ||
}, | ||
id: { | ||
type: String, | ||
default : void 0 | ||
} | ||
} , { | ||
versionKey : false | ||
}); | ||
FHIRValidationFilesSchema.index({ | ||
"url": 1, | ||
"hash": 1 | ||
}, { | ||
background: true | ||
}); | ||
FHIRValidationFilesSchema.index({ | ||
"id": 1 | ||
}, { | ||
background: true | ||
}); | ||
let FHIRValidationFilesModel = mongodb.model('FHIRValidationFiles', FHIRValidationFilesSchema, 'FHIRValidationFiles'); | ||
return FHIRValidationFilesModel; | ||
}; |
Oops, something went wrong.