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/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  mergeIntoYaml,
12
12
  parseCapabilityStatement,
13
13
  stringifyDocument
14
- } from "./chunk-G3DCRADV.js";
14
+ } from "./chunk-RRAIF3A2.js";
15
15
  export {
16
16
  FHIR_VERSIONS,
17
17
  FHIR_VERSION_NUMBERS,
package/docs/REFERENCE.md CHANGED
@@ -7,6 +7,7 @@ Full CLI, library API, codegen recipes, and limitations. For a quick start see t
7
7
  - [CLI](#cli)
8
8
  - [Library API](#library-api)
9
9
  - [Codegen recipes](#codegen-recipes)
10
+ - [Search parameters](#search-parameters)
10
11
  - [Viewing generated specs](#viewing-generated-specs)
11
12
  - [What this is for — and what it is not](#what-this-is-for--and-what-it-is-not)
12
13
  - [Profiles: what `--profile` applies](#profiles-what---profile-applies)
@@ -48,6 +49,13 @@ fhir-oas generate Patient -f r4 --ig hl7.fhir.us.core@5.0.1 --profile us-core-pa
48
49
  # List the profiles in an IG package
49
50
  fhir-oas list --ig ./hl7.fhir.us.core-5.0.1.tgz
50
51
 
52
+ # Limit the search parameters. Observation defines 38 on R4:
53
+ fhir-oas generate Observation -f r4 --search-params none # -> 0
54
+ fhir-oas generate Observation -f r4 --search-params minimal # -> 8, a common core
55
+ fhir-oas generate Observation -f r4 --search-params code,date # -> exactly the 2 you name
56
+ fhir-oas generate Patient Observation -f r4 \
57
+ --search-params Patient:name,birthdate Observation:code,date # per resource
58
+
51
59
  # Match a specific server's declared surface (resources, interactions, search params, operations)
52
60
  fhir-oas generate -f r4 --capability https://server.example.org/fhir # appends /metadata
53
61
  fhir-oas generate -f r4 --capability ./metadata.json # or a saved statement
@@ -58,17 +66,21 @@ fhir-oas generate -f r4 --capability ./metadata.json # or a s
58
66
  ## Library API
59
67
 
60
68
  ```ts
61
- import { generateOpenApi, listResources, mergeIntoYaml } from "fhir-openapi-translator";
69
+ import {
70
+ generateOpenApi,
71
+ listResources,
72
+ mergeIntoYaml,
73
+ } from "fhir-openapi-translator";
62
74
 
63
75
  const doc = generateOpenApi({
64
76
  resources: ["Patient", "Observation"],
65
77
  fhirVersion: "r4",
66
- openApiVersion: "3.0.3", // default
78
+ openApiVersion: "3.0.3", // default
67
79
  baseUrl: "https://fhir.example.org/r4",
68
80
  trim: { excludeNarrative: true, maxDepth: 3 },
69
81
  });
70
82
 
71
- listResources("r5"); // ["Account", "ActivityDefinition", ...]
83
+ listResources("r5"); // ["Account", "ActivityDefinition", ...]
72
84
 
73
85
  // Merge into existing YAML text, preserving comments and formatting
74
86
  const mergedYaml = mergeIntoYaml(doc, existingYamlText, { force: false });
@@ -79,11 +91,11 @@ Profiles from an IG package (`loadIg` is async for registry coordinates; a local
79
91
  ```ts
80
92
  import { generateOpenApi, loadIg } from "fhir-openapi-translator";
81
93
 
82
- const ig = await loadIg("hl7.fhir.us.core@5.0.1"); // or "./us-core.tgz", or a directory
94
+ const ig = await loadIg("hl7.fhir.us.core@5.0.1"); // or "./us-core.tgz", or a directory
83
95
  const doc = generateOpenApi({
84
96
  resources: ["Patient"],
85
97
  fhirVersion: "r4",
86
- ig, // or ig: "./us-core.tgz" (loaded synchronously)
98
+ ig, // or ig: "./us-core.tgz" (loaded synchronously)
87
99
  profiles: ["us-core-patient"],
88
100
  });
89
101
  ```
@@ -91,10 +103,15 @@ const doc = generateOpenApi({
91
103
  Match a server's CapabilityStatement (`loadCapabilityStatement` is async; `parseCapabilityStatement` takes an already-loaded object):
92
104
 
93
105
  ```ts
94
- import { generateOpenApi, loadCapabilityStatement } from "fhir-openapi-translator";
95
-
96
- const capability = await loadCapabilityStatement("https://server.example.org/fhir");
97
- const doc = generateOpenApi({ fhirVersion: "r4", capability }); // resources come from the statement
106
+ import {
107
+ generateOpenApi,
108
+ loadCapabilityStatement,
109
+ } from "fhir-openapi-translator";
110
+
111
+ const capability = await loadCapabilityStatement(
112
+ "https://server.example.org/fhir",
113
+ );
114
+ const doc = generateOpenApi({ fhirVersion: "r4", capability }); // resources come from the statement
98
115
  ```
99
116
 
100
117
  See `GenerateOptions` in the type declarations for the full API surface.
@@ -114,10 +131,10 @@ every schema in the document eagerly. FHIR's schemas are large and mutually
114
131
  recursive, so it degrades sharply as the spec grows. Measured on specs from
115
132
  this tool:
116
133
 
117
- | Spec | Swagger UI behaviour |
118
- |---|---|
119
- | 2 resources (~73 schemas) | renders fine; models expand promptly |
120
- | 6 resources (~84 schemas) | renders, but expanding one operation blocks the page for ~90s |
134
+ | Spec | Swagger UI behaviour |
135
+ | --------------------------- | -------------------------------------------------------------------------- |
136
+ | 2 resources (~73 schemas) | renders fine; models expand promptly |
137
+ | 6 resources (~84 schemas) | renders, but expanding one operation blocks the page for ~90s |
121
138
  | 39 resources (~188 schemas) | the **Schemas** section never finished rendering (gave up after 5 minutes) |
122
139
 
123
140
  [Redoc](https://github.com/Redocly/redoc) renders lazily and copes with these
@@ -127,11 +144,120 @@ specs far better. Prefer it for anything beyond a few resources:
127
144
  npx @redocly/cli preview-docs fhir-r4.openapi.yaml
128
145
  ```
129
146
 
130
- This is a *viewer* limitation, not a defect in the generated specs — they
147
+ This is a _viewer_ limitation, not a defect in the generated specs — they
131
148
  validate against the OpenAPI meta-schemas and feed code generators cleanly at
132
149
  any size. If you do need Swagger UI on a large spec, `--exclude-narrative` and
133
150
  `--max-depth` shrink the schema graph considerably.
134
151
 
152
+ ## Search parameters
153
+
154
+ Every search parameter is typed as a string (except `_count`). FHIR search
155
+ values carry prefixes, modifiers and `system|code` forms that a stricter schema
156
+ would reject, so the accepted values are advertised as metadata instead of
157
+ constraints:
158
+
159
+ | Field | On | Meaning |
160
+ | ------------------------ | ---------------------------------------- | -------------------------------------------------------- |
161
+ | `x-fhir-search-type` | every parameter | the FHIR search type (`token`, `date`, `reference`, ...) |
162
+ | `x-fhir-search-values` | token parameters over a required binding | the codes that FHIR version defines for that element |
163
+ | `x-fhir-search-prefixes` | `number`, `date`, `quantity` | the comparison prefixes the value may carry |
164
+
165
+ The same information is appended to the parameter `description`, because HL7's
166
+ own `SearchParameter.description` text is not uniform — `Encounter-status`
167
+ spells its codes out, `Observation-status` does not, though both are token
168
+ parameters over a required binding. Deriving them from the definitions makes
169
+ every parameter of a given kind read the same way, and makes them correct per
170
+ version: `Encounter.status` is `arrived | triaged | onleave | finished ...` on
171
+ R4 and `on-hold | completed | discharged ...` on R5.
172
+
173
+ Parameters shared across resources (`Patient.gender | Person.gender | ...`)
174
+ are resolved to the branch for the resource being generated, so
175
+ `MedicationRequest.status` and `MedicationDispense.status` each get their own
176
+ code list rather than one standing in for the other.
177
+
178
+ **Why not `enum`?** Because it would reject valid searches: comma-OR
179
+ (`?status=final,amended`), the token `system|code` form
180
+ (`?status=http://hl7.org/fhir/observation-status|final`), and every
181
+ `:modifier` spelling (`?status:not=entered-in-error`). Consumers that want hard
182
+ validation can read `x-fhir-search-values` and generate it themselves.
183
+
184
+ ### Emitting fewer of them
185
+
186
+ Servers commonly index only a subset of the standard search parameters — each
187
+ one indexed costs storage and write throughput — and a spec that advertises
188
+ parameters the server does not support is wrong in the direction that causes
189
+ support tickets. `--search-params` narrows what is emitted:
190
+
191
+ ```sh
192
+ fhir-oas generate Observation -f r4 --search-params none # none at all
193
+ fhir-oas generate Observation -f r4 --search-params minimal # the common core below
194
+ fhir-oas generate Observation -f r4 --search-params code,date # exactly the codes you name
195
+ fhir-oas generate Patient Observation -f r4 \
196
+ --search-params Patient:name,birthdate Observation:code,date
197
+ ```
198
+
199
+ A code list is taken literally — it is _your_ list, and nothing is added to it.
200
+
201
+ ### Tuning a preset
202
+
203
+ No fixed rule can know which parameters matter for a given resource.
204
+ `Observation.based-on`, `CarePlan.goal` and `MedicationStatement.adherence` are
205
+ each central to their resource, and none is common enough to curate globally. A
206
+ preset is therefore a starting point that `+code` and `-code` adjust:
207
+
208
+ ```sh
209
+ --search-params minimal,+based-on # add to a preset
210
+ --search-params minimal,+goal,+condition,+focus # add as many as you like
211
+ --search-params minimal,-category # remove from a preset
212
+ --search-params minimal,+based-on,-category # add and remove together
213
+ --search-params all,-note,-derived-from # everything except
214
+ --search-params none,+code,+date # build up from nothing
215
+ --search-params Observation:minimal,+based-on CarePlan:minimal,+goal
216
+ ```
217
+
218
+ The preset comes first; everything after it is an adjustment. There is no
219
+ limit on how many, and additions and removals may be mixed in any order. Every code named
220
+ anywhere — in a list, an `+add`, or a `-remove` — must exist on that resource,
221
+ so a typo fails loudly instead of quietly shrinking the contract. Mixing a
222
+ preset with bare codes is rejected rather than guessed at: write `minimal,+code`
223
+ to extend the preset, or drop the preset to give an exact list.
224
+
225
+ `minimal` is the one curated selection, and it is tiered so that it means
226
+ something for every resource rather than only those that happen to use common
227
+ parameter names:
228
+
229
+ 1. The parameters most servers index regardless of resource type —
230
+ `identifier`, `status`, `patient`, `subject`, `encounter`, `code`,
231
+ `category`, `date`, `type`, `url`, `name` — where the resource defines any.
232
+ 2. Otherwise, parameters addressing a **top-level element** directly
233
+ (`Linkage.author`) rather than something nested or filtered
234
+ (`Linkage.item.resource`). These address the resource itself and are the
235
+ cheapest to index.
236
+ 3. Otherwise, everything the resource defines.
237
+
238
+ On R4 that takes `Observation` from 38 parameters to 8 (`category`, `code`,
239
+ `date`, `encounter`, `identifier`, `patient`, `status`, `subject`) and
240
+ `Patient` from 23 to 2 (`identifier`, `name`), both via tier 1. `Linkage`,
241
+ whose parameters are `author`, `item` and `source` and which names no common
242
+ code, goes from 3 to 1 via tier 2 rather than to nothing.
243
+
244
+ Across R4/R4B/R5 no resource that defines a search parameter is left with none,
245
+ while a median resource keeps 4 and roughly two thirds of resource-specific
246
+ parameters are dropped. A resource that defines **no** search parameters at all
247
+ (`Binary`, `OperationOutcome`) yields none, because there is nothing to select
248
+ — that is not `minimal` doing anything.
249
+
250
+ It remains a convenience and a judgement call: FHIR has no "commonly indexed"
251
+ marker, so tier 1 is a fixed hand-picked list. When the selection matters, name
252
+ the codes explicitly or derive them from your server with `--capability` —
253
+ either is exact, where `minimal` is only a reasonable default.
254
+
255
+ Note that this trims the _contract_, not the server — it does not itself make
256
+ anything faster. Its value is that the published spec matches the deployment.
257
+ If your server already declares its tuned surface, `--capability` derives the
258
+ same restriction from `/metadata` with nothing to maintain by hand, and the two
259
+ combine: passing both emits only parameters that satisfy each.
260
+
135
261
  ## What this is for — and what it is not
136
262
 
137
263
  **Intended use**
@@ -144,8 +270,8 @@ any size. If you do need Swagger UI on a large spec, `--exclude-narrative` and
144
270
 
145
271
  **Not intended for — do not use this as**
146
272
 
147
- - **A FHIR validator.** Passing schema validation does *not* make a resource FHIR-conformant: FHIRPath invariants, terminology bindings, and profile constraints (slicing, must-support, cardinality refinements) are not fully represented in OpenAPI. Validate with a real FHIR validator (HAPI, the official validator, server-side `$validate`).
148
- - **A full profile / conformance engine.** `--profile` applies the *representable* profile constraints (see the table below), but slicing, extension slices, `pattern[x]`, FHIRPath invariants, and must-support are **not enforced** — they are surfaced as description notes and `x-fhir-constraints-omitted`, not as schema rules.
273
+ - **A FHIR validator.** Passing schema validation does _not_ make a resource FHIR-conformant: FHIRPath invariants, terminology bindings, and profile constraints (slicing, must-support, cardinality refinements) are not fully represented in OpenAPI. Validate with a real FHIR validator (HAPI, the official validator, server-side `$validate`).
274
+ - **A full profile / conformance engine.** `--profile` applies the _representable_ profile constraints (see the table below), but slicing, extension slices, `pattern[x]`, FHIRPath invariants, and must-support are **not enforced** — they are surfaced as description notes and `x-fhir-constraints-omitted`, not as schema rules.
149
275
  - **A replacement for a full FHIR SDK.** HAPI FHIR and Firely offer richer, spec-aware models and conformance tooling than any OpenAPI codegen can; where you need that depth, use them alongside this rather than instead of it.
150
276
  - **XML payload handling.** Only the FHIR JSON representation is modeled.
151
277
 
@@ -153,27 +279,39 @@ any size. If you do need Swagger UI on a large spec, `--exclude-narrative` and
153
279
 
154
280
  Given `--ig <package> --profile <id>`, the profile snapshot is turned into a schema named after the profile (`USCorePatient`), referenced from the base `/Patient` paths:
155
281
 
156
- | Profile constraint | Schema effect |
157
- |---|---|
158
- | `min ≥ 1` | property becomes `required` |
159
- | `max: "0"` | property omitted |
160
- | `max: "1"` on a base array | scalar instead of array |
161
- | required binding, resolvable ValueSet (IG-local, then vendored core; ≤150 codes) | inline `enum` |
162
- | `fixed[x]` | `const` (3.1) / single-value `enum` (3.0.3) |
163
- | choice-type narrowing | only the permitted `value[x]` expansions emitted |
164
- | `pattern[x]`, slicing, invariants, must-support | *not enforced* — noted in `description` + `x-fhir-constraints-omitted` |
282
+ | Profile constraint | Schema effect |
283
+ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
284
+ | `min ≥ 1` | property becomes `required` |
285
+ | `max: "0"` | property omitted |
286
+ | `max: "1"` on a base array | scalar instead of array |
287
+ | required binding, resolvable ValueSet (IG-local, then vendored core; ≤150 codes) | inline `enum` |
288
+ | `fixed[x]` on an element emitted as its own property | `const` (3.1) / single-value `enum` (3.0.3) |
289
+ | `fixed[x]` **inside** a datatype or slice (e.g. `Observation.category.coding.code`) | _not applied_ — see below |
290
+ | choice-type narrowing | only the permitted `value[x]` expansions emitted |
291
+ | `pattern[x]`, slicing, invariants, must-support | _not enforced_ — noted in `description` + `x-fhir-constraints-omitted` |
292
+
293
+ Note on `fixed[x]`: real IGs usually pin values at paths _inside_ a datatype
294
+ (`Observation.category.coding.code`) or inside a slice, rather than on a
295
+ resource element directly. Those datatypes are emitted once as shared schemas
296
+ and referenced with `$ref`, so a constraint that applies to one profile's use
297
+ of `CodeableConcept` cannot be written into the shared `CodeableConcept`
298
+ schema. Such fixed values are therefore **not** represented — US Core Blood
299
+ Pressure, for example, declares six and none appear as `const`. Only fixed
300
+ values on elements the profile emits as their own property are applied.
165
301
 
166
302
  The IG package is a local `.tgz` / unpacked directory, or a `name@version` coordinate fetched from `packages.fhir.org` and cached under `~/.fhir-oas/packages`. Profiles must ship a snapshot (differential-only packages error); the package FHIR version must match `--fhir-version`; only the public registry is supported (no auth).
167
303
 
168
304
  ## Assumptions and known limitations
169
305
 
170
306
  - Output is inherently lossy relative to the FHIR specification (see above); it trades fidelity for reach across language ecosystems.
171
- - Enums cover *required* bindings whose ValueSet expands to a bounded code list (≤150 codes, no filters); extensible/preferred bindings and open code systems (BCP-47 languages, MIME types) stay plain strings by design.
307
+ - Enums cover _required_ bindings whose ValueSet expands to a bounded code list (≤150 codes, no filters); extensible/preferred bindings and open code systems (BCP-47 languages, MIME types) stay plain strings by design.
172
308
  - Primitive-extension properties (`_field`) are kept for JSON fidelity; they roughly double the property count of each model.
173
- - Search parameters are typed as strings (except `_count`), because FHIR search values carry prefixes and modifiers (`ge2021-01-01`, `code:below=...`) that stricter types would reject. The FHIR type is preserved in `x-fhir-search-type`.
309
+ - Search parameters are typed as strings (except `_count`), because FHIR search values carry prefixes and modifiers (`ge2021-01-01`, `code:below=...`) that stricter types would reject. The FHIR type, accepted codes and accepted prefixes are surfaced as metadata instead — see [Search parameters](#search-parameters).
310
+ - Search _modifiers_ (`:exact`, `:contains`, `:not`, `:missing`, `:below`, ...) are not emitted as separate parameters; only the base parameter name is. Chained (`subject.name`) and composite-on-the-fly parameters are likewise not enumerated beyond those the specification defines.
174
311
  - Operations are resource-scoped only: system-level operations (`GET /$export`-style, `$convert`, ...), `POST /_search`, batch/transaction semantics, and conditional headers beyond `If-Match` are not modeled. Multi-part operation parameters collapse to a generic `Parameters` body.
175
- - `--capability` reflects a server's *declared* surface: resource types the FHIR version doesn't define are skipped, and declared operations are emitted only when they map to a known OperationDefinition (system-level interactions like transaction/batch are not modeled). It restricts what's generated; it does not verify the server actually behaves as declared.
312
+ - `--capability` reflects a server's _declared_ surface: resource types the FHIR version doesn't define are skipped, and declared operations are emitted only when they map to a known OperationDefinition (system-level interactions like transaction/batch are not modeled). It restricts what's generated; it does not verify the server actually behaves as declared.
176
313
  - The two definition backends can differ cosmetically (e.g. naming of deeply nested backbone elements, primitive regex patterns); `schema-json` is the default and the reference.
314
+ - They also differ on one thing that is **not** cosmetic: **mandatory primitive elements**. `structure-def` marks them `required` (the StructureDefinition says `min: 1`), while `schema-json` does not, because the official `fhir.schema.json` omits them — a FHIR primitive may legitimately appear as only its `_element` extension sibling (e.g. carrying a `dataAbsentReason`), so the JSON property itself is not strictly required. `Observation.status` is the canonical example: required under `structure-def`, optional under `schema-json`. Mandatory _complex_ elements (`Observation.code`) are required under both. Pick `structure-def` if you want stricter generated models.
177
315
 
178
316
  ## Development
179
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-openapi-translator",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Generate an OpenAPI (Swagger) spec for any FHIR resource (R4/R4B/R5) — for typed model & client codegen in any language. Supports US Core profiles, operations, and CapabilityStatements.",
5
5
  "keywords": [
6
6
  "fhir",
@@ -44,12 +44,14 @@
44
44
  ".": {
45
45
  "types": "./dist/index.d.ts",
46
46
  "import": "./dist/index.js"
47
- }
47
+ },
48
+ "./package.json": "./package.json"
48
49
  },
49
50
  "files": [
50
51
  "dist",
51
52
  "definitions",
52
- "docs"
53
+ "docs",
54
+ "CHANGELOG.md"
53
55
  ],
54
56
  "scripts": {
55
57
  "build": "tsup",