fhir-openapi-translator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/definitions/r4/fhir.schema.json.gz +0 -0
- package/definitions/r4/operation-definitions.json.gz +0 -0
- package/definitions/r4/search-parameters.json.gz +0 -0
- package/definitions/r4/structure-definitions.json.gz +0 -0
- package/definitions/r4b/fhir.schema.json.gz +0 -0
- package/definitions/r4b/operation-definitions.json.gz +0 -0
- package/definitions/r4b/search-parameters.json.gz +0 -0
- package/definitions/r4b/structure-definitions.json.gz +0 -0
- package/definitions/r5/fhir.schema.json.gz +0 -0
- package/definitions/r5/operation-definitions.json.gz +0 -0
- package/definitions/r5/search-parameters.json.gz +0 -0
- package/definitions/r5/structure-definitions.json.gz +0 -0
- package/dist/chunk-G3DCRADV.js +1429 -0
- package/dist/chunk-G3DCRADV.js.map +1 -0
- package/dist/cli.js +170 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +253 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/docs/REFERENCE.md +200 -0
- package/docs/demo.gif +0 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1429 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var FHIR_VERSIONS = ["r4", "r4b", "r5"];
|
|
3
|
+
var FHIR_VERSION_NUMBERS = {
|
|
4
|
+
r4: "4.0.1",
|
|
5
|
+
r4b: "4.3.0",
|
|
6
|
+
r5: "5.0.0"
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/ig/package.ts
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import path from "path";
|
|
14
|
+
|
|
15
|
+
// src/ig/minimize.ts
|
|
16
|
+
var MAX_ENUM_CODES = 150;
|
|
17
|
+
var FHIR_TYPE_EXTENSION = "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type";
|
|
18
|
+
function fixedValue(el) {
|
|
19
|
+
for (const key of Object.keys(el)) {
|
|
20
|
+
if (key.startsWith("fixed") && key.length > 5) return el[key];
|
|
21
|
+
}
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
function omittedConstraints(el) {
|
|
25
|
+
const notes = [];
|
|
26
|
+
if (el.slicing || el.sliceName) notes.push("slicing");
|
|
27
|
+
if (Object.keys(el).some((k) => k.startsWith("pattern") && k.length > 7)) {
|
|
28
|
+
notes.push("pattern");
|
|
29
|
+
}
|
|
30
|
+
return notes.length > 0 ? notes : void 0;
|
|
31
|
+
}
|
|
32
|
+
function minimizeElement(el, resolveValueSet) {
|
|
33
|
+
const out = { path: el.path, min: el.min ?? 0, max: el.max ?? "*" };
|
|
34
|
+
if (el.short) out.short = el.short;
|
|
35
|
+
if (el.definition) out.definition = el.definition;
|
|
36
|
+
if (el.contentReference) out.contentReference = el.contentReference;
|
|
37
|
+
if (el.type) {
|
|
38
|
+
out.types = el.type.map((t) => {
|
|
39
|
+
const type = { code: t.code };
|
|
40
|
+
if (t.targetProfile) type.targetProfile = t.targetProfile;
|
|
41
|
+
const fhirType = (t.extension ?? []).find((e) => e.url === FHIR_TYPE_EXTENSION);
|
|
42
|
+
if (fhirType?.valueUrl) type.fhirType = fhirType.valueUrl;
|
|
43
|
+
return type;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (el.binding?.strength === "required" && el.binding.valueSet) {
|
|
47
|
+
out.binding = { strength: el.binding.strength, valueSet: el.binding.valueSet };
|
|
48
|
+
const codes = resolveValueSet?.(el.binding.valueSet);
|
|
49
|
+
if (codes) out.binding.codes = [...codes].sort();
|
|
50
|
+
}
|
|
51
|
+
const fixed = fixedValue(el);
|
|
52
|
+
if (fixed !== void 0) out.fixed = fixed;
|
|
53
|
+
if (el.mustSupport) out.mustSupport = true;
|
|
54
|
+
const omitted = omittedConstraints(el);
|
|
55
|
+
if (omitted) out.omittedConstraints = omitted;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function minimizeStructureDefinition(sd, resolveValueSet) {
|
|
59
|
+
return {
|
|
60
|
+
name: sd.name,
|
|
61
|
+
url: sd.url,
|
|
62
|
+
kind: sd.kind,
|
|
63
|
+
type: sd.type,
|
|
64
|
+
abstract: !!sd.abstract,
|
|
65
|
+
baseDefinition: sd.baseDefinition,
|
|
66
|
+
elements: (sd.snapshot?.element ?? []).map((el) => minimizeElement(el, resolveValueSet))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function buildValueSetResolver(resources, fallback) {
|
|
70
|
+
const codeSystems = /* @__PURE__ */ new Map();
|
|
71
|
+
const valueSets = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const r of resources) {
|
|
73
|
+
if (r.resourceType === "CodeSystem" && r.url) codeSystems.set(r.url, r);
|
|
74
|
+
if (r.resourceType === "ValueSet" && r.url) valueSets.set(r.url, r);
|
|
75
|
+
}
|
|
76
|
+
function conceptCodes(concepts, out) {
|
|
77
|
+
for (const c of concepts ?? []) {
|
|
78
|
+
out.push(c.code);
|
|
79
|
+
if (c.concept) conceptCodes(c.concept, out);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function resolveGroup(group) {
|
|
84
|
+
if (group.filter?.length || group.valueSet?.length) return void 0;
|
|
85
|
+
if (group.concept?.length) return group.concept.map((c) => c.code);
|
|
86
|
+
if (!group.system) return void 0;
|
|
87
|
+
const cs = codeSystems.get(group.system);
|
|
88
|
+
if (!cs || cs.content === "not-present" || !cs.concept) return void 0;
|
|
89
|
+
return conceptCodes(cs.concept, []);
|
|
90
|
+
}
|
|
91
|
+
return function resolve(valueSetUrl) {
|
|
92
|
+
const versionless = valueSetUrl.split("|")[0];
|
|
93
|
+
const vs = valueSets.get(versionless);
|
|
94
|
+
if (!vs?.compose?.include?.length) return fallback?.(valueSetUrl);
|
|
95
|
+
const codes = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const group of vs.compose.include) {
|
|
97
|
+
const groupCodes = resolveGroup(group);
|
|
98
|
+
if (!groupCodes) return fallback?.(valueSetUrl);
|
|
99
|
+
for (const code of groupCodes) codes.add(code);
|
|
100
|
+
}
|
|
101
|
+
for (const group of vs.compose.exclude ?? []) {
|
|
102
|
+
const groupCodes = resolveGroup(group);
|
|
103
|
+
if (!groupCodes) return fallback?.(valueSetUrl);
|
|
104
|
+
for (const code of groupCodes) codes.delete(code);
|
|
105
|
+
}
|
|
106
|
+
if (codes.size === 0 || codes.size > MAX_ENUM_CODES) return fallback?.(valueSetUrl);
|
|
107
|
+
return [...codes];
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/ig/package.ts
|
|
112
|
+
var FHIR_VERSION_BY_NUMBER = {
|
|
113
|
+
"4.0.1": "r4",
|
|
114
|
+
"4.0.0": "r4",
|
|
115
|
+
"4.3.0": "r4b",
|
|
116
|
+
"5.0.0": "r5"
|
|
117
|
+
};
|
|
118
|
+
var REGISTRY_BASE = "https://packages.fhir.org";
|
|
119
|
+
async function loadIg(input, options = {}) {
|
|
120
|
+
if (isRegistryCoordinate(input)) {
|
|
121
|
+
const tarball = await fetchFromRegistry(input, options);
|
|
122
|
+
return buildContext(readFromTarball(tarball), options.coreValueSetFallback);
|
|
123
|
+
}
|
|
124
|
+
return loadIgSync(input, options);
|
|
125
|
+
}
|
|
126
|
+
function loadIgSync(input, options = {}) {
|
|
127
|
+
if (isRegistryCoordinate(input)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`"${input}" looks like a registry package coordinate, which requires a network fetch. Use loadIg() (async) or the CLI, or download the package and pass a local path.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const stat = fs.existsSync(input) ? fs.statSync(input) : void 0;
|
|
133
|
+
if (!stat) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`--ig "${input}" is not a file or directory. Pass a package .tgz, an unpacked package directory, or a registry coordinate like hl7.fhir.us.core@5.0.1.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const files = stat.isDirectory() ? readFromDirectory(input) : readFromTarball(input);
|
|
139
|
+
return buildContext(files, options.coreValueSetFallback);
|
|
140
|
+
}
|
|
141
|
+
function isRegistryCoordinate(input) {
|
|
142
|
+
if (fs.existsSync(input)) return false;
|
|
143
|
+
if (input.includes("/") || input.includes("\\") || input.endsWith(".tgz")) return false;
|
|
144
|
+
return /^[a-z0-9][a-z0-9.\-]*(@[\w.\-]+)?$/i.test(input);
|
|
145
|
+
}
|
|
146
|
+
function packageDir(root) {
|
|
147
|
+
const nested = path.join(root, "package");
|
|
148
|
+
if (fs.existsSync(path.join(nested, "package.json"))) return nested;
|
|
149
|
+
if (fs.existsSync(path.join(root, "package.json"))) return root;
|
|
150
|
+
throw new Error(`No package.json found under "${root}" (looked in ./ and ./package)`);
|
|
151
|
+
}
|
|
152
|
+
function readFromDirectory(dir) {
|
|
153
|
+
const base = packageDir(dir);
|
|
154
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(base, "package.json"), "utf8"));
|
|
155
|
+
const resources = [];
|
|
156
|
+
for (const file of fs.readdirSync(base).sort()) {
|
|
157
|
+
if (!file.endsWith(".json") || file === "package.json" || file === ".index.json") continue;
|
|
158
|
+
try {
|
|
159
|
+
resources.push(JSON.parse(fs.readFileSync(path.join(base, file), "utf8")));
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { packageJson, resources };
|
|
164
|
+
}
|
|
165
|
+
function readFromTarball(tarballPath) {
|
|
166
|
+
if (!fs.existsSync(tarballPath)) {
|
|
167
|
+
throw new Error(`Package tarball not found: ${tarballPath}`);
|
|
168
|
+
}
|
|
169
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fhir-oas-ig-"));
|
|
170
|
+
try {
|
|
171
|
+
execFileSync("tar", ["-xzf", tarballPath, "-C", tmp], { stdio: "pipe" });
|
|
172
|
+
return readFromDirectory(tmp);
|
|
173
|
+
} finally {
|
|
174
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function fetchFromRegistry(coordinate, options) {
|
|
178
|
+
const [name, version] = coordinate.split("@");
|
|
179
|
+
const cacheDir = path.join(options.cacheDir ?? path.join(os.homedir(), ".fhir-oas"), "packages");
|
|
180
|
+
const resolvedVersion = version ?? "latest";
|
|
181
|
+
const cachePath = path.join(cacheDir, `${name}#${resolvedVersion}.tgz`);
|
|
182
|
+
if (fs.existsSync(cachePath)) return cachePath;
|
|
183
|
+
const url = `${REGISTRY_BASE}/${name}/${resolvedVersion}`;
|
|
184
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
185
|
+
let response;
|
|
186
|
+
try {
|
|
187
|
+
response = await doFetch(url);
|
|
188
|
+
} catch (cause) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Failed to reach the FHIR package registry at ${url}: ${cause.message}. If you are offline or behind a restrictive proxy, download the package and pass --ig ./${name}.tgz instead.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`FHIR package registry returned ${response.status} for ${url}. Check the package name and version, or download it and pass --ig ./${name}.tgz.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
199
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
200
|
+
fs.writeFileSync(cachePath, buffer);
|
|
201
|
+
return cachePath;
|
|
202
|
+
}
|
|
203
|
+
function detectFhirVersion(packageJson) {
|
|
204
|
+
const versions = packageJson["fhir-version-list"] ?? packageJson.fhirVersions ?? [];
|
|
205
|
+
for (const v of versions) {
|
|
206
|
+
const mapped = FHIR_VERSION_BY_NUMBER[v];
|
|
207
|
+
if (mapped) return mapped;
|
|
208
|
+
}
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Could not determine a supported FHIR version for IG package "${packageJson.name}" (found: ${versions.join(", ") || "none"}). Supported: ${FHIR_VERSIONS.join(", ")}.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
function buildContext(files, coreFallback) {
|
|
214
|
+
const fhirVersion = detectFhirVersion(files.packageJson);
|
|
215
|
+
const terminology = files.resources.filter(
|
|
216
|
+
(r) => r.resourceType === "ValueSet" || r.resourceType === "CodeSystem"
|
|
217
|
+
);
|
|
218
|
+
const resolveValueSet = buildValueSetResolver(terminology, coreFallback);
|
|
219
|
+
const profiles = [];
|
|
220
|
+
const profilesMissingSnapshot = [];
|
|
221
|
+
for (const r of files.resources) {
|
|
222
|
+
if (r.resourceType === "StructureDefinition" && r.derivation === "constraint" && r.kind === "resource") {
|
|
223
|
+
if (!r.snapshot) {
|
|
224
|
+
profilesMissingSnapshot.push(r.name ?? r.url ?? "unknown");
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const sd = r;
|
|
228
|
+
profiles.push({
|
|
229
|
+
url: sd.url.split("|")[0],
|
|
230
|
+
id: sd.id,
|
|
231
|
+
name: sd.name,
|
|
232
|
+
type: sd.type,
|
|
233
|
+
definition: minimizeStructureDefinition(sd, resolveValueSet)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
profiles.sort((a, b) => a.url.localeCompare(b.url));
|
|
238
|
+
return {
|
|
239
|
+
name: files.packageJson.name ?? "unknown",
|
|
240
|
+
version: files.packageJson.version ?? "unknown",
|
|
241
|
+
fhirVersion,
|
|
242
|
+
profiles,
|
|
243
|
+
profilesMissingSnapshot,
|
|
244
|
+
resolveValueSet
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/definitions.ts
|
|
249
|
+
import { gunzipSync } from "zlib";
|
|
250
|
+
import fs2 from "fs";
|
|
251
|
+
import path2 from "path";
|
|
252
|
+
import { fileURLToPath } from "url";
|
|
253
|
+
function definitionsRoot() {
|
|
254
|
+
let dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
255
|
+
for (let i = 0; i < 5; i++) {
|
|
256
|
+
const candidate = path2.join(dir, "definitions");
|
|
257
|
+
if (fs2.existsSync(candidate)) return candidate;
|
|
258
|
+
dir = path2.dirname(dir);
|
|
259
|
+
}
|
|
260
|
+
throw new Error("Could not locate the vendored FHIR definitions directory");
|
|
261
|
+
}
|
|
262
|
+
var cache = /* @__PURE__ */ new Map();
|
|
263
|
+
function loadGzJson(fhirVersion, file) {
|
|
264
|
+
const key = `${fhirVersion}/${file}`;
|
|
265
|
+
let value = cache.get(key);
|
|
266
|
+
if (value === void 0) {
|
|
267
|
+
const fullPath = path2.join(definitionsRoot(), fhirVersion, `${file}.gz`);
|
|
268
|
+
value = JSON.parse(gunzipSync(fs2.readFileSync(fullPath)).toString("utf8"));
|
|
269
|
+
cache.set(key, value);
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function loadFhirSchema(fhirVersion) {
|
|
274
|
+
return loadGzJson(fhirVersion, "fhir.schema.json");
|
|
275
|
+
}
|
|
276
|
+
function loadSearchParameters(fhirVersion) {
|
|
277
|
+
const bundle = loadGzJson(fhirVersion, "search-parameters.json");
|
|
278
|
+
return (bundle.entry ?? []).map((e) => e.resource).filter(
|
|
279
|
+
// R4B ships a few draft codesystem-extensions-* SearchParameters with no
|
|
280
|
+
// `base`; they can't be attached to any resource, so drop them here.
|
|
281
|
+
(r) => r?.resourceType === "SearchParameter" && Array.isArray(r.base)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function loadOperationDefinitions(fhirVersion) {
|
|
285
|
+
return loadGzJson(fhirVersion, "operation-definitions.json");
|
|
286
|
+
}
|
|
287
|
+
function loadStructureDefinitions(fhirVersion) {
|
|
288
|
+
return loadGzJson(fhirVersion, "structure-definitions.json");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/ir/structureWalker.ts
|
|
292
|
+
function segmentToPascal(segment) {
|
|
293
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
294
|
+
}
|
|
295
|
+
function isBackbone(el) {
|
|
296
|
+
return (el.types ?? []).some((t) => t.code === "BackboneElement" || t.code === "Element");
|
|
297
|
+
}
|
|
298
|
+
function systemTypeSchema(code) {
|
|
299
|
+
if (!code.startsWith("http://hl7.org/fhirpath/System.")) return void 0;
|
|
300
|
+
const kind = code.slice("http://hl7.org/fhirpath/System.".length);
|
|
301
|
+
switch (kind) {
|
|
302
|
+
case "Boolean":
|
|
303
|
+
return { type: "boolean" };
|
|
304
|
+
case "Integer":
|
|
305
|
+
case "Decimal":
|
|
306
|
+
return { type: "number" };
|
|
307
|
+
default:
|
|
308
|
+
return { type: "string" };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function buildElementTree(sd) {
|
|
312
|
+
const rootPath = sd.type ?? sd.name;
|
|
313
|
+
let root;
|
|
314
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const el of sd.elements) {
|
|
316
|
+
const node = { element: el, children: /* @__PURE__ */ new Map() };
|
|
317
|
+
nodes.set(el.path, node);
|
|
318
|
+
if (el.path === rootPath) {
|
|
319
|
+
root = node;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const parentPath = el.path.slice(0, el.path.lastIndexOf("."));
|
|
323
|
+
const segment = el.path.slice(el.path.lastIndexOf(".") + 1);
|
|
324
|
+
nodes.get(parentPath)?.children.set(segment, node);
|
|
325
|
+
}
|
|
326
|
+
return root;
|
|
327
|
+
}
|
|
328
|
+
function contentReferenceName(reference, sd, rootName) {
|
|
329
|
+
const path3 = reference.replace(/^#/, "");
|
|
330
|
+
const [root, ...rest] = path3.split(".");
|
|
331
|
+
if (rest.length === 0) return root ?? path3;
|
|
332
|
+
const prefix = root === (sd.type ?? sd.name) ? rootName : root ?? "";
|
|
333
|
+
return [prefix, ...rest.map(segmentToPascal)].join("_");
|
|
334
|
+
}
|
|
335
|
+
function emitStructureDefinitionSchemas(sd, definitions, opts) {
|
|
336
|
+
const root = buildElementTree(sd);
|
|
337
|
+
if (!root) return;
|
|
338
|
+
emitObjectDefinition(sd, opts.rootName, root, definitions, opts.isResourceRoot, opts);
|
|
339
|
+
}
|
|
340
|
+
function emitObjectDefinition(sd, name, node, definitions, isResourceRoot, opts) {
|
|
341
|
+
if (definitions.has(name)) return;
|
|
342
|
+
const properties = {};
|
|
343
|
+
const required = [];
|
|
344
|
+
const displayName = opts.resourceDisplayName ?? sd.type ?? sd.name;
|
|
345
|
+
if (isResourceRoot) {
|
|
346
|
+
properties.resourceType = {
|
|
347
|
+
description: `This is a ${displayName} resource`,
|
|
348
|
+
const: displayName
|
|
349
|
+
};
|
|
350
|
+
required.push("resourceType");
|
|
351
|
+
}
|
|
352
|
+
const definition = {
|
|
353
|
+
...node.element.definition ? { description: node.element.definition } : {},
|
|
354
|
+
properties,
|
|
355
|
+
additionalProperties: false
|
|
356
|
+
};
|
|
357
|
+
definitions.set(name, definition);
|
|
358
|
+
for (const [segment, child] of node.children) {
|
|
359
|
+
emitProperty(sd, name, segment, child, properties, required, definitions, opts);
|
|
360
|
+
}
|
|
361
|
+
if (required.length > 0) definition.required = required.sort();
|
|
362
|
+
}
|
|
363
|
+
function emitProperty(sd, parentName, segment, node, properties, required, definitions, opts) {
|
|
364
|
+
const el = node.element;
|
|
365
|
+
if (opts.profile && el.max === "0") return;
|
|
366
|
+
const isArray = el.max === "*" || Number(el.max) > 1;
|
|
367
|
+
const isChoice = segment.endsWith("[x]");
|
|
368
|
+
let description = el.definition ?? el.short;
|
|
369
|
+
const omitted = [];
|
|
370
|
+
if (opts.profile) {
|
|
371
|
+
if (el.mustSupport) omitted.push("must-support");
|
|
372
|
+
if (el.omittedConstraints) omitted.push(...el.omittedConstraints);
|
|
373
|
+
if (omitted.length > 0) {
|
|
374
|
+
const note = `Profile constraints not enforced here: ${omitted.join(", ")}.`;
|
|
375
|
+
description = description ? `${description} ${note}` : note;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const addProperty = (fieldName, schema, primitive) => {
|
|
379
|
+
const annotated = omitted.length > 0 && !("$ref" in schema) ? { ...schema, "x-fhir-constraints-omitted": omitted } : schema;
|
|
380
|
+
properties[fieldName] = wrapCardinality(annotated, isArray, description);
|
|
381
|
+
if (primitive) {
|
|
382
|
+
properties[`_${fieldName}`] = wrapCardinality(
|
|
383
|
+
{ $ref: "#/definitions/Element" },
|
|
384
|
+
isArray,
|
|
385
|
+
`Extensions for ${fieldName}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
if (isChoice) {
|
|
390
|
+
const base = segment.slice(0, -3);
|
|
391
|
+
for (const type of el.types ?? []) {
|
|
392
|
+
const fieldName = base + segmentToPascal(type.code);
|
|
393
|
+
const { schema, primitive } = schemaForTypeCode(type.code, el, opts);
|
|
394
|
+
addProperty(fieldName, schema, primitive);
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (el.contentReference) {
|
|
399
|
+
const target = contentReferenceName(el.contentReference, sd, opts.rootName);
|
|
400
|
+
addProperty(segment, { $ref: `#/definitions/${target}` }, false);
|
|
401
|
+
} else if (isBackbone(el) && node.children.size > 0) {
|
|
402
|
+
const childName = `${parentName}_${segmentToPascal(segment)}`;
|
|
403
|
+
emitObjectDefinition(sd, childName, node, definitions, false, opts);
|
|
404
|
+
addProperty(segment, { $ref: `#/definitions/${childName}` }, false);
|
|
405
|
+
} else {
|
|
406
|
+
const type = (el.types ?? [])[0];
|
|
407
|
+
if (!type) return;
|
|
408
|
+
const { schema, primitive } = schemaForTypeCode(type.code, el, opts);
|
|
409
|
+
addProperty(segment, schema, primitive);
|
|
410
|
+
}
|
|
411
|
+
if (el.min >= 1) required.push(segment);
|
|
412
|
+
}
|
|
413
|
+
function schemaForTypeCode(code, el, opts) {
|
|
414
|
+
const primitive = code.charAt(0) === code.charAt(0).toLowerCase();
|
|
415
|
+
const system = systemTypeSchema(code);
|
|
416
|
+
if (system) return { schema: system, primitive: false };
|
|
417
|
+
if (code === "Resource" || code === "DomainResource") {
|
|
418
|
+
return { schema: { $ref: "#/definitions/ResourceList" }, primitive: false };
|
|
419
|
+
}
|
|
420
|
+
if (opts.profile && el.fixed !== void 0) {
|
|
421
|
+
return { schema: { const: el.fixed }, primitive };
|
|
422
|
+
}
|
|
423
|
+
if (code === "code" && el.binding?.codes?.length) {
|
|
424
|
+
return { schema: { enum: el.binding.codes }, primitive: true };
|
|
425
|
+
}
|
|
426
|
+
return { schema: { $ref: `#/definitions/${code}` }, primitive };
|
|
427
|
+
}
|
|
428
|
+
function wrapCardinality(schema, isArray, description) {
|
|
429
|
+
const withDescription = (s) => description && !("$ref" in s) ? { description, ...s } : s;
|
|
430
|
+
if (isArray) {
|
|
431
|
+
return {
|
|
432
|
+
...description ? { description } : {},
|
|
433
|
+
type: "array",
|
|
434
|
+
items: schema
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if ("$ref" in schema && description) {
|
|
438
|
+
return schema;
|
|
439
|
+
}
|
|
440
|
+
return withDescription(schema);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/ig/profile.ts
|
|
444
|
+
function buildCoreValueSetFallback(fhirVersion) {
|
|
445
|
+
const map = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const sd of loadStructureDefinitions(fhirVersion)) {
|
|
447
|
+
for (const el of sd.elements) {
|
|
448
|
+
if (el.binding?.codes?.length && el.binding.valueSet) {
|
|
449
|
+
const key = el.binding.valueSet.split("|")[0];
|
|
450
|
+
if (!map.has(key)) map.set(key, el.binding.codes);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return (valueSetUrl) => map.get(valueSetUrl.split("|")[0]);
|
|
455
|
+
}
|
|
456
|
+
function sanitizeSchemaName(name) {
|
|
457
|
+
const cleaned = name.replace(/[^A-Za-z0-9_]/g, "");
|
|
458
|
+
return /^[A-Za-z]/.test(cleaned) ? cleaned : `Profile${cleaned}`;
|
|
459
|
+
}
|
|
460
|
+
function findProfile(context, requested) {
|
|
461
|
+
const needle = requested.toLowerCase();
|
|
462
|
+
const matches = context.profiles.filter(
|
|
463
|
+
(p) => p.url.toLowerCase() === needle || p.id?.toLowerCase() === needle || p.name.toLowerCase() === needle
|
|
464
|
+
);
|
|
465
|
+
if (matches.length === 1) return matches[0];
|
|
466
|
+
if (matches.length > 1) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Profile "${requested}" is ambiguous in ${context.name}; match by canonical URL. Candidates: ${matches.map((p) => p.url).join(", ")}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (context.profilesMissingSnapshot.some((p) => p.toLowerCase() === needle)) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`Profile "${requested}" in ${context.name} has no snapshot. Snapshot generation is out of scope; use a snapshot-bearing release of the IG.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const available = context.profiles.map((p) => p.id ?? p.name).sort();
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Profile "${requested}" not found in ${context.name}@${context.version}. Available profiles: ${available.join(", ") || "none"}`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
function applyProfile(context, requested, registry) {
|
|
482
|
+
const profile = findProfile(context, requested);
|
|
483
|
+
let schemaName = sanitizeSchemaName(profile.name);
|
|
484
|
+
if (registry.definitions.has(schemaName) && schemaName !== profile.type) {
|
|
485
|
+
schemaName = `${schemaName}Profile`;
|
|
486
|
+
}
|
|
487
|
+
emitStructureDefinitionSchemas(profile.definition, registry.definitions, {
|
|
488
|
+
rootName: schemaName,
|
|
489
|
+
isResourceRoot: true,
|
|
490
|
+
resourceDisplayName: profile.type,
|
|
491
|
+
profile: true
|
|
492
|
+
});
|
|
493
|
+
const schema = registry.definitions.get(schemaName);
|
|
494
|
+
if (schema) schema["x-fhir-profile"] = profile.url;
|
|
495
|
+
return { schemaName, resourceType: profile.type, url: profile.url };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/backends/schemaJson.ts
|
|
499
|
+
function buildRegistryFromSchemaJson(fhirVersion) {
|
|
500
|
+
const schema = loadFhirSchema(fhirVersion);
|
|
501
|
+
const definitions = new Map(Object.entries(schema.definitions));
|
|
502
|
+
let resourceNames;
|
|
503
|
+
if (schema.discriminator?.mapping) {
|
|
504
|
+
resourceNames = Object.keys(schema.discriminator.mapping);
|
|
505
|
+
} else {
|
|
506
|
+
resourceNames = [...definitions.entries()].filter(([, def]) => {
|
|
507
|
+
const props = def.properties;
|
|
508
|
+
return props?.resourceType !== void 0 && "const" in (props.resourceType ?? {});
|
|
509
|
+
}).map(([name]) => name);
|
|
510
|
+
}
|
|
511
|
+
return { fhirVersion, definitions, resourceNames };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/backends/structureDefinition.ts
|
|
515
|
+
function buildRegistryFromStructureDefinitions(fhirVersion) {
|
|
516
|
+
const sds = loadStructureDefinitions(fhirVersion);
|
|
517
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
518
|
+
const resourceNames = [];
|
|
519
|
+
const byName = new Map(sds.map((sd) => [sd.name, sd]));
|
|
520
|
+
for (const sd of sds) {
|
|
521
|
+
if (sd.kind === "primitive-type") {
|
|
522
|
+
definitions.set(sd.name, primitiveSchema(sd));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!definitions.has("xhtml")) {
|
|
526
|
+
definitions.set("xhtml", {
|
|
527
|
+
type: "string",
|
|
528
|
+
description: "XHTML narrative content"
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
for (const sd of sds) {
|
|
532
|
+
if (sd.kind === "primitive-type" || sd.abstract) continue;
|
|
533
|
+
emitStructureDefinitionSchemas(sd, definitions, {
|
|
534
|
+
rootName: sd.name,
|
|
535
|
+
isResourceRoot: sd.kind === "resource"
|
|
536
|
+
});
|
|
537
|
+
if (sd.kind === "resource") resourceNames.push(sd.name);
|
|
538
|
+
}
|
|
539
|
+
for (const abstractName of ["Element", "BackboneElement"]) {
|
|
540
|
+
const sd = byName.get(abstractName);
|
|
541
|
+
if (sd && !definitions.has(abstractName)) {
|
|
542
|
+
emitStructureDefinitionSchemas(sd, definitions, {
|
|
543
|
+
rootName: sd.name,
|
|
544
|
+
isResourceRoot: false
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
resourceNames.sort();
|
|
549
|
+
definitions.set("ResourceList", {
|
|
550
|
+
oneOf: resourceNames.map((name) => ({ $ref: `#/definitions/${name}` }))
|
|
551
|
+
});
|
|
552
|
+
return { fhirVersion, definitions, resourceNames };
|
|
553
|
+
}
|
|
554
|
+
function primitiveJsonType(name) {
|
|
555
|
+
switch (name) {
|
|
556
|
+
case "boolean":
|
|
557
|
+
return { type: "boolean" };
|
|
558
|
+
case "decimal":
|
|
559
|
+
return { type: "number" };
|
|
560
|
+
case "integer":
|
|
561
|
+
case "positiveInt":
|
|
562
|
+
case "unsignedInt":
|
|
563
|
+
return { type: "number" };
|
|
564
|
+
// integer64 (R5) is represented as a JSON string per the spec.
|
|
565
|
+
default:
|
|
566
|
+
return { type: "string" };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function primitiveSchema(sd) {
|
|
570
|
+
const root = sd.elements[0];
|
|
571
|
+
return {
|
|
572
|
+
...primitiveJsonType(sd.name),
|
|
573
|
+
...root?.definition ? { description: root.definition } : {}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/emit/schema.ts
|
|
578
|
+
var DEFINITIONS_REF = "#/definitions/";
|
|
579
|
+
var COMPONENTS_REF = "#/components/schemas/";
|
|
580
|
+
var NO_ENUMS_NOTE_MAX_CODES = 25;
|
|
581
|
+
function convertSchema(node, target, options = {}) {
|
|
582
|
+
return convertNode(node, target, options);
|
|
583
|
+
}
|
|
584
|
+
function convertNode(node, target, options) {
|
|
585
|
+
if (Array.isArray(node)) {
|
|
586
|
+
return node.map((item) => convertNode(item, target, options));
|
|
587
|
+
}
|
|
588
|
+
if (!node || typeof node !== "object") return node;
|
|
589
|
+
const out = {};
|
|
590
|
+
const source = node;
|
|
591
|
+
let strippedEnum;
|
|
592
|
+
for (const [key, value] of Object.entries(source)) {
|
|
593
|
+
switch (key) {
|
|
594
|
+
case "$schema":
|
|
595
|
+
case "$comment":
|
|
596
|
+
case "id":
|
|
597
|
+
case "$id":
|
|
598
|
+
break;
|
|
599
|
+
case "$ref":
|
|
600
|
+
out.$ref = typeof value === "string" && value.startsWith(DEFINITIONS_REF) ? COMPONENTS_REF + value.slice(DEFINITIONS_REF.length) : value;
|
|
601
|
+
break;
|
|
602
|
+
case "const":
|
|
603
|
+
if (target === "3.1.0") out.const = value;
|
|
604
|
+
else out.enum = [value];
|
|
605
|
+
break;
|
|
606
|
+
case "enum":
|
|
607
|
+
if (options.noEnums && Array.isArray(value)) strippedEnum = value;
|
|
608
|
+
else out.enum = value;
|
|
609
|
+
break;
|
|
610
|
+
case "pattern":
|
|
611
|
+
if (source.type === void 0 || source.type === "string") out.pattern = value;
|
|
612
|
+
break;
|
|
613
|
+
default:
|
|
614
|
+
out[key] = convertNode(value, target, options);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (strippedEnum) {
|
|
618
|
+
if (out.type === void 0 && out.$ref === void 0) out.type = "string";
|
|
619
|
+
const listed = strippedEnum.slice(0, NO_ENUMS_NOTE_MAX_CODES).join(" | ");
|
|
620
|
+
const ellipsis = strippedEnum.length > NO_ENUMS_NOTE_MAX_CODES ? " | ..." : "";
|
|
621
|
+
const note = `Codes (FHIR required binding, not enforced here): ${listed}${ellipsis}`;
|
|
622
|
+
out.description = out.description ? `${out.description} ${note}` : note;
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/ir/registry.ts
|
|
628
|
+
var REF_PREFIX = "#/definitions/";
|
|
629
|
+
function refName(ref3) {
|
|
630
|
+
return ref3.startsWith(REF_PREFIX) ? ref3.slice(REF_PREFIX.length) : void 0;
|
|
631
|
+
}
|
|
632
|
+
function collectRefs(node, out = /* @__PURE__ */ new Set()) {
|
|
633
|
+
if (Array.isArray(node)) {
|
|
634
|
+
for (const item of node) collectRefs(item, out);
|
|
635
|
+
} else if (node && typeof node === "object") {
|
|
636
|
+
for (const [key, value] of Object.entries(node)) {
|
|
637
|
+
if (key === "$ref" && typeof value === "string") {
|
|
638
|
+
const name = refName(value);
|
|
639
|
+
if (name) out.add(name);
|
|
640
|
+
} else {
|
|
641
|
+
collectRefs(value, out);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
function stubSchema(reason) {
|
|
648
|
+
return {
|
|
649
|
+
type: "object",
|
|
650
|
+
additionalProperties: true,
|
|
651
|
+
description: reason
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function narrowedResourceList(included) {
|
|
655
|
+
return {
|
|
656
|
+
oneOf: included.map((name) => ({ $ref: `${REF_PREFIX}${name}` })),
|
|
657
|
+
description: "A contained/bundled FHIR resource. Narrowed by fhir-openapi-translator to the resource types requested at generation time; servers may return other types."
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function extractClosure(registry, roots, trim = {}, resourceListNarrowing = roots) {
|
|
661
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
662
|
+
const stubbed = /* @__PURE__ */ new Set();
|
|
663
|
+
const maxDepth = trim.maxDepth;
|
|
664
|
+
let frontier = [...new Set(roots)];
|
|
665
|
+
let depth = 0;
|
|
666
|
+
while (frontier.length > 0) {
|
|
667
|
+
const next = [];
|
|
668
|
+
for (const name of frontier) {
|
|
669
|
+
if (schemas.has(name)) continue;
|
|
670
|
+
const original = registry.definitions.get(name);
|
|
671
|
+
if (!original) {
|
|
672
|
+
throw new Error(`Unknown FHIR definition: ${name}`);
|
|
673
|
+
}
|
|
674
|
+
let schema = original;
|
|
675
|
+
let followDeps = true;
|
|
676
|
+
if (trim.excludeNarrative && name === "Narrative") {
|
|
677
|
+
schema = stubSchema(
|
|
678
|
+
"Human-readable narrative. Excluded from this specification (--exclude-narrative)."
|
|
679
|
+
);
|
|
680
|
+
followDeps = false;
|
|
681
|
+
stubbed.add(name);
|
|
682
|
+
} else if (maxDepth !== void 0 && depth > maxDepth) {
|
|
683
|
+
schema = stubSchema(
|
|
684
|
+
`FHIR ${name}. Pruned from this specification by --max-depth ${maxDepth}.`
|
|
685
|
+
);
|
|
686
|
+
followDeps = false;
|
|
687
|
+
stubbed.add(name);
|
|
688
|
+
} else if (name === "ResourceList") {
|
|
689
|
+
schema = narrowedResourceList(
|
|
690
|
+
[...new Set(resourceListNarrowing)].filter((n) => registry.definitions.has(n))
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
schemas.set(name, schema);
|
|
694
|
+
if (followDeps) {
|
|
695
|
+
for (const dep of collectRefs(schema)) {
|
|
696
|
+
if (!schemas.has(dep)) next.push(dep);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
frontier = next;
|
|
701
|
+
depth++;
|
|
702
|
+
}
|
|
703
|
+
return { schemas, stubbed };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/operations.ts
|
|
707
|
+
var FHIR_JSON = "application/fhir+json";
|
|
708
|
+
var SCHEMAS = "#/components/schemas/";
|
|
709
|
+
function isPrimitive(type) {
|
|
710
|
+
return !!type && type.charAt(0) === type.charAt(0).toLowerCase();
|
|
711
|
+
}
|
|
712
|
+
function camelCode(code) {
|
|
713
|
+
return code.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
714
|
+
}
|
|
715
|
+
function ref(schema) {
|
|
716
|
+
return { $ref: `${SCHEMAS}${schema}` };
|
|
717
|
+
}
|
|
718
|
+
function fhirContent(schema) {
|
|
719
|
+
return { [FHIR_JSON]: { schema: ref(schema) } };
|
|
720
|
+
}
|
|
721
|
+
function errorResponses() {
|
|
722
|
+
return {
|
|
723
|
+
default: {
|
|
724
|
+
description: "Error, with an OperationOutcome describing the problem",
|
|
725
|
+
content: fhirContent("OperationOutcome")
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function buildOperationPaths(fhirVersion, resource, knownResources, options = {}) {
|
|
730
|
+
const schemaFor = options.schemaFor ?? ((r) => r);
|
|
731
|
+
const paths = {};
|
|
732
|
+
const extraSchemaRoots = /* @__PURE__ */ new Set();
|
|
733
|
+
const applicable = loadOperationDefinitions(fhirVersion).filter(
|
|
734
|
+
(op) => (op.type || op.instance) && (op.resource.includes(resource) || op.resource.includes("Resource")) && (!options.only || options.only.has(op.code) || options.only.has(op.url))
|
|
735
|
+
);
|
|
736
|
+
for (const op of applicable) {
|
|
737
|
+
const inParams = op.parameters.filter((p) => p.use === "in");
|
|
738
|
+
const useGet = inParams.every((p) => isPrimitive(p.type));
|
|
739
|
+
const responseSchema = schemaFor(responseSchemaName(op, knownResources));
|
|
740
|
+
extraSchemaRoots.add(responseSchema);
|
|
741
|
+
if (!useGet) extraSchemaRoots.add("Parameters");
|
|
742
|
+
const buildOperation = (level) => {
|
|
743
|
+
const operation = {
|
|
744
|
+
tags: [resource],
|
|
745
|
+
summary: `$${op.code} (${level.toLowerCase()} level)`,
|
|
746
|
+
...op.description ? { description: op.description } : {},
|
|
747
|
+
operationId: `${camelCode(op.code)}${resource}${level}`,
|
|
748
|
+
externalDocs: { url: op.url },
|
|
749
|
+
responses: {
|
|
750
|
+
"200": {
|
|
751
|
+
description: `Result of the $${op.code} operation`,
|
|
752
|
+
content: fhirContent(responseSchema)
|
|
753
|
+
},
|
|
754
|
+
...errorResponses()
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
if (useGet) {
|
|
758
|
+
if (inParams.length > 0) operation.parameters = inParams.map(queryParameter);
|
|
759
|
+
} else {
|
|
760
|
+
operation.requestBody = {
|
|
761
|
+
required: inParams.some((p) => p.min >= 1),
|
|
762
|
+
content: fhirContent("Parameters")
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
return operation;
|
|
766
|
+
};
|
|
767
|
+
const method = useGet ? "get" : "post";
|
|
768
|
+
if (op.type) {
|
|
769
|
+
paths[`/${resource}/$${op.code}`] = { [method]: buildOperation("Type") };
|
|
770
|
+
}
|
|
771
|
+
if (op.instance) {
|
|
772
|
+
paths[`/${resource}/{id}/$${op.code}`] = {
|
|
773
|
+
parameters: [
|
|
774
|
+
{
|
|
775
|
+
name: "id",
|
|
776
|
+
in: "path",
|
|
777
|
+
required: true,
|
|
778
|
+
description: `Logical id of the ${resource}`,
|
|
779
|
+
schema: { type: "string", pattern: "^[A-Za-z0-9\\-\\.]{1,64}$" }
|
|
780
|
+
}
|
|
781
|
+
],
|
|
782
|
+
[method]: buildOperation("Instance")
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return { paths, extraSchemaRoots: [...extraSchemaRoots].sort() };
|
|
787
|
+
}
|
|
788
|
+
function responseSchemaName(op, knownResources) {
|
|
789
|
+
const outParams = op.parameters.filter((p) => p.use === "out");
|
|
790
|
+
if (outParams.length === 1) {
|
|
791
|
+
const only = outParams[0];
|
|
792
|
+
if (only.name === "return" && only.type && knownResources.has(only.type)) {
|
|
793
|
+
return only.type;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return "Parameters";
|
|
797
|
+
}
|
|
798
|
+
function queryParameter(param) {
|
|
799
|
+
const isArray = param.max === "*" || Number(param.max) > 1;
|
|
800
|
+
const base = { type: "string" };
|
|
801
|
+
return {
|
|
802
|
+
name: param.name,
|
|
803
|
+
in: "query",
|
|
804
|
+
required: param.min >= 1,
|
|
805
|
+
...param.documentation ? { description: param.documentation } : {},
|
|
806
|
+
// FHIR primitives serialize as strings in URLs; the FHIR type is kept
|
|
807
|
+
// as an extension, consistent with search parameters.
|
|
808
|
+
schema: isArray ? { type: "array", items: base } : base,
|
|
809
|
+
...isArray ? { explode: true } : {},
|
|
810
|
+
"x-fhir-type": param.type
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/paths.ts
|
|
815
|
+
var FHIR_JSON2 = "application/fhir+json";
|
|
816
|
+
var SCHEMAS2 = "#/components/schemas/";
|
|
817
|
+
var PARAMETERS = "#/components/parameters/";
|
|
818
|
+
function ref2(schema) {
|
|
819
|
+
return { $ref: `${SCHEMAS2}${schema}` };
|
|
820
|
+
}
|
|
821
|
+
function fhirContent2(schema) {
|
|
822
|
+
return { [FHIR_JSON2]: { schema: ref2(schema) } };
|
|
823
|
+
}
|
|
824
|
+
function errorResponses2() {
|
|
825
|
+
return {
|
|
826
|
+
default: {
|
|
827
|
+
description: "Error, with an OperationOutcome describing the problem",
|
|
828
|
+
content: fhirContent2("OperationOutcome")
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function idParameter(name, description) {
|
|
833
|
+
return {
|
|
834
|
+
name,
|
|
835
|
+
in: "path",
|
|
836
|
+
required: true,
|
|
837
|
+
description,
|
|
838
|
+
schema: { type: "string", pattern: "^[A-Za-z0-9\\-\\.]{1,64}$" }
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
var COMMON_SEARCH_PARAMETERS = {
|
|
842
|
+
_id: { description: "Logical id of this artifact", schema: { type: "string" } },
|
|
843
|
+
_lastUpdated: {
|
|
844
|
+
description: "When the resource version last changed (supports date prefixes, e.g. ge2021-01-01)",
|
|
845
|
+
schema: { type: "string" }
|
|
846
|
+
},
|
|
847
|
+
_tag: { description: "Tags applied to this resource", schema: { type: "string" } },
|
|
848
|
+
_profile: { description: "Profiles this resource claims to conform to", schema: { type: "string" } },
|
|
849
|
+
_security: { description: "Security labels applied to this resource", schema: { type: "string" } },
|
|
850
|
+
_text: { description: "Search on the narrative of the resource", schema: { type: "string" } },
|
|
851
|
+
_content: { description: "Search on the entire content of the resource", schema: { type: "string" } },
|
|
852
|
+
_sort: { description: "Sort order of the results (comma-separated parameter names, '-' prefix for descending)", schema: { type: "string" } },
|
|
853
|
+
_count: { description: "Maximum number of results per page", schema: { type: "integer", minimum: 0 } },
|
|
854
|
+
_include: { description: "Include referenced resources in the results", schema: { type: "string" } },
|
|
855
|
+
_revinclude: { description: "Include resources that reference the matches in the results", schema: { type: "string" } },
|
|
856
|
+
_summary: {
|
|
857
|
+
description: "Return only a portion of each resource",
|
|
858
|
+
schema: { type: "string", enum: ["true", "text", "data", "count", "false"] }
|
|
859
|
+
},
|
|
860
|
+
_total: {
|
|
861
|
+
description: "Requested precision of the Bundle.total",
|
|
862
|
+
schema: { type: "string", enum: ["none", "estimate", "accurate"] }
|
|
863
|
+
},
|
|
864
|
+
_elements: { description: "Restrict returned elements (comma-separated element names)", schema: { type: "string" } }
|
|
865
|
+
};
|
|
866
|
+
function commonSearchParameterComponents() {
|
|
867
|
+
const out = {};
|
|
868
|
+
for (const [name, { description, schema }] of Object.entries(COMMON_SEARCH_PARAMETERS)) {
|
|
869
|
+
out[name] = { name, in: "query", required: false, description, schema };
|
|
870
|
+
}
|
|
871
|
+
return out;
|
|
872
|
+
}
|
|
873
|
+
function resourceSearchParameters(fhirVersion, resource, only) {
|
|
874
|
+
return loadSearchParameters(fhirVersion).filter((sp) => sp.base.includes(resource) && (!only || only.has(sp.code))).sort((a, b) => a.code.localeCompare(b.code)).map((sp) => ({
|
|
875
|
+
name: sp.code,
|
|
876
|
+
in: "query",
|
|
877
|
+
required: false,
|
|
878
|
+
description: sp.description,
|
|
879
|
+
// FHIR search values carry prefixes and modifiers, so all are strings.
|
|
880
|
+
schema: { type: "string" },
|
|
881
|
+
"x-fhir-search-type": sp.type
|
|
882
|
+
}));
|
|
883
|
+
}
|
|
884
|
+
var historyParameters = () => [
|
|
885
|
+
{ $ref: `${PARAMETERS}_count` },
|
|
886
|
+
{
|
|
887
|
+
name: "_since",
|
|
888
|
+
in: "query",
|
|
889
|
+
required: false,
|
|
890
|
+
description: "Only include versions created at or after this instant",
|
|
891
|
+
schema: { type: "string", format: "date-time" }
|
|
892
|
+
}
|
|
893
|
+
];
|
|
894
|
+
function buildResourcePaths(fhirVersion, resource, schemaName = resource, options = {}) {
|
|
895
|
+
const want = (interaction) => !options.interactions || options.interactions.has(interaction);
|
|
896
|
+
const body = () => fhirContent2(schemaName);
|
|
897
|
+
const idParam = () => idParameter("id", `Logical id of the ${resource}`);
|
|
898
|
+
const paths = {};
|
|
899
|
+
const typeItem = {};
|
|
900
|
+
if (want("search-type")) {
|
|
901
|
+
typeItem.get = {
|
|
902
|
+
tags: [resource],
|
|
903
|
+
summary: `Search for ${resource} resources`,
|
|
904
|
+
operationId: `search${resource}`,
|
|
905
|
+
parameters: [
|
|
906
|
+
...Object.keys(COMMON_SEARCH_PARAMETERS).map((name) => ({ $ref: `${PARAMETERS}${name}` })),
|
|
907
|
+
...resourceSearchParameters(fhirVersion, resource, options.searchParamCodes)
|
|
908
|
+
],
|
|
909
|
+
responses: {
|
|
910
|
+
"200": {
|
|
911
|
+
description: `Bundle of matching ${resource} resources`,
|
|
912
|
+
content: fhirContent2("Bundle")
|
|
913
|
+
},
|
|
914
|
+
...errorResponses2()
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (want("create")) {
|
|
919
|
+
typeItem.post = {
|
|
920
|
+
tags: [resource],
|
|
921
|
+
summary: `Create a ${resource} resource`,
|
|
922
|
+
operationId: `create${resource}`,
|
|
923
|
+
requestBody: { required: true, content: body() },
|
|
924
|
+
responses: {
|
|
925
|
+
"201": { description: `${resource} created`, content: body() },
|
|
926
|
+
...errorResponses2()
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
if (Object.keys(typeItem).length > 0) paths[`/${resource}`] = typeItem;
|
|
931
|
+
const instanceItem = {};
|
|
932
|
+
if (want("read")) {
|
|
933
|
+
instanceItem.get = {
|
|
934
|
+
tags: [resource],
|
|
935
|
+
summary: `Read a ${resource} resource by id`,
|
|
936
|
+
operationId: `read${resource}`,
|
|
937
|
+
responses: {
|
|
938
|
+
"200": { description: `The ${resource} resource`, content: body() },
|
|
939
|
+
...errorResponses2()
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (want("update")) {
|
|
944
|
+
instanceItem.put = {
|
|
945
|
+
tags: [resource],
|
|
946
|
+
summary: `Update (or create) a ${resource} resource by id`,
|
|
947
|
+
operationId: `update${resource}`,
|
|
948
|
+
parameters: [
|
|
949
|
+
{
|
|
950
|
+
name: "If-Match",
|
|
951
|
+
in: "header",
|
|
952
|
+
required: false,
|
|
953
|
+
description: "Version-aware update: weak ETag of the version being updated",
|
|
954
|
+
schema: { type: "string" }
|
|
955
|
+
}
|
|
956
|
+
],
|
|
957
|
+
requestBody: { required: true, content: body() },
|
|
958
|
+
responses: {
|
|
959
|
+
"200": { description: `${resource} updated`, content: body() },
|
|
960
|
+
"201": { description: `${resource} created`, content: body() },
|
|
961
|
+
...errorResponses2()
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (want("patch")) {
|
|
966
|
+
instanceItem.patch = {
|
|
967
|
+
tags: [resource],
|
|
968
|
+
summary: `Patch a ${resource} resource by id`,
|
|
969
|
+
operationId: `patch${resource}`,
|
|
970
|
+
requestBody: {
|
|
971
|
+
required: true,
|
|
972
|
+
content: {
|
|
973
|
+
"application/json-patch+json": {
|
|
974
|
+
schema: {
|
|
975
|
+
type: "array",
|
|
976
|
+
items: { type: "object", additionalProperties: true },
|
|
977
|
+
description: "JSON Patch operations (RFC 6902)"
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
responses: {
|
|
983
|
+
"200": { description: `${resource} patched`, content: body() },
|
|
984
|
+
...errorResponses2()
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (want("delete")) {
|
|
989
|
+
instanceItem.delete = {
|
|
990
|
+
tags: [resource],
|
|
991
|
+
summary: `Delete a ${resource} resource by id`,
|
|
992
|
+
operationId: `delete${resource}`,
|
|
993
|
+
responses: { "204": { description: `${resource} deleted` }, ...errorResponses2() }
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
if (Object.keys(instanceItem).length > 0) {
|
|
997
|
+
paths[`/${resource}/{id}`] = { parameters: [idParam()], ...instanceItem };
|
|
998
|
+
}
|
|
999
|
+
if (want("history-instance")) {
|
|
1000
|
+
paths[`/${resource}/{id}/_history`] = {
|
|
1001
|
+
parameters: [idParam()],
|
|
1002
|
+
get: {
|
|
1003
|
+
tags: [resource],
|
|
1004
|
+
summary: `History of a ${resource} instance`,
|
|
1005
|
+
operationId: `history${resource}Instance`,
|
|
1006
|
+
parameters: historyParameters(),
|
|
1007
|
+
responses: {
|
|
1008
|
+
"200": { description: "History bundle", content: fhirContent2("Bundle") },
|
|
1009
|
+
...errorResponses2()
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
if (want("vread")) {
|
|
1015
|
+
paths[`/${resource}/{id}/_history/{vid}`] = {
|
|
1016
|
+
parameters: [idParam(), idParameter("vid", "Version id of the resource")],
|
|
1017
|
+
get: {
|
|
1018
|
+
tags: [resource],
|
|
1019
|
+
summary: `Read a specific version of a ${resource} resource`,
|
|
1020
|
+
operationId: `vread${resource}`,
|
|
1021
|
+
responses: {
|
|
1022
|
+
"200": { description: `The ${resource} resource version`, content: body() },
|
|
1023
|
+
...errorResponses2()
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (want("history-type")) {
|
|
1029
|
+
paths[`/${resource}/_history`] = {
|
|
1030
|
+
get: {
|
|
1031
|
+
tags: [resource],
|
|
1032
|
+
summary: `History across all ${resource} resources`,
|
|
1033
|
+
operationId: `history${resource}Type`,
|
|
1034
|
+
parameters: historyParameters(),
|
|
1035
|
+
responses: {
|
|
1036
|
+
"200": { description: "History bundle", content: fhirContent2("Bundle") },
|
|
1037
|
+
...errorResponses2()
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return paths;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/generate.ts
|
|
1046
|
+
function buildRegistry(fhirVersion, source) {
|
|
1047
|
+
return source === "structure-def" ? buildRegistryFromStructureDefinitions(fhirVersion) : buildRegistryFromSchemaJson(fhirVersion);
|
|
1048
|
+
}
|
|
1049
|
+
function resolveResourceName(registry, requested) {
|
|
1050
|
+
const match = registry.resourceNames.find(
|
|
1051
|
+
(name) => name.toLowerCase() === requested.toLowerCase()
|
|
1052
|
+
);
|
|
1053
|
+
if (!match) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`Unknown FHIR ${registry.fhirVersion.toUpperCase()} resource: "${requested}". Run "fhir-oas list --fhir-version ${registry.fhirVersion}" to see available resources.`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
return match;
|
|
1059
|
+
}
|
|
1060
|
+
function listResources(fhirVersion, source = "schema-json") {
|
|
1061
|
+
assertFhirVersion(fhirVersion);
|
|
1062
|
+
return [...buildRegistry(fhirVersion, source).resourceNames].sort();
|
|
1063
|
+
}
|
|
1064
|
+
function assertFhirVersion(fhirVersion) {
|
|
1065
|
+
if (!FHIR_VERSIONS.includes(fhirVersion)) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Unsupported FHIR version "${fhirVersion}". Supported: ${FHIR_VERSIONS.join(", ")}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function assertCapabilityFhirVersion(capability, fhirVersion) {
|
|
1072
|
+
const declared = capability.fhirVersion;
|
|
1073
|
+
if (!declared) return;
|
|
1074
|
+
const expectedMajor = FHIR_VERSION_NUMBERS[fhirVersion].split(".").slice(0, 2).join(".");
|
|
1075
|
+
const declaredPrefix = declared.split(".").slice(0, 2).join(".");
|
|
1076
|
+
if (declaredPrefix && expectedMajor && declaredPrefix !== expectedMajor) {
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`CapabilityStatement targets FHIR ${declared}, but generation is for ${fhirVersion.toUpperCase()} (${FHIR_VERSION_NUMBERS[fhirVersion]}). Set --fhir-version to match the server.`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function resolveIg(options) {
|
|
1083
|
+
const ig = options.ig;
|
|
1084
|
+
if (!ig) throw new Error("--profile requires --ig: point at the IG package.");
|
|
1085
|
+
if (typeof ig === "string") {
|
|
1086
|
+
return loadIgSync(ig, { coreValueSetFallback: buildCoreValueSetFallback(options.fhirVersion) });
|
|
1087
|
+
}
|
|
1088
|
+
return ig;
|
|
1089
|
+
}
|
|
1090
|
+
function generateOpenApi(options) {
|
|
1091
|
+
assertFhirVersion(options.fhirVersion);
|
|
1092
|
+
const openApiVersion = options.openApiVersion ?? "3.0.3";
|
|
1093
|
+
if (openApiVersion !== "3.0.3" && openApiVersion !== "3.1.0") {
|
|
1094
|
+
throw new Error(`Unsupported OpenAPI version "${openApiVersion}". Supported: 3.0.3, 3.1.0`);
|
|
1095
|
+
}
|
|
1096
|
+
const registry = buildRegistry(options.fhirVersion, options.source ?? "schema-json");
|
|
1097
|
+
const capability = options.capability;
|
|
1098
|
+
const capabilityByResource = /* @__PURE__ */ new Map();
|
|
1099
|
+
if (capability) {
|
|
1100
|
+
assertCapabilityFhirVersion(capability, options.fhirVersion);
|
|
1101
|
+
for (const cap of capability.resources) {
|
|
1102
|
+
const match = registry.resourceNames.find(
|
|
1103
|
+
(name) => name.toLowerCase() === cap.type.toLowerCase()
|
|
1104
|
+
);
|
|
1105
|
+
if (match) capabilityByResource.set(match, cap);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const requested = (options.resources ?? []).map((r) => resolveResourceName(registry, r));
|
|
1109
|
+
if (requested.length === 0 && capabilityByResource.size === 0) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
capability ? "The CapabilityStatement declares no resources this FHIR version supports." : "At least one FHIR resource name (or a --capability statement) is required"
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
const schemaByResource = /* @__PURE__ */ new Map();
|
|
1115
|
+
const resourceSet = new Set(
|
|
1116
|
+
capability ? requested.length > 0 ? requested.filter((r) => capabilityByResource.has(r)) : [...capabilityByResource.keys()] : requested
|
|
1117
|
+
);
|
|
1118
|
+
if (options.profiles?.length) {
|
|
1119
|
+
const ig = resolveIg(options);
|
|
1120
|
+
if (ig.fhirVersion !== options.fhirVersion) {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`IG package "${ig.name}" targets FHIR ${ig.fhirVersion.toUpperCase()}, but generation is for ${options.fhirVersion.toUpperCase()}. Use --fhir-version ${ig.fhirVersion}.`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
for (const profileId of options.profiles) {
|
|
1126
|
+
const applied = applyProfile(ig, profileId, registry);
|
|
1127
|
+
const base = resolveResourceName(registry, applied.resourceType);
|
|
1128
|
+
if (schemaByResource.has(base)) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`Multiple profiles target ${base}; generate one profiled resource per run.`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
schemaByResource.set(base, applied.schemaName);
|
|
1134
|
+
resourceSet.add(base);
|
|
1135
|
+
}
|
|
1136
|
+
} else if (options.ig) {
|
|
1137
|
+
throw new Error("--ig requires --profile: name the profile(s) to apply.");
|
|
1138
|
+
}
|
|
1139
|
+
const resources = [...resourceSet];
|
|
1140
|
+
const schemaFor = (resource) => schemaByResource.get(resource) ?? resource;
|
|
1141
|
+
const operationPaths = {};
|
|
1142
|
+
const operationRoots = [];
|
|
1143
|
+
if (capability || options.operations) {
|
|
1144
|
+
const knownResources = new Set(registry.resourceNames);
|
|
1145
|
+
for (const resource of resources) {
|
|
1146
|
+
const cap = capabilityByResource.get(resource);
|
|
1147
|
+
if (capability && (!cap || cap.operations.size === 0)) continue;
|
|
1148
|
+
const result = buildOperationPaths(options.fhirVersion, resource, knownResources, {
|
|
1149
|
+
schemaFor,
|
|
1150
|
+
only: cap?.operations
|
|
1151
|
+
});
|
|
1152
|
+
Object.assign(operationPaths, result.paths);
|
|
1153
|
+
operationRoots.push(...result.extraSchemaRoots);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const roots = [
|
|
1157
|
+
.../* @__PURE__ */ new Set([...resources.map(schemaFor), "Bundle", "OperationOutcome", ...operationRoots])
|
|
1158
|
+
];
|
|
1159
|
+
const { schemas } = extractClosure(registry, roots, options.trim ?? {}, roots);
|
|
1160
|
+
const componentSchemas = {};
|
|
1161
|
+
const convertOptions = { noEnums: options.trim?.noEnums };
|
|
1162
|
+
for (const name of [...schemas.keys()].sort()) {
|
|
1163
|
+
componentSchemas[name] = convertSchema(schemas.get(name), openApiVersion, convertOptions);
|
|
1164
|
+
}
|
|
1165
|
+
const paths = {};
|
|
1166
|
+
for (const resource of resources) {
|
|
1167
|
+
const cap = capabilityByResource.get(resource);
|
|
1168
|
+
Object.assign(
|
|
1169
|
+
paths,
|
|
1170
|
+
buildResourcePaths(options.fhirVersion, resource, schemaFor(resource), {
|
|
1171
|
+
interactions: cap?.interactions,
|
|
1172
|
+
searchParamCodes: cap?.searchParamCodes
|
|
1173
|
+
})
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
Object.assign(paths, operationPaths);
|
|
1177
|
+
const fhirNumber = FHIR_VERSION_NUMBERS[options.fhirVersion];
|
|
1178
|
+
const document = {
|
|
1179
|
+
openapi: openApiVersion,
|
|
1180
|
+
info: {
|
|
1181
|
+
title: options.title ?? `FHIR ${options.fhirVersion.toUpperCase()} REST API: ${resources.join(", ")}`,
|
|
1182
|
+
description: `OpenAPI definition of the FHIR ${options.fhirVersion.toUpperCase()} (${fhirNumber}) RESTful interactions and JSON schemas for: ${resources.join(", ")}. Generated by fhir-openapi-translator from the official HL7 FHIR definitions. Schema validity does not imply full FHIR conformance: profiles, terminology bindings, and FHIRPath invariants are not represented.`,
|
|
1183
|
+
version: fhirNumber
|
|
1184
|
+
},
|
|
1185
|
+
...options.baseUrl ? { servers: [{ url: options.baseUrl }] } : {},
|
|
1186
|
+
tags: resources.map((resource) => ({
|
|
1187
|
+
name: resource,
|
|
1188
|
+
description: `Operations on the ${resource} resource`
|
|
1189
|
+
})),
|
|
1190
|
+
paths,
|
|
1191
|
+
components: {
|
|
1192
|
+
parameters: commonSearchParameterComponents(),
|
|
1193
|
+
schemas: componentSchemas
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
return document;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/capability.ts
|
|
1200
|
+
import fs3 from "fs";
|
|
1201
|
+
var KNOWN_INTERACTIONS = /* @__PURE__ */ new Set([
|
|
1202
|
+
"read",
|
|
1203
|
+
"vread",
|
|
1204
|
+
"update",
|
|
1205
|
+
"patch",
|
|
1206
|
+
"delete",
|
|
1207
|
+
"history-instance",
|
|
1208
|
+
"history-type",
|
|
1209
|
+
"create",
|
|
1210
|
+
"search-type"
|
|
1211
|
+
]);
|
|
1212
|
+
async function loadCapabilityStatement(input, options = {}) {
|
|
1213
|
+
let json;
|
|
1214
|
+
if (/^https?:\/\//i.test(input)) {
|
|
1215
|
+
const url = /\/metadata\/?$/.test(input) ? input : `${input.replace(/\/$/, "")}/metadata`;
|
|
1216
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
1217
|
+
let response;
|
|
1218
|
+
try {
|
|
1219
|
+
response = await doFetch(url, { headers: { Accept: "application/fhir+json" } });
|
|
1220
|
+
} catch (cause) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`Failed to fetch CapabilityStatement from ${url}: ${cause.message}. Save it locally (curl -H 'Accept: application/fhir+json' ${url} > metadata.json) and pass the file instead.`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
if (!response.ok) {
|
|
1226
|
+
throw new Error(`CapabilityStatement request to ${url} returned ${response.status}.`);
|
|
1227
|
+
}
|
|
1228
|
+
json = await response.json();
|
|
1229
|
+
} else {
|
|
1230
|
+
if (!fs3.existsSync(input)) throw new Error(`No such CapabilityStatement file: ${input}`);
|
|
1231
|
+
json = JSON.parse(fs3.readFileSync(input, "utf8"));
|
|
1232
|
+
}
|
|
1233
|
+
return parseCapabilityStatement(json);
|
|
1234
|
+
}
|
|
1235
|
+
function parseCapabilityStatement(json) {
|
|
1236
|
+
const statement = json;
|
|
1237
|
+
if (statement?.resourceType !== "CapabilityStatement") {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`Expected a CapabilityStatement resource, got "${statement?.resourceType ?? "unknown"}".`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1243
|
+
for (const rest of statement.rest ?? []) {
|
|
1244
|
+
if (rest.mode && rest.mode !== "server") continue;
|
|
1245
|
+
for (const raw of rest.resource ?? []) {
|
|
1246
|
+
if (!raw.type) continue;
|
|
1247
|
+
let cap = byType.get(raw.type);
|
|
1248
|
+
if (!cap) {
|
|
1249
|
+
cap = {
|
|
1250
|
+
type: raw.type,
|
|
1251
|
+
interactions: /* @__PURE__ */ new Set(),
|
|
1252
|
+
searchParamCodes: /* @__PURE__ */ new Set(),
|
|
1253
|
+
operations: /* @__PURE__ */ new Set()
|
|
1254
|
+
};
|
|
1255
|
+
byType.set(raw.type, cap);
|
|
1256
|
+
}
|
|
1257
|
+
for (const i of raw.interaction ?? []) {
|
|
1258
|
+
if (i.code && KNOWN_INTERACTIONS.has(i.code)) cap.interactions.add(i.code);
|
|
1259
|
+
}
|
|
1260
|
+
for (const sp of raw.searchParam ?? []) {
|
|
1261
|
+
if (sp.name) cap.searchParamCodes.add(sp.name);
|
|
1262
|
+
}
|
|
1263
|
+
for (const op of raw.operation ?? []) {
|
|
1264
|
+
if (op.name) cap.operations.add(op.name);
|
|
1265
|
+
if (op.definition) cap.operations.add(op.definition);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (byType.size === 0) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
"CapabilityStatement declares no server resources (rest[].resource). Nothing to generate."
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
fhirVersion: statement.fhirVersion,
|
|
1276
|
+
resources: [...byType.values()].sort((a, b) => a.type.localeCompare(b.type))
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/merge.ts
|
|
1281
|
+
import { Document, parseDocument, isMap } from "yaml";
|
|
1282
|
+
var MergeConflictError = class extends Error {
|
|
1283
|
+
constructor(conflicts) {
|
|
1284
|
+
super(
|
|
1285
|
+
"Refusing to overwrite existing, differing entries (use --force to overwrite):\n" + conflicts.map((c) => ` - ${c.location}`).join("\n")
|
|
1286
|
+
);
|
|
1287
|
+
this.conflicts = conflicts;
|
|
1288
|
+
this.name = "MergeConflictError";
|
|
1289
|
+
}
|
|
1290
|
+
conflicts;
|
|
1291
|
+
};
|
|
1292
|
+
var MERGED_SECTIONS = [
|
|
1293
|
+
["paths", ""],
|
|
1294
|
+
["components", "schemas"],
|
|
1295
|
+
["components", "parameters"]
|
|
1296
|
+
];
|
|
1297
|
+
function deepEqual(a, b) {
|
|
1298
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1299
|
+
}
|
|
1300
|
+
function unionResourceList(existing, generated) {
|
|
1301
|
+
const refsOf = (node) => {
|
|
1302
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return void 0;
|
|
1303
|
+
const oneOf = node.oneOf;
|
|
1304
|
+
if (!Array.isArray(oneOf)) return void 0;
|
|
1305
|
+
const refs = [];
|
|
1306
|
+
for (const item of oneOf) {
|
|
1307
|
+
if (!item || typeof item !== "object" || Object.keys(item).length !== 1) return void 0;
|
|
1308
|
+
const ref3 = item.$ref;
|
|
1309
|
+
if (typeof ref3 !== "string") return void 0;
|
|
1310
|
+
refs.push(ref3);
|
|
1311
|
+
}
|
|
1312
|
+
return refs;
|
|
1313
|
+
};
|
|
1314
|
+
const existingRefs = refsOf(existing);
|
|
1315
|
+
const generatedRefs = refsOf(generated);
|
|
1316
|
+
if (!existingRefs || !generatedRefs) return void 0;
|
|
1317
|
+
return {
|
|
1318
|
+
...generated,
|
|
1319
|
+
oneOf: [.../* @__PURE__ */ new Set([...existingRefs, ...generatedRefs])].map(($ref) => ({ $ref }))
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function computeMergePlan(generated, existingText, options) {
|
|
1323
|
+
const doc = parseDocument(existingText);
|
|
1324
|
+
if (doc.errors.length > 0) {
|
|
1325
|
+
throw new Error(`Cannot parse existing YAML: ${doc.errors[0]?.message}`);
|
|
1326
|
+
}
|
|
1327
|
+
if (doc.contents !== null && !isMap(doc.contents)) {
|
|
1328
|
+
throw new Error("Existing file is not a YAML mapping; refusing to merge");
|
|
1329
|
+
}
|
|
1330
|
+
const existingVersion = doc.getIn(["openapi"]);
|
|
1331
|
+
const generatedVersion = generated.openapi;
|
|
1332
|
+
if (existingVersion !== void 0 && String(existingVersion) !== String(generatedVersion)) {
|
|
1333
|
+
throw new Error(
|
|
1334
|
+
`OpenAPI version mismatch: existing file declares ${String(existingVersion)}, generating ${String(generatedVersion)}. Regenerate with a matching --openapi-version.`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const conflicts = [];
|
|
1338
|
+
const additions = [];
|
|
1339
|
+
for (const key of ["openapi", "info", "servers"]) {
|
|
1340
|
+
if (generated[key] !== void 0 && doc.getIn([key]) === void 0) {
|
|
1341
|
+
additions.push({ path: [key], value: generated[key] });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const generatedTags = generated.tags ?? [];
|
|
1345
|
+
if (generatedTags.length > 0) {
|
|
1346
|
+
const existingTags = doc.toJS()?.tags;
|
|
1347
|
+
if (existingTags === void 0) {
|
|
1348
|
+
additions.push({ path: ["tags"], value: generatedTags });
|
|
1349
|
+
} else {
|
|
1350
|
+
const existingNames = new Set(existingTags.map((t) => t?.name));
|
|
1351
|
+
let index = existingTags.length;
|
|
1352
|
+
for (const tag of generatedTags) {
|
|
1353
|
+
if (!existingNames.has(tag.name)) {
|
|
1354
|
+
additions.push({ path: ["tags", index++], value: tag });
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
for (const [section, subsection] of MERGED_SECTIONS) {
|
|
1360
|
+
const generatedSection = subsection ? generated[section]?.[subsection] : generated[section];
|
|
1361
|
+
if (!generatedSection) continue;
|
|
1362
|
+
const basePath = subsection ? [section, subsection] : [section];
|
|
1363
|
+
for (const [key, value] of Object.entries(generatedSection)) {
|
|
1364
|
+
const existing = doc.getIn([...basePath, key], true);
|
|
1365
|
+
if (existing === void 0) {
|
|
1366
|
+
additions.push({ path: [...basePath, key], value });
|
|
1367
|
+
} else {
|
|
1368
|
+
const existingJs = typeof existing?.toJS === "function" ? existing.toJS(doc) : existing;
|
|
1369
|
+
if (!deepEqual(existingJs, value)) {
|
|
1370
|
+
if (section === "components" && subsection === "schemas" && key === "ResourceList") {
|
|
1371
|
+
const union = unionResourceList(existingJs, value);
|
|
1372
|
+
if (union !== void 0) {
|
|
1373
|
+
if (!deepEqual(existingJs, union)) {
|
|
1374
|
+
additions.push({ path: [...basePath, key], value: union });
|
|
1375
|
+
}
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
conflicts.push({ location: [...basePath, key].join(".") });
|
|
1380
|
+
if (options.force) additions.push({ path: [...basePath, key], value });
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return { doc, conflicts, additions };
|
|
1386
|
+
}
|
|
1387
|
+
function mergeIntoYaml(generated, existingText, options = {}) {
|
|
1388
|
+
const { doc, conflicts, additions } = computeMergePlan(generated, existingText, options);
|
|
1389
|
+
if (doc.contents === null) {
|
|
1390
|
+
return stringifyDocument(generated);
|
|
1391
|
+
}
|
|
1392
|
+
if (conflicts.length > 0 && !options.force) {
|
|
1393
|
+
throw new MergeConflictError(conflicts);
|
|
1394
|
+
}
|
|
1395
|
+
for (const { path: path3, value } of additions) {
|
|
1396
|
+
doc.setIn(path3, doc.createNode(value));
|
|
1397
|
+
}
|
|
1398
|
+
return doc.toString({ lineWidth: 0 });
|
|
1399
|
+
}
|
|
1400
|
+
function diffAgainstYaml(generated, existingText) {
|
|
1401
|
+
const { doc, conflicts, additions } = computeMergePlan(generated, existingText, {});
|
|
1402
|
+
if (doc.contents === null) {
|
|
1403
|
+
return { missing: ["(entire document: file is empty)"], changed: [], inSync: false };
|
|
1404
|
+
}
|
|
1405
|
+
const missing = additions.map((a) => a.path.join("."));
|
|
1406
|
+
const changed = conflicts.map((c) => c.location);
|
|
1407
|
+
return { missing, changed, inSync: missing.length === 0 && changed.length === 0 };
|
|
1408
|
+
}
|
|
1409
|
+
function stringifyDocument(generated) {
|
|
1410
|
+
const doc = new Document(generated);
|
|
1411
|
+
return doc.toString({ lineWidth: 0 });
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export {
|
|
1415
|
+
FHIR_VERSIONS,
|
|
1416
|
+
FHIR_VERSION_NUMBERS,
|
|
1417
|
+
loadIg,
|
|
1418
|
+
loadIgSync,
|
|
1419
|
+
buildCoreValueSetFallback,
|
|
1420
|
+
listResources,
|
|
1421
|
+
generateOpenApi,
|
|
1422
|
+
loadCapabilityStatement,
|
|
1423
|
+
parseCapabilityStatement,
|
|
1424
|
+
MergeConflictError,
|
|
1425
|
+
mergeIntoYaml,
|
|
1426
|
+
diffAgainstYaml,
|
|
1427
|
+
stringifyDocument
|
|
1428
|
+
};
|
|
1429
|
+
//# sourceMappingURL=chunk-G3DCRADV.js.map
|