fhir-openapi-translator 0.1.0 → 0.2.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/CHANGELOG.md +106 -0
- package/README.md +16 -7
- 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 → chunk-RRAIF3A2.js} +232 -12
- package/dist/chunk-RRAIF3A2.js.map +1 -0
- package/dist/cli.js +31 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +1 -1
- package/docs/REFERENCE.md +166 -28
- package/package.json +5 -3
- package/dist/chunk-G3DCRADV.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-09-17
|
|
8
|
+
|
|
9
|
+
Search parameters gain derived metadata and a way to emit fewer of them.
|
|
10
|
+
|
|
11
|
+
No API or CLI breakage — every 0.1.1 invocation behaves as before. Generated
|
|
12
|
+
output does change, though, so **a committed spec will show drift**: on an R4
|
|
13
|
+
`Observation`, schemas and paths are byte-identical, 6 of 38 search parameters
|
|
14
|
+
gain `x-fhir-search-values`/`x-fhir-search-prefixes` and a fuller description,
|
|
15
|
+
and 4 more have trailing whitespace trimmed from HL7's description text.
|
|
16
|
+
Regenerate committed specs, or `fhir-oas check` will fail in CI.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `--search-params` limits which resource-specific search parameters are
|
|
21
|
+
emitted: a preset (`all`, `minimal`, `none`), an explicit code list
|
|
22
|
+
(`code,date,subject`), or per-resource (`Patient:name,birthdate`). Unknown
|
|
23
|
+
codes are an error rather than being silently dropped. Combines with
|
|
24
|
+
`--capability` by intersection. The common result parameters (`_id`,
|
|
25
|
+
`_count`, ...) are always emitted. `minimal` is tiered — common parameter
|
|
26
|
+
names first, then parameters addressing a top-level element directly, then
|
|
27
|
+
everything — so no resource that defines a search parameter is left with
|
|
28
|
+
none, while a median resource keeps 4. Presets can be tuned with `+code` and
|
|
29
|
+
`-code` (`minimal,+based-on`, `all,-note`), because no fixed rule can know
|
|
30
|
+
that `Observation.based-on`, `CarePlan.goal` or
|
|
31
|
+
`MedicationStatement.adherence` matter for their resource.
|
|
32
|
+
- `fhir-oas --version` (and `-V`) prints the installed version. It is read from
|
|
33
|
+
`package.json` at runtime rather than inlined at build time, so the CLI
|
|
34
|
+
cannot report a version the package does not have.
|
|
35
|
+
- `./package.json` is now a subpath export. Reaching for it previously raised
|
|
36
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`, which some bundlers and
|
|
37
|
+
version-introspection scripts trip over.
|
|
38
|
+
- `x-fhir-search-values` on token search parameters bound to a required
|
|
39
|
+
ValueSet, and `x-fhir-search-prefixes` on `number`/`date`/`quantity`
|
|
40
|
+
parameters, listing the comparison prefixes (`eq`, `ne`, `gt`, `lt`, `ge`,
|
|
41
|
+
`le`, `sa`, `eb`, `ap`) their values may carry. Both are metadata rather than
|
|
42
|
+
schema constraints: an `enum` would reject the comma-OR, `system|code` and
|
|
43
|
+
`:modifier` forms that FHIR search permits.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Search parameter descriptions are now derived from the definitions rather
|
|
48
|
+
than passed through from HL7's prose, which is not uniform — `Encounter-status`
|
|
49
|
+
spelled its codes out while `Observation-status` did not, though both are
|
|
50
|
+
token parameters over a required binding. Every parameter of a given kind now
|
|
51
|
+
reads the same way, and reports the codes its own FHIR version defines.
|
|
52
|
+
- Search parameters shared across resources carry a union expression
|
|
53
|
+
(`Patient.gender | Person.gender | ...`); the branch matching the resource
|
|
54
|
+
being generated is now resolved, so `MedicationRequest.status` and
|
|
55
|
+
`MedicationDispense.status` each report their own code list. Previously such
|
|
56
|
+
parameters were left unenumerated entirely.
|
|
57
|
+
|
|
58
|
+
## [0.1.1] - 2026-09-13
|
|
59
|
+
|
|
60
|
+
Three correctness fixes. Specs generated with 0.1.0 should be regenerated:
|
|
61
|
+
all three produced silently wrong output rather than errors.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **Every schema was missing its `id` property.** The OpenAPI emitter stripped
|
|
66
|
+
the JSON Schema `id`/`$id` keywords everywhere, including inside `properties`
|
|
67
|
+
maps where `id` is an ordinary FHIR element name. 660 of 679 R4 definitions
|
|
68
|
+
declare one, so nearly every generated model lacked its resource id.
|
|
69
|
+
- **Binding enums were absent on R4B and R5.** Enum generation read inline
|
|
70
|
+
enums from the official `fhir.schema.json`, but HL7 stopped inlining them
|
|
71
|
+
after R4 (R4 has 246 occurrences; R4B has 27 and R5 has 26). Required-binding
|
|
72
|
+
codes are now resolved from the vendored definitions on every version, so
|
|
73
|
+
`--no-enums` is once again the only thing that turns them off.
|
|
74
|
+
- **Profiles failed to generate against real IG packages.** A `contentReference`
|
|
75
|
+
written in the absolute canonical form that IG snapshot generators emit
|
|
76
|
+
(`http://hl7.org/fhir/StructureDefinition/Observation#Observation.referenceRange`)
|
|
77
|
+
was parsed as though it were the core `#Observation.referenceRange` short
|
|
78
|
+
form, producing a mangled schema name and aborting generation. US Core Blood
|
|
79
|
+
Pressure, among others, could not be generated at all.
|
|
80
|
+
|
|
81
|
+
### Documentation
|
|
82
|
+
|
|
83
|
+
- Corrected the `fixed[x]` → `const` claim. Real IGs usually pin values at
|
|
84
|
+
paths _inside_ a datatype or slice (`Observation.category.coding.code`).
|
|
85
|
+
Those datatypes are emitted once as shared schemas and referenced by `$ref`,
|
|
86
|
+
so such constraints cannot be represented and are **not** applied. Only fixed
|
|
87
|
+
values on elements a profile emits as their own property become a `const`.
|
|
88
|
+
|
|
89
|
+
### Testing
|
|
90
|
+
|
|
91
|
+
- Added `claims.test.ts`: one assertion block per README capability claim,
|
|
92
|
+
across every FHIR version, OpenAPI target and backend it claims to support.
|
|
93
|
+
- Added `fidelity.test.ts`: field-level checks that use the vendored FHIR
|
|
94
|
+
definitions as an oracle instead of hand-written expectations. Covers 45
|
|
95
|
+
resources drawn from the Foundation, Base, Clinical, Financial and
|
|
96
|
+
Specialized modules, in R4/R4B/R5, down to backbone depth — cardinality,
|
|
97
|
+
requiredness, primitive JSON types, choice expansion, `_field` primitive
|
|
98
|
+
extension siblings, and `$ref` resolution.
|
|
99
|
+
|
|
100
|
+
All three bugs above shared a cause: coverage that confirmed expectations at a
|
|
101
|
+
single point of a multi-point matrix. Enums were asserted only on R4, profiles
|
|
102
|
+
only on `us-core-patient`, and `id` never at all.
|
|
103
|
+
|
|
104
|
+
## [0.1.0] - 2026-09-12
|
|
105
|
+
|
|
106
|
+
Initial release.
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Turn any FHIR resource into an OpenAPI spec — and generate typed models in any language.**
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/fhir-openapi-translator)
|
|
6
|
+
[](https://www.npmjs.com/package/fhir-openapi-translator)
|
|
5
7
|
[](https://github.com/krishgok/fhir-openapi-translator/actions/workflows/ci.yml)
|
|
6
8
|
[](LICENSE)
|
|
7
9
|
[](https://nodejs.org)
|
|
@@ -14,9 +16,12 @@ HAPI FHIR and Firely give Java/.NET teams great FHIR models. Everyone else — G
|
|
|
14
16
|
## Install
|
|
15
17
|
|
|
16
18
|
```sh
|
|
17
|
-
npm install fhir-openapi-translator
|
|
19
|
+
npm install -g fhir-openapi-translator # `fhir-oas` on your PATH
|
|
20
|
+
npm install fhir-openapi-translator # or as a library dependency
|
|
18
21
|
```
|
|
19
22
|
|
|
23
|
+
Or run it without installing: `npx fhir-oas generate Patient --fhir-version r4`
|
|
24
|
+
|
|
20
25
|
Node.js ≥ 20. FHIR definitions ship with the package — no network, no server.
|
|
21
26
|
|
|
22
27
|
## Quick start
|
|
@@ -45,6 +50,9 @@ fhir-oas generate Patient -f r4 --operations
|
|
|
45
50
|
# Apply an Implementation Guide profile (e.g. US Core)
|
|
46
51
|
fhir-oas generate Patient -f r4 --ig hl7.fhir.us.core@5.0.1 --profile us-core-patient
|
|
47
52
|
|
|
53
|
+
# Emit only the search parameters your deployment indexes (38 -> 9 here)
|
|
54
|
+
fhir-oas generate Observation -f r4 --search-params minimal,+based-on
|
|
55
|
+
|
|
48
56
|
# Match one server's declared surface (reads its /metadata)
|
|
49
57
|
fhir-oas generate -f r4 --capability https://server.example.org/fhir
|
|
50
58
|
|
|
@@ -71,16 +79,17 @@ const doc = generateOpenApi({ resources: ["Patient"], fhirVersion: "r4" });
|
|
|
71
79
|
- **Custom operations** from the official OperationDefinitions.
|
|
72
80
|
- **Profiles / IGs** — apply US Core-style constraints from any IG package.
|
|
73
81
|
- **CapabilityStatement-driven** — generate exactly what a server supports.
|
|
82
|
+
- **Search parameters, documented and tunable** — accepted codes and comparison prefixes per FHIR version; emit only the ones your deployment indexes.
|
|
74
83
|
- **Merge mode & drift guard** — coexist with hand-written specs, catch drift in CI.
|
|
75
84
|
|
|
76
85
|
## Where it fits
|
|
77
86
|
|
|
78
|
-
|
|
|
79
|
-
|
|
80
|
-
| Output
|
|
81
|
-
| Runtime needed
|
|
82
|
-
| US Core / IG profiles
|
|
83
|
-
| Per-resource, codegen-tuned specs |
|
|
87
|
+
| | fhir-openapi-translator | HAPI / Firely |
|
|
88
|
+
| --------------------------------- | :----------------------: | :--------------------: |
|
|
89
|
+
| Output | OpenAPI (→ any language) | Java / .NET models |
|
|
90
|
+
| Runtime needed | none (offline CLI) | a running server / SDK |
|
|
91
|
+
| US Core / IG profiles | ✅ | ✅ |
|
|
92
|
+
| Per-resource, codegen-tuned specs | ✅ | — |
|
|
84
93
|
|
|
85
94
|
**Complements a FHIR SDK, doesn't replace it.** Keep HAPI or Firely for server-side models and conformance — this produces the OpenAPI contract around them: for consumers in any language, and for the tooling you already run (gateways, mock servers, contract tests).
|
|
86
95
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -326,7 +326,8 @@ function buildElementTree(sd) {
|
|
|
326
326
|
return root;
|
|
327
327
|
}
|
|
328
328
|
function contentReferenceName(reference, sd, rootName) {
|
|
329
|
-
const
|
|
329
|
+
const hash = reference.lastIndexOf("#");
|
|
330
|
+
const path3 = hash >= 0 ? reference.slice(hash + 1) : reference;
|
|
330
331
|
const [root, ...rest] = path3.split(".");
|
|
331
332
|
if (rest.length === 0) return root ?? path3;
|
|
332
333
|
const prefix = root === (sd.type ?? sd.name) ? rootName : root ?? "";
|
|
@@ -495,6 +496,148 @@ function applyProfile(context, requested, registry) {
|
|
|
495
496
|
return { schemaName, resourceType: profile.type, url: profile.url };
|
|
496
497
|
}
|
|
497
498
|
|
|
499
|
+
// src/searchParams.ts
|
|
500
|
+
var SEARCH_PREFIXES = ["eq", "ne", "gt", "lt", "ge", "le", "sa", "eb", "ap"];
|
|
501
|
+
var PREFIXABLE_TYPES = /* @__PURE__ */ new Set(["number", "date", "quantity"]);
|
|
502
|
+
function prefixesFor(searchParamType) {
|
|
503
|
+
return PREFIXABLE_TYPES.has(searchParamType) ? SEARCH_PREFIXES : void 0;
|
|
504
|
+
}
|
|
505
|
+
function requiredBindingCodes(fhirVersion) {
|
|
506
|
+
const index = /* @__PURE__ */ new Map();
|
|
507
|
+
for (const sd of loadStructureDefinitions(fhirVersion)) {
|
|
508
|
+
for (const el of sd.elements) {
|
|
509
|
+
if (el.binding?.strength === "required" && el.binding.codes?.length) {
|
|
510
|
+
index.set(el.path, el.binding.codes);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return index;
|
|
515
|
+
}
|
|
516
|
+
var bindingCache = /* @__PURE__ */ new Map();
|
|
517
|
+
function searchParamCodes(fhirVersion, resource, type, expression) {
|
|
518
|
+
if (type !== "token" || !expression) return void 0;
|
|
519
|
+
const branch = expression.split("|").map((part) => part.trim()).find((part) => part.startsWith(`${resource}.`) || !expression.includes("|"));
|
|
520
|
+
if (!branch || !/^[A-Za-z]+(?:\.[A-Za-z]+)+$/.test(branch)) return void 0;
|
|
521
|
+
if (branch.split(".")[0] !== resource) return void 0;
|
|
522
|
+
let index = bindingCache.get(fhirVersion);
|
|
523
|
+
if (!index) {
|
|
524
|
+
index = requiredBindingCodes(fhirVersion);
|
|
525
|
+
bindingCache.set(fhirVersion, index);
|
|
526
|
+
}
|
|
527
|
+
return index.get(branch);
|
|
528
|
+
}
|
|
529
|
+
var COMMON_CODES = /* @__PURE__ */ new Set([
|
|
530
|
+
"identifier",
|
|
531
|
+
"status",
|
|
532
|
+
"patient",
|
|
533
|
+
"subject",
|
|
534
|
+
"encounter",
|
|
535
|
+
"code",
|
|
536
|
+
"category",
|
|
537
|
+
"date",
|
|
538
|
+
"type",
|
|
539
|
+
"url",
|
|
540
|
+
"name"
|
|
541
|
+
]);
|
|
542
|
+
function directElementBranch(resource, expression) {
|
|
543
|
+
if (!expression) return void 0;
|
|
544
|
+
const branch = expression.split("|").map((part) => part.trim()).find((part) => part.startsWith(`${resource}.`));
|
|
545
|
+
return branch && /^[A-Za-z]+\.[A-Za-z]+$/.test(branch) ? branch : void 0;
|
|
546
|
+
}
|
|
547
|
+
function minimalCodes(resource, available) {
|
|
548
|
+
const common = available.filter((sp) => COMMON_CODES.has(sp.code));
|
|
549
|
+
if (common.length > 0) return new Set(common.map((sp) => sp.code));
|
|
550
|
+
const direct = available.filter((sp) => directElementBranch(resource, sp.expression));
|
|
551
|
+
if (direct.length > 0) return new Set(direct.map((sp) => sp.code));
|
|
552
|
+
return new Set(available.map((sp) => sp.code));
|
|
553
|
+
}
|
|
554
|
+
var PRESETS = /* @__PURE__ */ new Set(["all", "minimal", "none"]);
|
|
555
|
+
function parseOne(value) {
|
|
556
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
557
|
+
if (parts.length === 0) throw new Error(`--search-params: empty selection in "${value}"`);
|
|
558
|
+
const add = /* @__PURE__ */ new Set();
|
|
559
|
+
const remove = /* @__PURE__ */ new Set();
|
|
560
|
+
const literal = /* @__PURE__ */ new Set();
|
|
561
|
+
let preset;
|
|
562
|
+
for (const [index, part] of parts.entries()) {
|
|
563
|
+
if (part.startsWith("+") || part.startsWith("-")) {
|
|
564
|
+
const code = part.slice(1);
|
|
565
|
+
if (!code) throw new Error(`--search-params: "${part}" names no parameter`);
|
|
566
|
+
(part.startsWith("+") ? add : remove).add(code);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const lower = part.toLowerCase();
|
|
570
|
+
if (PRESETS.has(lower)) {
|
|
571
|
+
if (index !== 0) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`--search-params: preset "${part}" must come first in "${value}" (e.g. "minimal,+${parts[0]}").`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
preset = lower;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
literal.add(part);
|
|
580
|
+
}
|
|
581
|
+
if (preset && literal.size > 0) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`--search-params: "${value}" mixes the preset "${preset}" with bare codes ${[...literal].map((c) => `"${c}"`).join(", ")}. Prefix them with + to add to the preset, or drop the preset to list codes exactly.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const rule = { base: preset ?? literal };
|
|
587
|
+
if (add.size > 0) rule.add = add;
|
|
588
|
+
if (remove.size > 0) rule.remove = remove;
|
|
589
|
+
return rule;
|
|
590
|
+
}
|
|
591
|
+
function parseSearchParamSpec(specs) {
|
|
592
|
+
const selection = {};
|
|
593
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
594
|
+
for (const spec of specs) {
|
|
595
|
+
const colon = spec.indexOf(":");
|
|
596
|
+
if (colon > 0 && /^[A-Z][A-Za-z]*$/.test(spec.slice(0, colon))) {
|
|
597
|
+
const resource = spec.slice(0, colon);
|
|
598
|
+
if (byResource.has(resource)) {
|
|
599
|
+
throw new Error(`--search-params: ${resource} given more than once`);
|
|
600
|
+
}
|
|
601
|
+
byResource.set(resource, parseOne(spec.slice(colon + 1)));
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (selection.default !== void 0) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`--search-params: more than one unscoped selection ("${spec}"). Scope them per resource (Patient:name,birthdate) or pass a single list.`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
selection.default = parseOne(spec);
|
|
610
|
+
}
|
|
611
|
+
if (byResource.size > 0) selection.byResource = byResource;
|
|
612
|
+
return selection;
|
|
613
|
+
}
|
|
614
|
+
function resolveSearchParamCodes(selection, fhirVersion, resource) {
|
|
615
|
+
const rule = selection?.byResource?.get(resource) ?? selection?.default;
|
|
616
|
+
if (rule === void 0) return void 0;
|
|
617
|
+
if (rule.base === "all" && !rule.add && !rule.remove) return void 0;
|
|
618
|
+
const available = loadSearchParameters(fhirVersion).filter((sp) => sp.base.includes(resource));
|
|
619
|
+
const codes = new Set(available.map((sp) => sp.code));
|
|
620
|
+
const named = [
|
|
621
|
+
...typeof rule.base === "string" ? [] : rule.base,
|
|
622
|
+
...rule.add ?? [],
|
|
623
|
+
...rule.remove ?? []
|
|
624
|
+
];
|
|
625
|
+
const unknown = named.filter((code) => !codes.has(code));
|
|
626
|
+
if (unknown.length > 0) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`--search-params: ${resource} has no search parameter ${unknown.map((c) => `"${c}"`).join(", ")} in ${fhirVersion.toUpperCase()}. Run "fhir-oas generate ${resource} -f ${fhirVersion}" to see the available codes.`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
let selected;
|
|
632
|
+
if (rule.base === "all") selected = new Set(codes);
|
|
633
|
+
else if (rule.base === "none") selected = /* @__PURE__ */ new Set();
|
|
634
|
+
else if (rule.base === "minimal") selected = minimalCodes(resource, available);
|
|
635
|
+
else selected = new Set(rule.base);
|
|
636
|
+
for (const code of rule.add ?? []) selected.add(code);
|
|
637
|
+
for (const code of rule.remove ?? []) selected.delete(code);
|
|
638
|
+
return selected;
|
|
639
|
+
}
|
|
640
|
+
|
|
498
641
|
// src/backends/schemaJson.ts
|
|
499
642
|
function buildRegistryFromSchemaJson(fhirVersion) {
|
|
500
643
|
const schema = loadFhirSchema(fhirVersion);
|
|
@@ -508,8 +651,44 @@ function buildRegistryFromSchemaJson(fhirVersion) {
|
|
|
508
651
|
return props?.resourceType !== void 0 && "const" in (props.resourceType ?? {});
|
|
509
652
|
}).map(([name]) => name);
|
|
510
653
|
}
|
|
654
|
+
applyBindingEnums(definitions, fhirVersion);
|
|
511
655
|
return { fhirVersion, definitions, resourceNames };
|
|
512
656
|
}
|
|
657
|
+
var lowerFirst = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
658
|
+
function elementPathFor(definitionName, property) {
|
|
659
|
+
const [root, ...backbones] = definitionName.split("_");
|
|
660
|
+
return [root, ...backbones.map(lowerFirst), property].join(".");
|
|
661
|
+
}
|
|
662
|
+
function bindingCodeIndex(fhirVersion) {
|
|
663
|
+
const index = /* @__PURE__ */ new Map();
|
|
664
|
+
for (const sd of loadStructureDefinitions(fhirVersion)) {
|
|
665
|
+
for (const el of sd.elements) {
|
|
666
|
+
if (el.binding?.strength === "required" && el.binding.codes?.length) {
|
|
667
|
+
index.set(el.path, el.binding.codes);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return index;
|
|
672
|
+
}
|
|
673
|
+
function applyBindingEnums(definitions, fhirVersion) {
|
|
674
|
+
const codesByPath = bindingCodeIndex(fhirVersion);
|
|
675
|
+
const isCodeRef = (node) => !!node && node.$ref === "#/definitions/code";
|
|
676
|
+
for (const [name, definition] of definitions) {
|
|
677
|
+
const properties = definition.properties;
|
|
678
|
+
if (!properties) continue;
|
|
679
|
+
for (const [property, schema] of Object.entries(properties)) {
|
|
680
|
+
const codes = codesByPath.get(elementPathFor(name, property));
|
|
681
|
+
if (!codes) continue;
|
|
682
|
+
const description = schema.description;
|
|
683
|
+
const withDescription = (node) => description ? { description, ...node } : node;
|
|
684
|
+
if (isCodeRef(schema)) {
|
|
685
|
+
properties[property] = withDescription({ enum: [...codes] });
|
|
686
|
+
} else if (schema.type === "array" && isCodeRef(schema.items)) {
|
|
687
|
+
properties[property] = withDescription({ type: "array", items: { enum: [...codes] } });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
513
692
|
|
|
514
693
|
// src/backends/structureDefinition.ts
|
|
515
694
|
function buildRegistryFromStructureDefinitions(fhirVersion) {
|
|
@@ -593,9 +772,22 @@ function convertNode(node, target, options) {
|
|
|
593
772
|
switch (key) {
|
|
594
773
|
case "$schema":
|
|
595
774
|
case "$comment":
|
|
775
|
+
// Draft-04/06 schema keywords. These are only keywords at schema-node
|
|
776
|
+
// level: inside `properties` the same names are FHIR element names, and
|
|
777
|
+
// nearly every FHIR element has an `id`. See the "properties" case.
|
|
596
778
|
case "id":
|
|
597
779
|
case "$id":
|
|
598
780
|
break;
|
|
781
|
+
case "properties": {
|
|
782
|
+
const converted = {};
|
|
783
|
+
for (const [propertyName, propertySchema] of Object.entries(
|
|
784
|
+
value ?? {}
|
|
785
|
+
)) {
|
|
786
|
+
converted[propertyName] = convertNode(propertySchema, target, options);
|
|
787
|
+
}
|
|
788
|
+
out.properties = converted;
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
599
791
|
case "$ref":
|
|
600
792
|
out.$ref = typeof value === "string" && value.startsWith(DEFINITIONS_REF) ? COMPONENTS_REF + value.slice(DEFINITIONS_REF.length) : value;
|
|
601
793
|
break;
|
|
@@ -870,16 +1062,41 @@ function commonSearchParameterComponents() {
|
|
|
870
1062
|
}
|
|
871
1063
|
return out;
|
|
872
1064
|
}
|
|
1065
|
+
function isCodeListDescription(description) {
|
|
1066
|
+
return description.includes("|") && /^[A-Za-z0-9\-.\s|+]+$/.test(description);
|
|
1067
|
+
}
|
|
1068
|
+
function describeSearchParameter(description, codes, prefixes) {
|
|
1069
|
+
let base = description.trim();
|
|
1070
|
+
if (codes?.length && isCodeListDescription(base)) base = "";
|
|
1071
|
+
const notes = [];
|
|
1072
|
+
if (codes?.length) notes.push(`Accepted values: ${codes.join(" | ")}.`);
|
|
1073
|
+
if (prefixes?.length) {
|
|
1074
|
+
notes.push(`Values may carry a comparison prefix (${prefixes.join(", ")}), e.g. ge2021-01-01.`);
|
|
1075
|
+
}
|
|
1076
|
+
if (notes.length === 0) return base;
|
|
1077
|
+
if (base.length === 0) return notes.join(" ");
|
|
1078
|
+
const separator = base.includes("\n") ? "\n\n" : /[.?!]$/.test(base) ? " " : ". ";
|
|
1079
|
+
return `${base}${separator}${notes.join(" ")}`;
|
|
1080
|
+
}
|
|
873
1081
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1082
|
+
return loadSearchParameters(fhirVersion).filter((sp) => sp.base.includes(resource) && (!only || only.has(sp.code))).sort((a, b) => a.code.localeCompare(b.code)).map((sp) => {
|
|
1083
|
+
const codes = searchParamCodes(fhirVersion, resource, sp.type, sp.expression);
|
|
1084
|
+
const prefixes = prefixesFor(sp.type);
|
|
1085
|
+
return {
|
|
1086
|
+
name: sp.code,
|
|
1087
|
+
in: "query",
|
|
1088
|
+
required: false,
|
|
1089
|
+
description: describeSearchParameter(sp.description ?? "", codes, prefixes),
|
|
1090
|
+
// FHIR search values carry prefixes and modifiers, so all are strings.
|
|
1091
|
+
// The accepted values are advertised via x-fhir-search-values rather
|
|
1092
|
+
// than `enum`, which would reject the comma-OR (`status=final,amended`),
|
|
1093
|
+
// system|code and `:modifier` forms that FHIR search permits.
|
|
1094
|
+
schema: { type: "string" },
|
|
1095
|
+
"x-fhir-search-type": sp.type,
|
|
1096
|
+
...codes?.length ? { "x-fhir-search-values": codes } : {},
|
|
1097
|
+
...prefixes?.length ? { "x-fhir-search-prefixes": prefixes } : {}
|
|
1098
|
+
};
|
|
1099
|
+
});
|
|
883
1100
|
}
|
|
884
1101
|
var historyParameters = () => [
|
|
885
1102
|
{ $ref: `${PARAMETERS}_count` },
|
|
@@ -1165,11 +1382,13 @@ function generateOpenApi(options) {
|
|
|
1165
1382
|
const paths = {};
|
|
1166
1383
|
for (const resource of resources) {
|
|
1167
1384
|
const cap = capabilityByResource.get(resource);
|
|
1385
|
+
const selected = resolveSearchParamCodes(options.searchParams, options.fhirVersion, resource);
|
|
1386
|
+
const searchParamCodes2 = cap?.searchParamCodes && selected ? new Set([...selected].filter((code) => cap.searchParamCodes.has(code))) : selected ?? cap?.searchParamCodes;
|
|
1168
1387
|
Object.assign(
|
|
1169
1388
|
paths,
|
|
1170
1389
|
buildResourcePaths(options.fhirVersion, resource, schemaFor(resource), {
|
|
1171
1390
|
interactions: cap?.interactions,
|
|
1172
|
-
searchParamCodes:
|
|
1391
|
+
searchParamCodes: searchParamCodes2
|
|
1173
1392
|
})
|
|
1174
1393
|
);
|
|
1175
1394
|
}
|
|
@@ -1417,6 +1636,7 @@ export {
|
|
|
1417
1636
|
loadIg,
|
|
1418
1637
|
loadIgSync,
|
|
1419
1638
|
buildCoreValueSetFallback,
|
|
1639
|
+
parseSearchParamSpec,
|
|
1420
1640
|
listResources,
|
|
1421
1641
|
generateOpenApi,
|
|
1422
1642
|
loadCapabilityStatement,
|
|
@@ -1426,4 +1646,4 @@ export {
|
|
|
1426
1646
|
diffAgainstYaml,
|
|
1427
1647
|
stringifyDocument
|
|
1428
1648
|
};
|
|
1429
|
-
//# sourceMappingURL=chunk-
|
|
1649
|
+
//# sourceMappingURL=chunk-RRAIF3A2.js.map
|