Skip to content

Commit

Permalink
feat: add new method for $validate API
Browse files Browse the repository at this point in the history
- 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
Chinlinlee committed Mar 6, 2022
1 parent 9c59674 commit 5b82908
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 8,053 deletions.
2 changes: 1 addition & 1 deletion api/FHIR/Patient/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ if (_.get(config, "Patient.interaction.create", true)) {
router.post('/', require('./controller/postPatient'));
}

//router.post('/([\$])validate', require('./controller/postPatientValidate'));
router.post('/([\$])validate', require('./controller/postPatientValidate'));

if (_.get(config, "Patient.interaction.update", true)) {
router.put('/:id', require("./controller/putPatient"));
Expand Down
41 changes: 25 additions & 16 deletions api/FHIRApiService/$validate.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const _ = require('lodash');
const fetch = require('node-fetch');
const FHIR = require('fhir').Fhir;
const fhirVersions = require('fhir').Versions;
const ParseConformance = require('fhir/parseConformance').ParseConformance;
const { handleError, issue, OperationOutcome } = require('../../models/FHIR/httpMessage');
const { getDeepKeys } = require('../apiService');
const { handleError } = require('../../models/FHIR/httpMessage');
const { validateByProfile, validateByMetaProfile } = require('../../models/FHIR/fhir-validator');
const { logger } = require('../../utils/log');
const path = require('path');
const PWD_FILENAME = path.relative(process.cwd(), __filename);

/**
*
* @param {import('express').Request} req
Expand All @@ -21,15 +22,23 @@ module.exports = async function (req, res, resourceType) {
}
return res.status(code).send(item);
};
let operationOutcomeMessage;
return doRes(400, {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "processing",
"code": "processing",
"diagnostics": "$validate is not support"
}
]
});
try {
let operationOutcomeMessage;
let profileUrl = _.get(req.query, "profile");
if (profileUrl) {
operationOutcomeMessage = await validateByProfile(profileUrl, req.body);
} else {
operationOutcomeMessage = await validateByMetaProfile(req.body);
}
let haveError = (_.get(operationOutcomeMessage, "issue")) ? operationOutcomeMessage.issue.find(v=> v.severity === "error") : false;
if (haveError) {
return doRes(412, operationOutcomeMessage);
}
return doRes(200, operationOutcomeMessage);
} catch(e) {
let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e));
logger.error(`[Error: ${errorStr}] [From-File: ${PWD_FILENAME}]`);
let operationOutcomeError = handleError.exception(errorStr);
return doRes(500, operationOutcomeError);
}
};
2 changes: 1 addition & 1 deletion api_generator/API_Generator_V2.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function generateAPI(option) {
router.post('/', require('./controller/post${res}'));
}
//router.post('/([\\$])validate', require('./controller/post${res}Validate'));
router.post('/([\\$])validate', require('./controller/post${res}Validate'));
if (_.get(config, "${res}.interaction.update", true)) {
router.put('/:id', require("./controller/put${res}"));
Expand Down
182 changes: 182 additions & 0 deletions models/FHIR/fhir-validator.js
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;
78 changes: 78 additions & 0 deletions models/FHIR/schedule-update-validation-files.js
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();
});
35 changes: 35 additions & 0 deletions models/mongodb/staticModel/FHIRValidationFiles.js
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;
};
Loading

0 comments on commit 5b82908

Please sign in to comment.