diff --git a/package.json b/package.json index 115d9510..e0d64a58 100644 --- a/package.json +++ b/package.json @@ -68,10 +68,10 @@ "serverless-plugin-typescript": "^1.1.9", "serverless-step-functions": "^2.27.1", "sinon": "^9.0.2", + "tmp-promise": "^3.0.2", "ts-jest": "^26.4.4", "typescript": "^4.1.3", - "wait-for-expect": "^3.0.2", - "tmp-promise": "^3.0.2" + "wait-for-expect": "^3.0.2" }, "resolutions": { "dot-prop": "^5.1.1", diff --git a/scripts/compile-igs.ts b/scripts/compile-igs.ts index ab2ae127..3065f797 100644 --- a/scripts/compile-igs.ts +++ b/scripts/compile-igs.ts @@ -1,14 +1,16 @@ -import { join, dirname } from 'path'; import yargs from 'yargs'; -import { SearchImplementationGuides } from 'fhir-works-on-aws-search-es'; +import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; +import { SearchImplementationGuides } from 'fhir-works-on-aws-search-es'; +import { StructureDefinitionImplementationGuides } from 'fhir-works-on-aws-routing/lib/implementationGuides'; import { IGCompiler } from '../src/implementationGuides/IGCompiler'; +import { COMPILED_IGS_DIRECTORY } from '../src/implementationGuides/loadCompiledIGs'; const PROJECT_DIR = join(__dirname, '..'); function parseCmdOptions() { return yargs(process.argv.slice(2)) - .usage('Usage: $0 [--ignoreVersion, -i ] [--igPath, -p IG pack directory] [--outputFile, -o output ]') + .usage('Usage: $0 [--ignoreVersion, -i ] [--igPath, -p IG pack directory] [--outputDir, -o output ]') .describe('ignoreVersion', "Don't care whether version of dependency lines up with version of installed IG") .boolean('ignoreVersion') .default('ignoreVersion', false) @@ -16,9 +18,9 @@ function parseCmdOptions() { .describe('igPath', 'Path to folder with IG pack sub folders') .default('igPath', join(PROJECT_DIR, 'implementationGuides/')) .alias('p', 'igPath') - .describe('outputFile', 'Path to compiled output JSON file') - .alias('o', 'outputFile') - .default('outputFile', join(PROJECT_DIR, 'compiledImplementationGuides/fhir-works-on-aws-search-es.json')).argv; + .describe('outputDir', 'Path to compiled output JSON file') + .alias('o', 'outputDir') + .default('outputDir', join(PROJECT_DIR, COMPILED_IGS_DIRECTORY)).argv; } /** @@ -34,14 +36,18 @@ async function compileIGs() { console.log(`IGs folder '${cmdArgs.igPath}' does not exist. No IGs found, exiting...`); return; } - const compiledIgsDir = dirname(cmdArgs.outputFile); + const compiledIgsDir = cmdArgs.outputDir.toString(); if (!existsSync(compiledIgsDir)) { console.log(`folder for compiled IGs '${compiledIgsDir}' does not exist, creating it`); mkdirSync(compiledIgsDir, { recursive: true }); } try { - await new IGCompiler(SearchImplementationGuides, options).compileIGs(cmdArgs.igPath, cmdArgs.outputFile); + await new IGCompiler( + SearchImplementationGuides, + new StructureDefinitionImplementationGuides(), + options, + ).compileIGs(cmdArgs.igPath, cmdArgs.outputDir); } catch (ex) { console.error('Exception: ', ex.message, ex.stack); } diff --git a/src/config.ts b/src/config.ts index 7d48176d..b2af507a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -24,7 +24,6 @@ const baseResources = fhirVersion === '4.0.1' ? BASE_R4_RESOURCES : BASE_STU3_RE const authService = IS_OFFLINE ? stubs.passThroughAuthz : new RBACHandler(RBACRules(baseResources), fhirVersion); const dynamoDbDataService = new DynamoDbDataService(DynamoDb); const dynamoDbBundleService = new DynamoDbBundleService(DynamoDb); - const esSearch = new ElasticSearchService( [ { @@ -79,6 +78,7 @@ export const fhirConfig: FhirConfig = { profile: { systemOperations: ['transaction'], bundle: dynamoDbBundleService, + compiledImplementationGuides: loadImplementationGuides('fhir-works-on-aws-routing'), systemHistory: stubs.history, systemSearch: stubs.search, bulkDataAccess: dynamoDbDataService, diff --git a/src/implementationGuides/IGCompiler.test.ts b/src/implementationGuides/IGCompiler.test.ts index 71896815..15055c62 100644 --- a/src/implementationGuides/IGCompiler.test.ts +++ b/src/implementationGuides/IGCompiler.test.ts @@ -24,7 +24,7 @@ interface IGVersion { describe('IGCompiler tests', () => { let workDir: DirectoryResult; let igsDir: PathLike; - let output: string; + let outputDir: string; async function createIGs(options: IGCompilerOptions, igs: { [key: string]: IGVersion }) { for (const [igName, igInfo] of Object.entries(igs)) { @@ -32,6 +32,7 @@ describe('IGCompiler tests', () => { mkdirSync(igPath); await storeJson(join(igPath, 'searchParam1.json'), { igName, name: 'param1' }); await storeJson(join(igPath, 'searchParam2.json'), { igName, name: 'param2' }); + await storeJson(join(igPath, 'structureDefinition1.json'), { igName, name: 'structureDefinition1' }); const indexJson = { files: [ { @@ -44,7 +45,11 @@ describe('IGCompiler tests', () => { }, { resourceType: 'ValueSet', - filename: 'searchParam1.json', + filename: 'valueSet1.json', + }, + { + resourceType: 'StructureDefinition', + filename: 'structureDefinition1.json', }, ], }; @@ -62,7 +67,7 @@ describe('IGCompiler tests', () => { }); } const implementationGuides = new MockImplementationGuides(); - const igCompiler = new IGCompiler(implementationGuides, options); + const igCompiler = new IGCompiler(implementationGuides, implementationGuides, options); console.log('Done creating IGs'); return igCompiler; } @@ -71,7 +76,8 @@ describe('IGCompiler tests', () => { workDir = await dir({ unsafeCleanup: true }); igsDir = join(workDir.path, 'igs'); mkdirSync(igsDir); - output = join(workDir.path, 'output.json'); + outputDir = join(workDir.path, 'output'); + mkdirSync(outputDir); console.log('before each'); }); @@ -102,9 +108,9 @@ describe('IGCompiler tests', () => { deps: ['hl7.fhir.us.core@3.1.0'], }, }); - await igCompiler.compileIGs(igsDir, output); - expect(existsSync(output)).toEqual(true); - expect(await loadJson(output)).toEqual({ + await igCompiler.compileIGs(igsDir, outputDir); + expect(existsSync(outputDir)).toEqual(true); + expect(await loadJson(join(outputDir, 'fhir-works-on-aws-search-es.json'))).toEqual({ input: [ { igName: 'hl7.fhir.us.carin-bb', @@ -140,6 +146,15 @@ describe('IGCompiler tests', () => { }, ], }); + + expect(await loadJson(join(outputDir, 'fhir-works-on-aws-routing.json'))).toEqual({ + input: [ + { igName: 'hl7.fhir.us.carin-bb', name: 'structureDefinition1' }, + { igName: 'hl7.fhir.us.core', name: 'structureDefinition1' }, + { igName: 'hl7.fhir.us.davinci-pdex-plan-net', name: 'structureDefinition1' }, + { igName: 'us.nlm.vsac', name: 'structureDefinition1' }, + ], + }); }); it('missing dependencies', async () => { @@ -156,7 +171,7 @@ describe('IGCompiler tests', () => { deps: ['hl7.fhir.r4.core@4.0.1', 'us.nlm.vsac@0.3.0'], }, }); - await expect(igCompiler.compileIGs(igsDir, output)).rejects.toThrow('Missing dependency us.nlm.vsac@0.3.0'); + await expect(igCompiler.compileIGs(igsDir, outputDir)).rejects.toThrow('Missing dependency us.nlm.vsac@0.3.0'); }); it('circular dependencies', async () => { @@ -185,7 +200,7 @@ describe('IGCompiler tests', () => { deps: ['hl7.fhir.us.core@3.1.0', 'hl7.fhir.us.davinci-pdex-plan-net@1.0.0'], }, }); - await expect(igCompiler.compileIGs(igsDir, output)).rejects.toThrow( + await expect(igCompiler.compileIGs(igsDir, outputDir)).rejects.toThrow( 'Circular dependency found: hl7.fhir.us.carin-bb@1.0.0 -> hl7.fhir.us.core@3.1.0 -> us.nlm.vsac@0.3.0 -> hl7.fhir.us.core@3.1.0', ); }); diff --git a/src/implementationGuides/IGCompiler.ts b/src/implementationGuides/IGCompiler.ts index 2757eb2e..8b683eac 100644 --- a/src/implementationGuides/IGCompiler.ts +++ b/src/implementationGuides/IGCompiler.ts @@ -49,24 +49,29 @@ export interface IGInfo { } /** - * Helper class used for compiling IGs. Its main functions are: - * - Looks through a folder of IG packs - * - Make sure no dependencies are missing - * - and there are no circular dependencies - * - calls compile function for each SearchParameters - * + * Helper class used for compiling Implementation Guides packages */ export class IGCompiler { private options: IGCompilerOptions; - private readonly implementationGuides: ImplementationGuides; + private readonly searchImplementationGuides: ImplementationGuides; - constructor(implementationGuides: ImplementationGuides, options: IGCompilerOptions) { + private readonly structureDefinitionImplementationGuides: ImplementationGuides; + + constructor( + searchImplementationGuides: ImplementationGuides, + structureDefinitionImplementationGuides: ImplementationGuides, + options: IGCompilerOptions, + ) { + this.searchImplementationGuides = searchImplementationGuides; + this.structureDefinitionImplementationGuides = structureDefinitionImplementationGuides; this.options = options; - this.implementationGuides = implementationGuides; } - async collectResources(igDir: PathLike): Promise { + private async collectResources( + igDir: PathLike, + resourceType: 'SearchParameter' | 'StructureDefinition', + ): Promise { const indexJson = path.join(igDir.toString(), '.index.json'); if (!existsSync(indexJson)) { throw new Error(`'.index.json' not found in ${igDir}`); @@ -74,7 +79,7 @@ export class IGCompiler { const index: any = await loadJson(indexJson); const resources = []; for (const file of index.files) { - if (file.resourceType === 'SearchParameter') { + if (file.resourceType === resourceType) { const filePath = path.join(igDir.toString(), file.filename); console.log(`Compiling ${filePath}`); resources.push(await loadJson(filePath)); @@ -84,7 +89,9 @@ export class IGCompiler { } /** - * Main public function + * Compiles the implementation guides packages located at `igsDir` and saves the results in `outputPath` + * + * This method delegates the compilation of specific resource types to the implementations of `ImplementationGuides.compile` from other fhir-works-on-aws modules. * @param igsDir * @param outputPath */ @@ -94,22 +101,33 @@ export class IGCompiler { } const igInfos = await this.collectIGInfos(igsDir); this.validateDependencies(igInfos); - const resources: any[] = []; + + const searchParams: any[] = []; + const structureDefinitions: any[] = []; for (const igInfo of igInfos) { - resources.push(...(await this.collectResources(igInfo.path))); + searchParams.push(...(await this.collectResources(igInfo.path, 'SearchParameter'))); + structureDefinitions.push(...(await this.collectResources(igInfo.path, 'StructureDefinition'))); } - const compiledResources = await this.implementationGuides.compile(resources); - await storeJson(outputPath, compiledResources); + const compiledSearchParams = await this.searchImplementationGuides.compile(searchParams); + const compiledStructureDefinitions = await this.structureDefinitionImplementationGuides.compile( + structureDefinitions, + ); + + await storeJson(path.join(outputPath.toString(), 'fhir-works-on-aws-search-es.json'), compiledSearchParams); + await storeJson( + path.join(outputPath.toString(), 'fhir-works-on-aws-routing.json'), + compiledStructureDefinitions, + ); } - createIGKey(name: string, version: string) { + private createIGKey(name: string, version: string) { if (this.options.ignoreVersion) { return name; } return `${name}@${version}`; } - async extractIgInfo(igDir: PathLike): Promise { + private async extractIgInfo(igDir: PathLike): Promise { const packagePath = path.join(igDir.toString(), 'package.json'); if (!existsSync(packagePath)) { throw new Error(`'package.json' not found in ${igDir}`); @@ -135,7 +153,7 @@ export class IGCompiler { return igInfo; } - async collectIGInfos(igsDir: PathLike): Promise { + private async collectIGInfos(igsDir: PathLike): Promise { const igInfos: IGInfo[] = []; for (const igPath of await listIgDirs(igsDir)) { console.log(`looking at ig path: ${igPath}`); @@ -151,7 +169,7 @@ export class IGCompiler { return igInfos; } - validateDependencies(igInfos: IGInfo[]): void { + private validateDependencies(igInfos: IGInfo[]): void { const parentMap: { [key: string]: string[] } = {}; for (const igInfo of igInfos) { parentMap[igInfo.id] = igInfo.dependencies; @@ -161,7 +179,7 @@ export class IGCompiler { } } - depthFirst(parents: string[], pMap: { [key: string]: string[] }): void { + private depthFirst(parents: string[], pMap: { [key: string]: string[] }): void { const igId = parents[parents.length - 1]; const dependencies = pMap[igId]; if (!dependencies) { diff --git a/src/implementationGuides/loadCompiledIGs.ts b/src/implementationGuides/loadCompiledIGs.ts index ec78adad..2fbc73d0 100644 --- a/src/implementationGuides/loadCompiledIGs.ts +++ b/src/implementationGuides/loadCompiledIGs.ts @@ -4,17 +4,28 @@ */ import path from 'path'; -import { existsSync, readFileSync } from 'fs'; +import { existsSync, PathLike, readFileSync } from 'fs'; -const COMPILED_IGS_DIRECTORY = 'compiledImplementationGuides'; +export const COMPILED_IGS_DIRECTORY = 'compiledImplementationGuides'; +/** + * Loads the compiled Implementation Guides for a given 'fhir-works-at-aws' module. + * By default they are located on a file named "compiledImplementationGuides/.json" + * @param moduleName + * @param implementationGuidesPath - allows to override the path to the compiled Implementation Guides directory + */ // eslint-disable-next-line import/prefer-default-export -export const loadImplementationGuides = (moduleName: string): any[] | undefined => { - const implementationGuidesPath = path.join(__dirname, '..', '..', COMPILED_IGS_DIRECTORY); - const searchIgsPath = path.join(implementationGuidesPath, `${moduleName}.json`); +export const loadImplementationGuides = ( + moduleName: string, + implementationGuidesPath?: PathLike, +): any[] | undefined => { + const resolvedImplementationGuidesPath = + implementationGuidesPath ?? path.join(__dirname, '..', '..', COMPILED_IGS_DIRECTORY); + + const igsPath = path.join(resolvedImplementationGuidesPath.toString(), `${moduleName}.json`); - if (existsSync(searchIgsPath)) { - return JSON.parse(readFileSync(searchIgsPath, { encoding: 'utf8' })); + if (existsSync(igsPath)) { + return JSON.parse(readFileSync(igsPath, { encoding: 'utf8' })); } return undefined; };