ecqm-bundler
Advanced tools
Comparing version 0.0.1 to 0.0.2
@@ -5,3 +5,3 @@ "use strict"; | ||
function getMainLibraryId(cql) { | ||
const re = /library ([a-zA-Z0-9_]+)( version .*)?/g; | ||
const re = /^library ([a-zA-Z0-9_]+)( version .*)?/gm; | ||
const matches = re.exec(cql); | ||
@@ -8,0 +8,0 @@ if (matches) { |
@@ -15,13 +15,43 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getELM = void 0; | ||
exports.getELM = exports.resolveDependencies = exports.getCQLInfo = void 0; | ||
const fs_1 = __importDefault(require("fs")); | ||
const path_1 = __importDefault(require("path")); | ||
const cql_translation_service_client_1 = require("cql-translation-service-client"); | ||
function translateCQL(paths, client) { | ||
const cql_1 = require("./cql"); | ||
function getCQLInfo(cqlFiles) { | ||
const info = {}; | ||
cqlFiles.forEach(f => { | ||
const cql = fs_1.default.readFileSync(f, 'utf8'); | ||
const libraryId = (0, cql_1.getMainLibraryId)(cql); | ||
if (libraryId) { | ||
info[libraryId] = { cql }; | ||
} | ||
}); | ||
return info; | ||
} | ||
exports.getCQLInfo = getCQLInfo; | ||
function resolveDependencies(mainCqlContent, allCql) { | ||
const res = []; | ||
const re = /^include ([a-zA-Z0-9_]+)( version .*)?/gm; | ||
let includes; | ||
while ((includes = re.exec(mainCqlContent))) { | ||
const libraryId = includes[1]; | ||
res.push(libraryId); | ||
if (allCql[libraryId]) { | ||
res.push(...resolveDependencies(allCql[libraryId].cql, allCql)); | ||
} | ||
} | ||
return res; | ||
} | ||
exports.resolveDependencies = resolveDependencies; | ||
function translateCQL(paths, client, mainLibraryId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const cqlRequestBody = {}; | ||
paths.forEach(f => { | ||
cqlRequestBody[path_1.default.basename(f, '.cql')] = { | ||
cql: fs_1.default.readFileSync(f, 'utf8') | ||
}; | ||
const cqlRequestBody = getCQLInfo(paths); | ||
const usedDependencyIds = [ | ||
...new Set(resolveDependencies(cqlRequestBody[mainLibraryId].cql, cqlRequestBody)) | ||
]; | ||
// Only include cql files that are explicitly included by the dependency chain | ||
Object.keys(cqlRequestBody).forEach(k => { | ||
if (k !== mainLibraryId && !usedDependencyIds.includes(k)) { | ||
delete cqlRequestBody[k]; | ||
} | ||
}); | ||
@@ -44,6 +74,6 @@ const elm = yield client.convertCQL(cqlRequestBody); | ||
} | ||
function getELM(cqlPaths, translatorUrl) { | ||
function getELM(cqlPaths, translatorUrl, mainLibraryId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const client = new cql_translation_service_client_1.Client(`${translatorUrl}?annotations=true&locators=true`); | ||
const librariesOrError = yield translateCQL(cqlPaths, client); | ||
const librariesOrError = yield translateCQL(cqlPaths, client, mainLibraryId); | ||
if (librariesOrError instanceof Error) | ||
@@ -50,0 +80,0 @@ throw librariesOrError; |
@@ -26,5 +26,7 @@ #!/usr/bin/env node | ||
program | ||
.requiredOption('-c, --cql-file <path>') | ||
.addOption(new commander_1.Option('--deps <deps...>', 'List of CQL dependency files of the main file').default([])) | ||
.option('--deps-directory <path>', 'Directory containing all dependent CQL files') | ||
.option('-c, --cql-file <path>') | ||
.option('-e,--elm-file <path>') | ||
.option('--debug', 'Enable debug mode to write contents to a ./debug directory', false) | ||
.addOption(new commander_1.Option('--deps <deps...>', 'List of CQL or ELM dependency files of the main file').default([])) | ||
.option('--deps-directory <path>', 'Directory containing all dependent CQL or ELM files') | ||
.option('-n,--numer <expr>', 'Numerator expression name of measure', 'Numerator') | ||
@@ -46,4 +48,12 @@ .option('-i,--ipop <expr>', 'Numerator expression name of measure', 'Initial Population') | ||
const opts = program.opts(); | ||
if (opts.elmFile && opts.cqlFile) { | ||
logger_1.default.error('ERROR: Cannot use both -c/--cql-file and -e/--elm-file\n'); | ||
program.help(); | ||
} | ||
if (!(opts.elmFile || opts.cqlFile)) { | ||
logger_1.default.error('ERROR: Must specify one of -c/--cql-file or -e/--elm-file\n'); | ||
program.help(); | ||
} | ||
if (opts.deps.length !== 0 && opts.depsDirectory) { | ||
console.error('ERROR: Must specify only one of -d/--deps and --deps-directory\n'); | ||
logger_1.default.error('ERROR: Must specify only one of -d/--deps and --deps-directory\n'); | ||
program.help(); | ||
@@ -63,35 +73,62 @@ } | ||
.readdirSync(opts.depsDirectory) | ||
.filter(f => path_1.default.extname(f) === '.cql' && f !== path_1.default.basename(opts.cqlFile)) | ||
.filter(f => { | ||
if (opts.elmFile) { | ||
return path_1.default.extname(f) === '.json' && f !== path_1.default.basename(opts.elmFile); | ||
} | ||
else { | ||
return path_1.default.extname(f) === '.cql' && f !== path_1.default.basename(opts.cqlFile); | ||
} | ||
}) | ||
.map(f => path_1.default.join(depsBasePath, f)); | ||
} | ||
logger_1.default.info(`Successfully gathered ${deps.length} dependencies`); | ||
const mainCQLPath = path_1.default.resolve(opts.cqlFile); | ||
const allCQL = [mainCQLPath, ...deps]; | ||
logger_1.default.info(`Using ${mainCQLPath} as main library`); | ||
function main() { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const mainLibraryId = (0, cql_1.getMainLibraryId)(fs_1.default.readFileSync(mainCQLPath, 'utf8')); | ||
let elm; | ||
try { | ||
logger_1.default.info('Translating all CQL'); | ||
const [result, errors] = yield (0, translator_1.getELM)(allCQL, opts.translatorUrl); | ||
if (result == null) { | ||
logger_1.default.error('Error translating CQL:'); | ||
console.error(errors); | ||
let mainLibraryId = null; | ||
if (opts.elmFile) { | ||
const mainELM = JSON.parse(fs_1.default.readFileSync(path_1.default.resolve(opts.elmFile), 'utf8')); | ||
mainLibraryId = mainELM.library.identifier.id; | ||
if (!mainLibraryId) { | ||
logger_1.default.error(`Could not locate main library ID in ${opts.elmFile}`); | ||
process.exit(1); | ||
} | ||
elm = result; | ||
elm = [mainELM, ...deps.map(d => JSON.parse(fs_1.default.readFileSync(path_1.default.resolve(d), 'utf8')))]; | ||
} | ||
catch (e) { | ||
logger_1.default.error(`HTTP error translating CQL: ${e.message}`); | ||
console.error(e.stack); | ||
if ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data) { | ||
console.log(e.response.data); | ||
else { | ||
const mainCQLPath = path_1.default.resolve(opts.cqlFile); | ||
const allCQL = [mainCQLPath, ...deps]; | ||
logger_1.default.info(`Using ${mainCQLPath} as main library`); | ||
mainLibraryId = (0, cql_1.getMainLibraryId)(fs_1.default.readFileSync(mainCQLPath, 'utf8')); | ||
if (!mainLibraryId) { | ||
logger_1.default.error(`Could not locate main library ID in ${opts.cqlFile}`); | ||
process.exit(1); | ||
} | ||
process.exit(1); | ||
try { | ||
logger_1.default.info('Translating all CQL'); | ||
const [result, errors] = yield (0, translator_1.getELM)(allCQL, opts.translatorUrl, mainLibraryId); | ||
if (result == null) { | ||
logger_1.default.error('Error translating CQL:'); | ||
console.error(errors); | ||
process.exit(1); | ||
} | ||
elm = result; | ||
} | ||
catch (e) { | ||
logger_1.default.error(`HTTP error translating CQL: ${e.message}`); | ||
console.error(e.stack); | ||
if ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data) { | ||
console.log(e.response.data); | ||
} | ||
process.exit(1); | ||
} | ||
} | ||
if (!mainLibraryId) { | ||
logger_1.default.error(`Could not locate main library ID in ${mainCQLPath}`); | ||
process.exit(1); | ||
if (opts.debug === true) { | ||
if (!fs_1.default.existsSync('./debug')) { | ||
fs_1.default.mkdirSync('./debug'); | ||
} | ||
elm.forEach(e => { | ||
fs_1.default.writeFileSync(`./debug/${e.library.identifier.id}.json`, JSON.stringify(e, null, 2), 'utf8'); | ||
}); | ||
} | ||
@@ -98,0 +135,0 @@ const mainLibELM = (0, elm_1.findELMByIdentifier)({ |
{ | ||
"name": "ecqm-bundler", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "CLI for bundling FHIR-based eCQMs", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -15,4 +15,6 @@ # ecqm-bundler | ||
Must have an instance of the [cql-translation-service](https://github.com/cqframework/cql-translation-service) running somewhere. | ||
For bundling with CQL files as input, you must have an instance of the [cql-translation-service](https://github.com/cqframework/cql-translation-service) running somewhere. | ||
Bundling is also supported with JSON ELM content directly. See [Bundling from ELM Content](#bundling-from-elm-content) | ||
## Basic Usage | ||
@@ -32,2 +34,14 @@ | ||
**NOTE**: It is okay for the dependency directory to include more CQL files that won't be used. The bundler will parse the CQL content to identify all of the libaries | ||
that need to be included with the translation request, and will omit any CQL files that aren't included somewhere in the dependency tree starting from the | ||
main CQL file | ||
### Bundling from ELM Content | ||
``` bash | ||
ecqm-bundler -e /path/to/main/elm/file.json --deps-directory /path/to/deps/directory -v /path/to/valueset/directory | ||
``` | ||
This will forego the CQL translation and bundle the libraries with the ELM content provided. | ||
## Advanced Usage/Features | ||
@@ -70,2 +84,9 @@ | ||
### Debugging | ||
Debug mode will write all of the ELM content to a file in the `./debug` directory, which it will create. This is useful for inspecting the contents of translated CQL before it gets | ||
base64 encoded onto a FHIR Library resource. | ||
To enable, use `--debug` as an option in the CLI amongst the other options | ||
## Full List of Options | ||
@@ -78,4 +99,6 @@ | ||
-c, --cql-file <path> | ||
--deps <deps...> List of CQL dependency files of the main file (default: []) | ||
--deps-directory <path> Directory containing all dependent CQL files | ||
-e,--elm-file <path> | ||
--debug Enable debug mode to write contents to a ./debug directory (default: false) | ||
--deps <deps...> List of CQL or ELM dependency files of the main file (default: []) | ||
--deps-directory <path> Directory containing all dependent CQL or ELM files | ||
-n,--numer <expr> Numerator expression name of measure (default: "Numerator") | ||
@@ -82,0 +105,0 @@ -i,--ipop <expr> Numerator expression name of measure (default: "Initial Population") |
export function getMainLibraryId(cql: string): string | null { | ||
const re = /library ([a-zA-Z0-9_]+)( version .*)?/g; | ||
const re = /^library ([a-zA-Z0-9_]+)( version .*)?/gm; | ||
@@ -4,0 +4,0 @@ const matches = re.exec(cql); |
import fs from 'fs'; | ||
import path from 'path'; | ||
import translationService, { Client } from 'cql-translation-service-client'; | ||
import { getMainLibraryId } from './cql'; | ||
async function translateCQL(paths: string[], client: Client): Promise<any> { | ||
const cqlRequestBody: translationService.CqlLibraries = {}; | ||
export function getCQLInfo(cqlFiles: string[]): translationService.CqlLibraries { | ||
const info: translationService.CqlLibraries = {}; | ||
paths.forEach(f => { | ||
cqlRequestBody[path.basename(f, '.cql')] = { | ||
cql: fs.readFileSync(f, 'utf8') | ||
}; | ||
cqlFiles.forEach(f => { | ||
const cql = fs.readFileSync(f, 'utf8'); | ||
const libraryId = getMainLibraryId(cql); | ||
if (libraryId) { | ||
info[libraryId] = { cql }; | ||
} | ||
}); | ||
return info; | ||
} | ||
export function resolveDependencies( | ||
mainCqlContent: string, | ||
allCql: translationService.CqlLibraries | ||
): string[] { | ||
const res: string[] = []; | ||
const re = /^include ([a-zA-Z0-9_]+)( version .*)?/gm; | ||
let includes; | ||
while ((includes = re.exec(mainCqlContent))) { | ||
const libraryId = includes[1]; | ||
res.push(libraryId); | ||
if (allCql[libraryId]) { | ||
res.push(...resolveDependencies(allCql[libraryId].cql, allCql)); | ||
} | ||
} | ||
return res; | ||
} | ||
async function translateCQL(paths: string[], client: Client, mainLibraryId: string): Promise<any> { | ||
const cqlRequestBody = getCQLInfo(paths); | ||
const usedDependencyIds = [ | ||
...new Set(resolveDependencies(cqlRequestBody[mainLibraryId].cql, cqlRequestBody)) | ||
]; | ||
// Only include cql files that are explicitly included by the dependency chain | ||
Object.keys(cqlRequestBody).forEach(k => { | ||
if (k !== mainLibraryId && !usedDependencyIds.includes(k)) { | ||
delete cqlRequestBody[k]; | ||
} | ||
}); | ||
const elm = await client.convertCQL(cqlRequestBody); | ||
@@ -35,6 +74,7 @@ return elm; | ||
cqlPaths: string[], | ||
translatorUrl: string | ||
translatorUrl: string, | ||
mainLibraryId: string | ||
): Promise<[any[] | null, any[] | null]> { | ||
const client = new Client(`${translatorUrl}?annotations=true&locators=true`); | ||
const librariesOrError = await translateCQL(cqlPaths, client); | ||
const librariesOrError = await translateCQL(cqlPaths, client, mainLibraryId); | ||
@@ -41,0 +81,0 @@ if (librariesOrError instanceof Error) throw librariesOrError; |
102
src/index.ts
@@ -24,7 +24,11 @@ #!/usr/bin/env node | ||
program | ||
.requiredOption('-c, --cql-file <path>') | ||
.option('-c, --cql-file <path>') | ||
.option('-e,--elm-file <path>') | ||
.option('--debug', 'Enable debug mode to write contents to a ./debug directory', false) | ||
.addOption( | ||
new Option('--deps <deps...>', 'List of CQL dependency files of the main file').default([]) | ||
new Option('--deps <deps...>', 'List of CQL or ELM dependency files of the main file').default( | ||
[] | ||
) | ||
) | ||
.option('--deps-directory <path>', 'Directory containing all dependent CQL files') | ||
.option('--deps-directory <path>', 'Directory containing all dependent CQL or ELM files') | ||
.option('-n,--numer <expr>', 'Numerator expression name of measure', 'Numerator') | ||
@@ -60,4 +64,14 @@ .option('-i,--ipop <expr>', 'Numerator expression name of measure', 'Initial Population') | ||
if (opts.elmFile && opts.cqlFile) { | ||
logger.error('ERROR: Cannot use both -c/--cql-file and -e/--elm-file\n'); | ||
program.help(); | ||
} | ||
if (!(opts.elmFile || opts.cqlFile)) { | ||
logger.error('ERROR: Must specify one of -c/--cql-file or -e/--elm-file\n'); | ||
program.help(); | ||
} | ||
if (opts.deps.length !== 0 && opts.depsDirectory) { | ||
console.error('ERROR: Must specify only one of -d/--deps and --deps-directory\n'); | ||
logger.error('ERROR: Must specify only one of -d/--deps and --deps-directory\n'); | ||
program.help(); | ||
@@ -82,3 +96,9 @@ } | ||
.readdirSync(opts.depsDirectory) | ||
.filter(f => path.extname(f) === '.cql' && f !== path.basename(opts.cqlFile)) | ||
.filter(f => { | ||
if (opts.elmFile) { | ||
return path.extname(f) === '.json' && f !== path.basename(opts.elmFile); | ||
} else { | ||
return path.extname(f) === '.cql' && f !== path.basename(opts.cqlFile); | ||
} | ||
}) | ||
.map(f => path.join(depsBasePath, f)); | ||
@@ -89,34 +109,62 @@ } | ||
const mainCQLPath = path.resolve(opts.cqlFile); | ||
const allCQL = [mainCQLPath, ...deps]; | ||
logger.info(`Using ${mainCQLPath} as main library`); | ||
async function main(): Promise<fhir4.Bundle> { | ||
const mainLibraryId = getMainLibraryId(fs.readFileSync(mainCQLPath, 'utf8')); | ||
let elm: any[]; | ||
try { | ||
logger.info('Translating all CQL'); | ||
const [result, errors] = await getELM(allCQL, opts.translatorUrl); | ||
let mainLibraryId: string | null = null; | ||
if (opts.elmFile) { | ||
const mainELM = JSON.parse(fs.readFileSync(path.resolve(opts.elmFile), 'utf8')); | ||
if (result == null) { | ||
logger.error('Error translating CQL:'); | ||
console.error(errors); | ||
mainLibraryId = mainELM.library.identifier.id; | ||
if (!mainLibraryId) { | ||
logger.error(`Could not locate main library ID in ${opts.elmFile}`); | ||
process.exit(1); | ||
} | ||
elm = result; | ||
} catch (e: any) { | ||
logger.error(`HTTP error translating CQL: ${e.message}`); | ||
console.error(e.stack); | ||
if (e.response?.data) { | ||
console.log(e.response.data); | ||
elm = [mainELM, ...deps.map(d => JSON.parse(fs.readFileSync(path.resolve(d), 'utf8')))]; | ||
} else { | ||
const mainCQLPath = path.resolve(opts.cqlFile); | ||
const allCQL = [mainCQLPath, ...deps]; | ||
logger.info(`Using ${mainCQLPath} as main library`); | ||
mainLibraryId = getMainLibraryId(fs.readFileSync(mainCQLPath, 'utf8')); | ||
if (!mainLibraryId) { | ||
logger.error(`Could not locate main library ID in ${opts.cqlFile}`); | ||
process.exit(1); | ||
} | ||
process.exit(1); | ||
try { | ||
logger.info('Translating all CQL'); | ||
const [result, errors] = await getELM(allCQL, opts.translatorUrl, mainLibraryId); | ||
if (result == null) { | ||
logger.error('Error translating CQL:'); | ||
console.error(errors); | ||
process.exit(1); | ||
} | ||
elm = result; | ||
} catch (e: any) { | ||
logger.error(`HTTP error translating CQL: ${e.message}`); | ||
console.error(e.stack); | ||
if (e.response?.data) { | ||
console.log(e.response.data); | ||
} | ||
process.exit(1); | ||
} | ||
} | ||
if (!mainLibraryId) { | ||
logger.error(`Could not locate main library ID in ${mainCQLPath}`); | ||
process.exit(1); | ||
if (opts.debug === true) { | ||
if (!fs.existsSync('./debug')) { | ||
fs.mkdirSync('./debug'); | ||
} | ||
elm.forEach(e => { | ||
fs.writeFileSync( | ||
`./debug/${e.library.identifier.id}.json`, | ||
JSON.stringify(e, null, 2), | ||
'utf8' | ||
); | ||
}); | ||
} | ||
@@ -123,0 +171,0 @@ |
@@ -10,3 +10,3 @@ import fs from 'fs'; | ||
getELM([simpleLibraryCQL, simpleLibraryDependencyCQL], TRANSLATOR_URL) | ||
getELM([simpleLibraryCQL, simpleLibraryDependencyCQL], TRANSLATOR_URL, 'SimpleLibrary') | ||
.then(([elm, errors]) => { | ||
@@ -13,0 +13,0 @@ if (elm == null || errors != null) { |
@@ -14,3 +14,3 @@ import { getMainLibraryId } from '../../src/helpers/cql'; | ||
using FHIR version 4.0.1 | ||
`; | ||
`.trim(); | ||
@@ -26,3 +26,3 @@ expect(getMainLibraryId(cql)).toEqual(libraryId); | ||
using FHIR version 4.0.1 | ||
`; | ||
`.trim(); | ||
@@ -29,0 +29,0 @@ expect(getMainLibraryId(cql)).toEqual(libraryId); |
@@ -31,3 +31,4 @@ import path from 'path'; | ||
[SIMPLE_LIBRARY_CQL_PATH, SIMPLE_LIBRARY_DEPENDENCY_CQL_PATH], | ||
MOCK_URL | ||
MOCK_URL, | ||
simpleLibraryELM.library.identifier.id | ||
); | ||
@@ -43,3 +44,7 @@ | ||
await expect(() => | ||
getELM([SIMPLE_LIBRARY_CQL_PATH, SIMPLE_LIBRARY_DEPENDENCY_CQL_PATH], MOCK_URL) | ||
getELM( | ||
[SIMPLE_LIBRARY_CQL_PATH, SIMPLE_LIBRARY_DEPENDENCY_CQL_PATH], | ||
MOCK_URL, | ||
simpleLibraryELM.library.identifier.id | ||
) | ||
).rejects.toThrow(); | ||
@@ -63,3 +68,7 @@ }); | ||
const [elm, errors] = await getELM([SIMPLE_LIBRARY_CQL_PATH], MOCK_URL); | ||
const [elm, errors] = await getELM( | ||
[SIMPLE_LIBRARY_CQL_PATH], | ||
MOCK_URL, | ||
simpleLibraryELM.library.identifier.id | ||
); | ||
@@ -66,0 +75,0 @@ expect(elm).toBeNull(); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2346921
46
53974
112
1