Skip to content

Commit 79237e8

Browse files
authored
Merge pull request #55 from atomic-ehr/fhir-schema-export
Add FHIRSchema and StructureDefinition export for introspection
2 parents de10a93 + c7612b5 commit 79237e8

11 files changed

Lines changed: 4129 additions & 90 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,11 @@ const builder = new APIBuilder()
145145
146146
// Optional: Introspection & debugging
147147
.throwException() // Throw on errors (optional)
148-
.introspection({
149-
typeSchemas: "./schemas",
150-
typeTree: "./tree.yaml"
148+
.introspection({
149+
typeSchemas: "./schemas", // Export TypeSchemas
150+
typeTree: "./tree.yaml", // Export type tree
151+
fhirSchemas: "./fhir-schemas", // Export FHIR schemas
152+
structureDefinitions: "./sd" // Export StructureDefinitions
151153
})
152154
153155
// Execute generation

examples/typescript-r4/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
fhir-types/type-schemas/
2+
fhir-types/fhir-schemas/
3+
fhir-types/structure-definitions/

examples/typescript-r4/generate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ if (require.main === module) {
1717
.introspection({
1818
typeSchemas: "type-schemas",
1919
typeTree: "type-tree.yaml",
20+
fhirSchemas: "fhir-schemas",
21+
structureDefinitions: "structure-definitions",
2022
})
2123
.outputTo("./examples/typescript-r4/fhir-types")
2224
.treeShake({

src/api/builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export class APIBuilder {
396396
const typeSchemas = await generateTypeSchemas(register, this.logger);
397397

398398
const tsIndexOpts = {
399-
resolutionTree: register.resolutionTree(),
399+
register,
400400
logger: this.logger,
401401
};
402402
let tsIndex = mkTypeSchemaIndex(typeSchemas, tsIndexOpts);

src/api/writer-generator/introspection.ts

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import * as Path from "node:path";
2-
import { extractNameFromCanonical, type TypeSchema } from "@root/typeschema/types";
2+
import type { StructureDefinition } from "@atomic-ehr/fhirschema";
3+
import type { RichFHIRSchema } from "@root/typeschema/types";
4+
import { type CanonicalUrl, extractNameFromCanonical, type TypeSchema } from "@root/typeschema/types";
35
import type { TypeSchemaIndex } from "@root/typeschema/utils";
46
import YAML from "yaml";
57
import { FileSystemWriter, type FileSystemWriterOptions } from "./writer";
68

79
export interface IntrospectionWriterOptions extends FileSystemWriterOptions {
810
typeSchemas?: string /** if .ndjson -- put in one file, else -- split into separated files*/;
911
typeTree?: string /** .json or .yaml file */;
12+
fhirSchemas?: string /** if .ndjson -- put in one file, else -- split into separated files*/;
13+
structureDefinitions?: string /** if .ndjson -- put in one file, else -- split into separated files*/;
1014
}
1115

1216
const normalizeFileName = (str: string): string => {
@@ -15,6 +19,43 @@ const normalizeFileName = (str: string): string => {
1519
return res;
1620
};
1721

22+
const typeSchemaToJson = (ts: TypeSchema, pretty: boolean): { filename: string; content: string } => {
23+
const pkgPath = normalizeFileName(ts.identifier.package);
24+
const name = normalizeFileName(`${ts.identifier.name}(${extractNameFromCanonical(ts.identifier.url)})`);
25+
const baseName = Path.join(pkgPath, name);
26+
27+
return {
28+
filename: baseName,
29+
content: JSON.stringify(ts, null, pretty ? 2 : undefined),
30+
};
31+
};
32+
33+
const fhirSchemaToJson = (fs: RichFHIRSchema, pretty: boolean): { filename: string; content: string } => {
34+
const pkgPath = normalizeFileName(fs.package_meta.name);
35+
const name = normalizeFileName(`${fs.name}(${extractNameFromCanonical(fs.url)})`);
36+
const baseName = Path.join(pkgPath, name);
37+
38+
return {
39+
filename: baseName,
40+
content: JSON.stringify(fs, null, pretty ? 2 : undefined),
41+
};
42+
};
43+
44+
const structureDefinitionToJson = (sd: StructureDefinition, pretty: boolean): { filename: string; content: string } => {
45+
const pkgPath = normalizeFileName(sd.package_name ?? "unknown");
46+
const name = normalizeFileName(`${sd.name}(${extractNameFromCanonical(sd.url as CanonicalUrl)})`);
47+
const baseName = Path.join(pkgPath, name);
48+
49+
// HACK: for some reason ID may change between CI and local install
50+
sd = structuredClone(sd);
51+
sd.id = undefined;
52+
53+
return {
54+
filename: baseName,
55+
content: JSON.stringify(sd, null, pretty ? 2 : undefined),
56+
};
57+
};
58+
1859
export class IntrospectionWriter extends FileSystemWriter<IntrospectionWriterOptions> {
1960
async generate(tsIndex: TypeSchemaIndex): Promise<void> {
2061
if (this.opts.typeSchemas) {
@@ -24,9 +65,12 @@ export class IntrospectionWriter extends FileSystemWriter<IntrospectionWriterOpt
2465
this.logger()?.debug(`IntrospectionWriter: Generating ${typeSchemas.length} schemas to ${outputPath}`);
2566

2667
if (Path.extname(outputPath) === ".ndjson") {
27-
this.writeToSingleFile(typeSchemas, outputPath);
68+
this.writeNdjson(typeSchemas, outputPath, typeSchemaToJson);
2869
} else {
29-
this.writeToSeparateFiles(typeSchemas, outputPath);
70+
this.writeJsonFiles(
71+
typeSchemas.map((ts) => typeSchemaToJson(ts, true)),
72+
outputPath,
73+
);
3074
}
3175

3276
this.logger()?.info(`Introspection generation completed: ${typeSchemas.length} schemas written`);
@@ -36,67 +80,74 @@ export class IntrospectionWriter extends FileSystemWriter<IntrospectionWriterOpt
3680
await this.writeTypeTree(tsIndex);
3781
this.logger()?.info(`IntrospectionWriter: Type tree exported to ${this.opts.typeTree}`);
3882
}
39-
}
40-
41-
private async writeToSingleFile(typeSchemas: TypeSchema[], outputFile: string): Promise<void> {
42-
this.logger()?.info(`Writing introspection data to single file: ${outputFile}`);
4383

44-
const dir = Path.dirname(outputFile);
45-
const file = Path.basename(outputFile);
84+
if (this.opts.fhirSchemas && tsIndex.register) {
85+
const outputPath = this.opts.fhirSchemas;
86+
const fhirSchemas = tsIndex.register.allFs();
4687

47-
this.cd(dir, () => {
48-
this.cat(file, () => {
49-
for (const ts of typeSchemas) {
50-
const json = JSON.stringify(ts);
51-
this.write(`${json}\n`);
52-
}
53-
});
54-
});
88+
this.logger()?.debug(`IntrospectionWriter: Generating ${fhirSchemas.length} FHIR schemas to ${outputPath}`);
5589

56-
this.logger()?.info(`Single file output: ${typeSchemas.length} introspection entries written to ${outputFile}`);
57-
}
90+
if (Path.extname(outputPath) === ".ndjson") {
91+
this.writeNdjson(fhirSchemas, outputPath, fhirSchemaToJson);
92+
} else {
93+
this.writeJsonFiles(
94+
fhirSchemas.map((fs) => fhirSchemaToJson(fs, true)),
95+
outputPath,
96+
);
97+
}
5898

59-
private async writeToSeparateFiles(typeSchemas: TypeSchema[], outputDir: string): Promise<void> {
60-
this.logger()?.info(`Writing introspection data to separate files in ${outputDir}`);
99+
this.logger()?.info(`FHIR schema generation completed: ${fhirSchemas.length} schemas written`);
100+
}
61101

62-
// Group introspection data by package and name
63-
const files: Record<string, TypeSchema[]> = {};
102+
if (this.opts.structureDefinitions && tsIndex.register) {
103+
const outputPath = this.opts.structureDefinitions;
104+
const structureDefinitions = tsIndex.register.allSd();
64105

65-
for (const ts of typeSchemas) {
66-
const pkgPath = normalizeFileName(ts.identifier.package);
67-
const name = normalizeFileName(`${ts.identifier.name}(${extractNameFromCanonical(ts.identifier.url)})`);
68-
const baseName = Path.join(pkgPath, name);
106+
this.logger()?.debug(
107+
`IntrospectionWriter: Generating ${structureDefinitions.length} StructureDefinitions to ${outputPath}`,
108+
);
69109

70-
if (!files[baseName]) {
71-
files[baseName] = [];
110+
if (Path.extname(outputPath) === ".ndjson") {
111+
this.writeNdjson(structureDefinitions, outputPath, structureDefinitionToJson);
112+
} else {
113+
this.writeJsonFiles(
114+
structureDefinitions.map((sd) => structureDefinitionToJson(sd, true)),
115+
outputPath,
116+
);
72117
}
73118

74-
if (!files[baseName].some((e) => JSON.stringify(e) === JSON.stringify(ts))) {
75-
files[baseName].push(ts);
76-
}
119+
this.logger()?.info(
120+
`StructureDefinition generation completed: ${structureDefinitions.length} schemas written`,
121+
);
77122
}
123+
}
124+
125+
private async writeNdjson<T>(
126+
items: T[],
127+
outputFile: string,
128+
toJson: (item: T, pretty: boolean) => { filename: string; content: string },
129+
): Promise<void> {
130+
this.cd(Path.dirname(outputFile), () => {
131+
this.cat(Path.basename(outputFile), () => {
132+
for (const item of items) {
133+
const { content } = toJson(item, false);
134+
this.write(`${content}\n`);
135+
}
136+
});
137+
});
138+
}
78139

140+
private async writeJsonFiles(items: { filename: string; content: string }[], outputDir: string): Promise<void> {
79141
this.cd(outputDir, () => {
80-
for (const [baseName, schemas] of Object.entries(files)) {
81-
schemas.forEach((schema, index) => {
82-
const fileName =
83-
index === 0 ? `${baseName}.introspection.json` : `${baseName}-${index}.introspection.json`;
84-
const dir = Path.dirname(fileName);
85-
const file = Path.basename(fileName);
86-
87-
this.cd(dir, () => {
88-
this.cat(file, () => {
89-
const json = JSON.stringify(schema, null, 2);
90-
this.write(json);
91-
});
142+
for (const { filename, content } of items) {
143+
const fileName = `${filename}.json`;
144+
this.cd(Path.dirname(fileName), () => {
145+
this.cat(Path.basename(fileName), () => {
146+
this.write(content);
92147
});
93148
});
94149
}
95150
});
96-
97-
this.logger()?.info(
98-
`Separate files output: ${typeSchemas.length} introspection entries written to ${outputDir} in ${Object.keys(files).length} groups`,
99-
);
100151
}
101152

102153
private async writeTypeTree(tsIndex: TypeSchemaIndex): Promise<void> {

src/typeschema/register.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type Register = {
1818
resolveFs(pkg: PackageMeta, canonicalUrl: CanonicalUrl): RichFHIRSchema | undefined;
1919
resolveFsGenealogy(pkg: PackageMeta, canonicalUrl: CanonicalUrl): RichFHIRSchema[];
2020
resolveFsSpecializations(pkg: PackageMeta, canonicalUrl: CanonicalUrl): RichFHIRSchema[];
21+
allSd(): StructureDefinition[];
2122
allFs(): RichFHIRSchema[];
2223
allVs(): RichValueSet[];
2324
resolveVs(_pkg: PackageMeta, canonicalUrl: CanonicalUrl): RichValueSet | undefined;
@@ -248,6 +249,23 @@ export const registerFromManager = async (
248249
if (isStructureDefinition(res)) return res as StructureDefinition;
249250
return undefined;
250251
},
252+
allSd: () =>
253+
Object.values(resolver)
254+
.flatMap((pkgIndex) =>
255+
Object.values(pkgIndex.canonicalResolution).flatMap((resolutions) =>
256+
resolutions.map((r) => {
257+
let sd = r.resource as StructureDefinition;
258+
if (!sd.package_name) {
259+
sd = structuredClone(sd);
260+
sd.package_name = pkgIndex.pkg.name;
261+
sd.package_version = pkgIndex.pkg.version;
262+
}
263+
return sd;
264+
}),
265+
),
266+
)
267+
.filter((r): r is StructureDefinition => isStructureDefinition(r))
268+
.sort((sd1, sd2) => sd1.url.localeCompare(sd2.url)),
251269
allFs: () => Object.values(resolver).flatMap((pkgIndex) => Object.values(pkgIndex.fhirSchemas)),
252270
allVs: () => Object.values(resolver).flatMap((pkgIndex) => Object.values(pkgIndex.valueSets)),
253271
resolveVs,

src/typeschema/tree-shake.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export const treeShake = (
280280
const shaked = collectDeps(focusedSchemas, {});
281281

282282
const report: TreeShakeReport = { skippedPackages: [], packages: {} };
283-
const shakedIndex = mkTypeSchemaIndex(shaked, { resolutionTree, logger, treeShakeReport: report });
283+
const shakedIndex = mkTypeSchemaIndex(shaked, { register: tsIndex.register, logger, treeShakeReport: report });
284284
mutableFillReport(report, tsIndex, shakedIndex);
285285
return shakedIndex;
286286
};

src/typeschema/utils.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as Path from "node:path";
33
import type { TreeShakeReport } from "@root/typeschema/tree-shake";
44
import type { CodegenLogger } from "@root/utils/codegen-logger";
55
import * as YAML from "yaml";
6-
import type { ResolutionTree } from "./register";
6+
import type { Register } from "./register";
77
import {
88
type CanonicalUrl,
99
type Field,
@@ -154,6 +154,7 @@ export type TypeSchemaIndex = {
154154
_relations: TypeRelation[];
155155
schemas: TypeSchema[];
156156
schemasByPackage: Record<PackageName, TypeSchema[]>;
157+
register?: Register;
157158
collectComplexTypes: () => RegularTypeSchema[];
158159
collectResources: () => RegularTypeSchema[];
159160
collectLogicalModels: () => RegularTypeSchema[];
@@ -177,11 +178,11 @@ type EntityTree = Record<PackageName, Record<Identifier["kind"], Record<Canonica
177178
export const mkTypeSchemaIndex = (
178179
schemas: TypeSchema[],
179180
{
180-
resolutionTree,
181+
register,
181182
logger,
182183
treeShakeReport,
183184
}: {
184-
resolutionTree?: ResolutionTree;
185+
register?: Register;
185186
logger?: CodegenLogger;
186187
treeShakeReport?: TreeShakeReport;
187188
},
@@ -222,7 +223,8 @@ export const mkTypeSchemaIndex = (
222223
return index[id.url]?.[id.package];
223224
};
224225
const resolveByUrl = (pkgName: PackageName, url: CanonicalUrl) => {
225-
if (resolutionTree) {
226+
if (register) {
227+
const resolutionTree = register.resolutionTree();
226228
const resolution = resolutionTree[pkgName]?.[url]?.[0];
227229
if (resolution) {
228230
return index[url]?.[resolution.pkg.name];
@@ -366,6 +368,7 @@ export const mkTypeSchemaIndex = (
366368
_relations: relations,
367369
schemas,
368370
schemasByPackage: groupByPackages(schemas),
371+
register,
369372
collectComplexTypes: () => schemas.filter(isComplexTypeTypeSchema),
370373
collectResources: () => schemas.filter(isResourceTypeSchema),
371374
collectLogicalModels: () => schemas.filter(isLogicalTypeSchema),

0 commit comments

Comments
 (0)