fhirsmith 0.12.0 → 0.13.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 +305 -0
- package/README.md +1 -1
- package/config-template.json +13 -0
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +60 -30
- package/publisher/validation.js +15 -3
- package/server.js +12 -3
- package/translations/Messages.properties +1 -0
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-icd11.js +805 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +38 -4
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/sct/expressions.js +194 -17
- package/tx/tx.fhir.org.yml +2 -1
- package/tx/tx.js +15 -42
- package/tx/workers/batch-validate.js +6 -16
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +6 -12
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +36 -45
- package/tx/workers/lookup.js +21 -39
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +11 -17
- package/tx/workers/validate.js +76 -77
- package/tx/workers/worker.js +70 -1
- package/tx/xml/xml-base.js +243 -76
- package/tx/xversion/xv-valueset.js +39 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,76 @@ All notable changes to the Health Intersections Node Server will be documented i
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- ICD-11 support, in three parts. `tx/importers/import-icd11.js` builds a SQLite database from
|
|
13
|
+
the WHO ICD-API - 110,441 concepts across MMS, ICF and the Foundation for the 2026-01 release,
|
|
14
|
+
in every language the API advertises. The native API is the content source because it is the
|
|
15
|
+
only one that can enumerate the classification: the FHIR endpoint exposes no all-codes value
|
|
16
|
+
set, accepts no client-supplied value sets, and its 28 top-level entities report no parent, so
|
|
17
|
+
nothing there tells a client what the roots are. `tx/importers/export-icd11-codesystem.js`
|
|
18
|
+
writes the content back out as FHIR CodeSystems, language supplements and the 25,354
|
|
19
|
+
postcoordination scale ValueSets, optionally as a FHIR NPM package. `tx/cs/cs-icd11.js` is the
|
|
20
|
+
provider; it is a skeleton at this point - metadata, code location, displays and definitions
|
|
21
|
+
work, while designations, properties, extendLookup, filters, iteration, subsumption and
|
|
22
|
+
postcoordinated expressions are still to come. The `icd-11` suite from the tx-ecosystem IG is
|
|
23
|
+
the specification for the rest of it. Postcoordinated expressions now work:
|
|
24
|
+
`tx/cs/icd11-expressions.js` parses the short-code, entity-uri and ICF dotted forms,
|
|
25
|
+
binds each value to the axis it belongs on, and renders the expression back. The binding
|
|
26
|
+
rule is that an axis not yet carrying a value is preferred over one that is, and required
|
|
27
|
+
axes come before WHO's declaration order - which is what keeps the two values of
|
|
28
|
+
1D01.0Y/1G41/1G40 on the two axes a coder meant them for instead of coalescing them onto
|
|
29
|
+
the first, and what stops a repeated value being silently dropped. `&` asserts that what
|
|
30
|
+
follows is a value on an axis of the stem, so a value on none of them is an error that
|
|
31
|
+
names the axes; `/` asserts only cluster membership, so a member that fits an unfilled
|
|
32
|
+
axis is taken as a value and one that does not starts a new stem. The provider also now
|
|
33
|
+
answers `concept is-a` / `descendent-of` filters and text search, iterates children and
|
|
34
|
+
roots, tests subsumption, and builds the 25,354 postcoordination scale value sets on
|
|
35
|
+
demand from the database rather than shipping them
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- The tx test runner now names its own output folder (`fhirsmith`) and labels each pass (`r4`,
|
|
40
|
+
`r5`, `r5-cached`), so the three passes stop writing over each other's expected/actual files.
|
|
41
|
+
It also passes its mode set to the validator, which was previously falling back to its own
|
|
42
|
+
default - one that does not include `icd-11`, so every icd-11 test came back "n/a" and was
|
|
43
|
+
counted as a failure. Needs validator 6.10.5 and validator-wrapper 1.4
|
|
44
|
+
- The R5 -> R4 cross-version conversion dropped `ValueSet.compose.property` - the element a
|
|
45
|
+
client uses to say which properties it wants back in an expansion. R4 and R3 have nowhere to
|
|
46
|
+
put it, so it now travels as
|
|
47
|
+
`http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.property` (the same
|
|
48
|
+
extension the Java convertors use) and is read back on the way up, with the extension removed
|
|
49
|
+
so it does not show up twice. Before this, an R4 client that asked for properties got an
|
|
50
|
+
expansion without them
|
|
51
|
+
- Every OperationOutcome the server emits now carries `details.text` and a tx-issue-type
|
|
52
|
+
coding in `details.coding`. `diagnostics` is stripped outright by the test harness, so
|
|
53
|
+
anything a client needs had to stop living *only* there -- it is still sent, and is still
|
|
54
|
+
the right place for server-specific detail: the four (in fact nine) private
|
|
55
|
+
`operationOutcome()` helpers that built `{severity, code, diagnostics}` by hand now all
|
|
56
|
+
delegate to `buildOperationOutcome()` in `tx/library/operation-outcome.js`, and
|
|
57
|
+
`Issue.asIssue()` falls back to a tx-issue-type derived from the FHIR issue type when an
|
|
58
|
+
Issue does not name one, so the guarantee holds without revisiting all ~170 Issue
|
|
59
|
+
construction sites. An informational issue that is not about a code or a value set -- the
|
|
60
|
+
server banner -- gets text but no coding, since it is not a tx issue
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- The exclude branch of `$expand` called `searchFilter(filter, prep, true)` -- the first two
|
|
65
|
+
arguments the wrong way round, so a provider that implements a text search would have been
|
|
66
|
+
handed the filter where it expected its own filter context. The other three call sites,
|
|
67
|
+
and the declared signature, are `(filterContext, filter, sort)`
|
|
68
|
+
- `$lookup` reported a code that is not in the code system as `not-found`; the code system
|
|
69
|
+
was found, so it is `invalid-code`, which is what the other 62 unknown-code expectations
|
|
70
|
+
in the tx-ecosystem suite assert
|
|
71
|
+
- `$lookup`'s two top-level catch blocks put `Issue.issueCode` -- a tx-issue-type -- into
|
|
72
|
+
`OperationOutcome.issue.code`, which takes a FHIR issue type. Two conventions were in use:
|
|
73
|
+
an `Issue` carries the FHIR issue type in `cause` and the tx-issue-type in `issueCode`,
|
|
74
|
+
while a plain Error tagged by its thrower carries the FHIR issue type in `issueCode` and
|
|
75
|
+
the tx-issue-type in `txIssueType`. Every catch block now goes through
|
|
76
|
+
`outcomeFromError()`, which is the one place that knows about both
|
|
77
|
+
|
|
8
78
|
## [0.12.0] - 2026-08-27
|
|
9
79
|
|
|
10
80
|
### Added
|
|
@@ -61,6 +131,153 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
61
131
|
|
|
62
132
|
### Fixed
|
|
63
133
|
|
|
134
|
+
- The XML parser could be made to loop forever, which on a single-threaded server means the
|
|
135
|
+
process stops answering anything at all - not just the request that carried the input. Several
|
|
136
|
+
scans used `indexOf` without checking for -1 and assigned the result to the cursor, sending it
|
|
137
|
+
backwards; `<ValueSet><x bare></x></ValueSet>` was enough, as was any `<!DOCTYPE` or an
|
|
138
|
+
unterminated comment. The parser has been rewritten to be total: every delimiter search fails
|
|
139
|
+
loudly, every loop asserts that it consumed input, nesting past 1000 elements is an error rather
|
|
140
|
+
than a stack overflow, and a document type declaration is refused outright (FHIR XML has no DTD,
|
|
141
|
+
and accepting one is how a parser ends up expanding entities on someone else's behalf).
|
|
142
|
+
Malformed documents now produce a positioned parse error
|
|
143
|
+
- SNOMED CT: normalising an expression discarded a primitive concept's defining attributes, so
|
|
144
|
+
real subsumption answers were lost. A primitive's definition is necessary but not sufficient, so
|
|
145
|
+
the concept itself must not be replaced by it - but its attributes are still entailed:
|
|
146
|
+
`4846001 |Anicteric viral hepatitis|` is primitive and carries an inferred
|
|
147
|
+
`363698007 |Finding site| = 10200004 |Liver structure|`, so `64572001:{363698007=10200004}`
|
|
148
|
+
really does subsume it, and the server said not-subsumed. The attributes are now added
|
|
149
|
+
alongside the concept, which keeps the other direction sound - the primitive stays in the focus
|
|
150
|
+
as an atom nothing else can match, so its conditions never become sufficient and nothing is
|
|
151
|
+
reported equivalent to it. Read from the concept's relationships at query time rather than from
|
|
152
|
+
the stored normal form, so existing caches need no rebuild
|
|
153
|
+
- IETF language codes (`urn:ietf:bcp:47`) are validated properly. Four things were wrong, all
|
|
154
|
+
visible only through the code system: the registry writes private-use allocations as ranges
|
|
155
|
+
(`QM..QZ`, `Qaaa..Qabx`, `qaa..qtz`, `XA..XZ`) and those were stored as literal keys nobody
|
|
156
|
+
could ever look up, so every code inside them read as unregistered while singly-allocated `AA`
|
|
157
|
+
worked; the 26 grandfathered tags were skipped at load, so registered tags like `i-klingon`
|
|
158
|
+
were rejected; a variant was never checked against the registry at all, so `en-abcdef` parsed
|
|
159
|
+
happily; and the registry's `Prefix` field - its only statement about which subtags may go
|
|
160
|
+
together - was ignored, so `en-cmn` passed even though `cmn` may only follow `zh`. Tags are
|
|
161
|
+
also matched case-insensitively now, per BCP 47 §2.1.1, so `en-us` is accepted as a way of
|
|
162
|
+
writing `en-US`. The stricter checks are options on the parser, applied by the code system;
|
|
163
|
+
Accept-Language parsing is deliberately left as it was. Redundant registry entries such as
|
|
164
|
+
`zh-Hans` still decompose into language plus script rather than being matched whole
|
|
165
|
+
- Language codes gained `=` filters on `language`, `script` and `region`, and three of the
|
|
166
|
+
combinations can now be expanded. The BCP 47 grammar is unbounded, so an expansion is only
|
|
167
|
+
possible where fixing part of a tag leaves a finite list of registry entries to vary: a fixed
|
|
168
|
+
language gives every region plus the bare language (344 codes), a fixed region gives every
|
|
169
|
+
language with it (8787, so too-costly unless paged), and language and region together give
|
|
170
|
+
every script plus the tag with none (275). Anything else - a script on its own, a language with
|
|
171
|
+
a script, an `exists` filter - answers 422 rather than pretending, and `filterMore` throws
|
|
172
|
+
rather than returning false so a mistake cannot surface as an empty expansion that reads like
|
|
173
|
+
"there are none". Every one of these is marked unclosed, since a variant, extension or
|
|
174
|
+
private-use subtag can always be added. A filter value that is not a registered subtag of the
|
|
175
|
+
right kind is refused when the filter is built, rather than silently selecting nothing
|
|
176
|
+
- `$subsumes` works for language codes, where it used to answer `not-subsumed` for everything
|
|
177
|
+
behind a comment reading "No subsumption in language codes". A tag is a set of named
|
|
178
|
+
components rather than an ordered path, so one tag subsumes another when it states a subset of
|
|
179
|
+
what the other states and the languages match: `en` subsumes `en-US`, `zh` subsumes
|
|
180
|
+
`zh-Hans-CN`, `de` subsumes `de-1901`. `en-US` subsumes `en-Latn-US` too, even though the added
|
|
181
|
+
script sits between the two subtags `en-US` names - that is RFC 4647's extended filtering
|
|
182
|
+
rather than basic filtering, and it is what keeps the relation transitive. Comparison is
|
|
183
|
+
case-insensitive, and a grandfathered tag relates only to itself, having no components to
|
|
184
|
+
compare. Suppress-Script is deliberately not consulted, so `en` subsumes `en-Latn` rather than
|
|
185
|
+
being equivalent to it
|
|
186
|
+
- A language code written in the wrong case now comes back with a `normalized-code` and an
|
|
187
|
+
information issue saying so, the way any other case-insensitive code system already behaved -
|
|
188
|
+
`en-us` validates, and the server answers with `en-US`. BCP 47 recommends lower case for the
|
|
189
|
+
language, Titlecase for the script and UPPER CASE for the region; the whole mechanism was
|
|
190
|
+
already in the validator, waiting on the code system to report the canonical spelling of a code
|
|
191
|
+
rather than echoing what the caller wrote
|
|
192
|
+
- Validating a language code now says which subtag is wrong and why - "the extLanguage 'cmn' in
|
|
193
|
+
the code 'en-cmn' may only be used with 'zh'" - instead of only "unknown code". The mechanism
|
|
194
|
+
for this already existed in the validator; the language provider had been discarding the reason
|
|
195
|
+
- `$subsumes` can now report that it cannot determine the relationship, as a 422 carrying
|
|
196
|
+
`cannot-determine` from the tx-issue-type code system (FHIR-58748). There is no outcome code
|
|
197
|
+
meaning "unknown", so a server that cannot decide has no honest answer to give and must report
|
|
198
|
+
an error; answering `not-subsumed` asserts something it has not established. The condition is
|
|
199
|
+
real in more than one code system, so the error is raised through one shared helper
|
|
200
|
+
(`cannotDetermineSubsumption`)
|
|
201
|
+
- Media types: subsumption is only decided on parameters the server understands - `charset`,
|
|
202
|
+
`format`, `delsp` and `version`. Reasoning that a parameter narrows the type, and that two of
|
|
203
|
+
its values exclude one another, are properties of that parameter's definition, and neither can
|
|
204
|
+
be asserted for one the server has never seen: `text/plain` vs `text/plain; foo=bar` might be
|
|
205
|
+
subsumption or might be the same thing. Where an unknown parameter is what the two codes differ
|
|
206
|
+
in, the answer is now cannot-determine. A parameter carried identically by both codes cannot
|
|
207
|
+
affect the relationship and is ignored, so it does not trigger a decline, and a type or subtype
|
|
208
|
+
mismatch stays decidable whatever parameters are present. `boundary` and `profile` are
|
|
209
|
+
deliberately excluded from the understood list: a differing boundary does not narrow anything
|
|
210
|
+
(two multipart bodies with different boundaries are the same media type) and profile values can
|
|
211
|
+
be hierarchical, so neither can be judged by comparing values
|
|
212
|
+
|
|
213
|
+
- SNOMED CT: `$subsumes` no longer answers `not-subsumed` when it has not established it. Comparing
|
|
214
|
+
normal forms is sound when it succeeds and not when it fails: SNOMED's OWL axiom reference set
|
|
215
|
+
carries general concept inclusions and property chains whose entailments are baked into the
|
|
216
|
+
distributed inferred relationships for precoordinated concepts, but a postcoordinated expression
|
|
217
|
+
is one the classifier never saw, so they are not available for it. Where either code is an
|
|
218
|
+
expression and no relationship follows structurally, the server now returns a 422 rather than
|
|
219
|
+
claiming there is none - the claim would be the one that wrongly drops a code out of a cohort,
|
|
220
|
+
and `$subsumes` has no outcome code meaning "unknown". Two plain concepts are unaffected: there
|
|
221
|
+
the transitive closure comes from the classifier's own output and all four outcomes are sound
|
|
222
|
+
|
|
223
|
+
- SNOMED CT: the MRCM `grouped` flag was parsed into the attribute domain rules and then never
|
|
224
|
+
read, so an attribute the concept model requires to be in a relationship group could be written
|
|
225
|
+
outside one and pass validation - `40468003:363698007=10200004` was accepted even though
|
|
226
|
+
`363698007 |Finding site|` is grouped and the expression is not classifiable. It is now
|
|
227
|
+
enforced, for expressions wherever they appear ($validate-code, $lookup and $subsumes all
|
|
228
|
+
validate through the same path), and the message names the attribute and shows the corrected
|
|
229
|
+
form. Attributes the MRCM marks as ungrouped, such as `272741003 |Laterality|`, are unaffected:
|
|
230
|
+
the flag is read from the concept model rather than every brace-less refinement being rejected.
|
|
231
|
+
SNOMED's postcoordination guide treats the ungrouped form as Close To User Form and defines a
|
|
232
|
+
transformation to the classifiable form; that transformation is not implemented, so the
|
|
233
|
+
expression is rejected rather than silently repaired
|
|
234
|
+
- Publisher: with no `sessionSecret` configured, the session middleware fell back to a constant
|
|
235
|
+
written into the source. In an open-source project that is a published signing key - anyone
|
|
236
|
+
could mint themselves an admin session on any deployment that had missed the setting, and
|
|
237
|
+
nothing about the server would look wrong. The fallback is now a random secret plus a warning,
|
|
238
|
+
which fails safe: logins simply do not survive a restart. Session cookies are also marked
|
|
239
|
+
`Secure` by default now (set `modules.publisher.cookieSecure` false to run over plain HTTP),
|
|
240
|
+
and `POST /login` is rate limited - it is the one unauthenticated route that runs bcrypt, and
|
|
241
|
+
bcrypt holds a libuv threadpool thread for ~100ms, so unlimited attempts stall the server's
|
|
242
|
+
file I/O as well as inviting guessing
|
|
243
|
+
- Publisher: twenty more values reached the task, history and admin pages unescaped -
|
|
244
|
+
`npm_package_id`, `version`, `status`, `user_name`, `website_name`, `local_folder`,
|
|
245
|
+
`failure_reason`, the task log messages, and the user and website names on the admin pages.
|
|
246
|
+
Most are constrained elsewhere, but `failure_reason` and the log messages carry build output
|
|
247
|
+
from the IG being published, which is not. The earlier fix covered `github_org`, `github_repo`
|
|
248
|
+
and `git_branch` only; output escaping is the boundary that has to be complete to be worth
|
|
249
|
+
anything
|
|
250
|
+
- The static file handler for `server.webBase` built its filename with `path.join(webBase, req.path)`
|
|
251
|
+
and no containment check, so a literal `..` in the request path walked straight out of the
|
|
252
|
+
directory: `GET /../../etc/passwd` read any file the server user could read. (The encoded forms
|
|
253
|
+
never worked - Express does not decode `req.path` - and a proxy in front normalises `..` away,
|
|
254
|
+
which is why this survived.) Paths are now resolved and required to stay inside the configured
|
|
255
|
+
directory, in `library/path-safety.js` so the rule is testable and reusable; a request that
|
|
256
|
+
escapes falls through to the next handler instead of being served
|
|
257
|
+
- `POST /packages/crawl` triggered a full registry crawl for anyone who asked. It now requires a
|
|
258
|
+
shared secret, `modules.packages.crawlToken`, in an `x-crawl-token` header, compared in constant
|
|
259
|
+
time. With no token configured the endpoint is disabled rather than open: an administrative
|
|
260
|
+
trigger that silently defaults to unauthenticated is how a deployment ends up exposed with
|
|
261
|
+
nothing about it looking wrong. The scheduled crawler is unaffected
|
|
262
|
+
|
|
263
|
+
- `CodeSystem.concept` and `ValueSet.expansion.contains` nest without limit in FHIR, and both are
|
|
264
|
+
walked recursively in a dozen places - the expansion map, the concept counts, the renderer, the
|
|
265
|
+
expander's index, the R5-to-R4 converter. A resource of a couple of hundred KB could nest deeply
|
|
266
|
+
enough to exhaust the JS stack, and hardening those walkers one at a time would not have been
|
|
267
|
+
enough anyway: `JSON.stringify` itself, which we do not control, overflows at a nesting depth of
|
|
268
|
+
about 1800 on node 22, so a deep enough resource could not be serialised even if every walker
|
|
269
|
+
were iterative. Nesting is now bounded once, at construction, at 100 levels - far past SNOMED
|
|
270
|
+
CT's ~30 or LOINC's ~10, and well clear of anything that breaks - which makes every walker safe
|
|
271
|
+
by construction, including ones not yet written. The bounding walk is iterative and doubles as
|
|
272
|
+
the concept count, so the tree is still traversed only once, and an over-nested resource comes
|
|
273
|
+
back as a 400 rather than a 500
|
|
274
|
+
|
|
275
|
+
- Publisher: the task output page wrote `github_org`, `github_repo` and `git_branch` into the HTML
|
|
276
|
+
without escaping. The first two are constrained to letters, digits and punctuation, but a branch
|
|
277
|
+
name was only checked against git's own rules, which say nothing about markup - so a branch
|
|
278
|
+
called `<script>...</script>` was stored XSS on a page that needs no login to read. The fields
|
|
279
|
+
are escaped now, and a branch name carrying `< > " ' &` is refused at the boundary as well
|
|
280
|
+
|
|
64
281
|
- `$expand`: `status` was lost from imported property declarations, and a concept carrying the
|
|
65
282
|
same property more than once had the repeats collapsed to a single value
|
|
66
283
|
- Expansion properties are de-duplicated when they arrive from more than one place (the request,
|
|
@@ -70,6 +287,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
70
287
|
the field the cache key hashes
|
|
71
288
|
- Boolean parameters passed as strings (as they always are on a GET) are now accepted, which
|
|
72
289
|
revives five parameters that were dead on GET requests
|
|
290
|
+
- `$subsumes`: an unknown `codeA`/`codeB` returned a bare OperationOutcome - no
|
|
291
|
+
`operationoutcome-message-id` extension and no `tx-issue-type#invalid-code` detail code - so
|
|
292
|
+
clients could not tell what kind of failure it was. It now reports the same issue that
|
|
293
|
+
`$validate-code` does for an unknown code
|
|
294
|
+
- SNOMED CT import did not record definition status, so every concept looked fully defined. A
|
|
295
|
+
normal form was then generated for concepts that have no definition to expand, and the result
|
|
296
|
+
was stored unparseable - which made `$subsumes` fail with a 500 for any post-coordinated
|
|
297
|
+
expression. **Caches built by the affected importer have to be rebuilt**
|
|
298
|
+
- SNOMED CT `isPrimitive` tested bit 0 of the concept flags, which is part of the status, instead
|
|
299
|
+
of the primitive bit
|
|
300
|
+
- A concept's own stored normal form was run through the MRCM postcoordination checks when it was
|
|
301
|
+
read back. Those rules are about expressions a client sends, not about precoordinated content,
|
|
302
|
+
and they rejected the normal form of any concept defined with a precoordinated-only attribute
|
|
303
|
+
- The expression parser did not resolve attribute names to concept references, so refinements were
|
|
304
|
+
compared by reference in one direction and by code in the other. Subsumption between two refined
|
|
305
|
+
expressions could come out true one way and false the other
|
|
306
|
+
- Subsumption checked each refinement group against only the first group whose attribute names
|
|
307
|
+
lined up. Where several groups share attribute names, the answer depended on the order the
|
|
308
|
+
groups happened to be in after normalisation
|
|
309
|
+
- `$subsumes` reported an unexpected error in `diagnostics`, which the test runner strips from
|
|
310
|
+
every issue it compares, so a failing test showed nothing but `severity`/`code`. The message now
|
|
311
|
+
goes in `details.text` (the same helper elsewhere in the server has the same problem)
|
|
312
|
+
- `$subsumes` ignored a front-loaded cache on a GET: it only looked for `tx-resource` and the
|
|
313
|
+
cache-id when the request had a Parameters body, so a client that put its code systems in a
|
|
314
|
+
server-side cache and sent the id in `X-Cache-Id` got "could not be found" for every one of
|
|
315
|
+
them
|
|
316
|
+
- `$subsumes` reported an unknown code against `codeA`/`codeB` in `expression` even when the code
|
|
317
|
+
came in as `codingA`/`codingB`
|
|
318
|
+
|
|
319
|
+
### Changed
|
|
320
|
+
|
|
321
|
+
- `$subsumes` on UCUM now answers `equivalent` for two different codes that mean the same unit -
|
|
322
|
+
`1/min` and `min-1`, `N` and `kg.m/s2`, `mg{total}` and `mg`. UCUM still has no hierarchy, so
|
|
323
|
+
nothing ever subsumes anything; this compares the canonical *form* (which carries the conversion
|
|
324
|
+
factor), not the canonical units, so `m` and `cm` remain distinct rather than collapsing onto
|
|
325
|
+
their shared canonical unit
|
|
326
|
+
- `$subsumes` on LOINC is implemented against the multiaxial hierarchy. The importer already
|
|
327
|
+
builds a full transitive closure from `PATH_TO_ROOT` in ComponentHierarchyBySystem - the same
|
|
328
|
+
table the `is-a` / `descendent-of` filters use - so subsumption and expansion now agree.
|
|
329
|
+
Relationships run Part-to-Part and Part-to-code, so two LOINC codes still never subsume each
|
|
330
|
+
other
|
|
331
|
+
- Mime types support a `base` filter, taking either `type` or `type/subtype`, which selects the
|
|
332
|
+
codes consistent with it - parameters on the code are ignored, so `text/plain; charset=utf-8`
|
|
333
|
+
matches a base of `text` and of `text/plain`. Media types cannot be enumerated, so the filter
|
|
334
|
+
validates a code rather than expanding; a value carrying parameters, a wildcard, or anything
|
|
335
|
+
that is not a type or type/subtype is rejected
|
|
336
|
+
- A `registered = true` mime type filter can be expanded: the IANA registry is a finite list, so
|
|
337
|
+
the expander walks it, and the expansion is marked `valueset-unclosed` because every registered
|
|
338
|
+
type also has unboundedly many parameterised forms that belong to the value set. A second filter
|
|
339
|
+
in the same include still narrows it, so `registered = true` + `base = text/plain` expands to
|
|
340
|
+
one code
|
|
341
|
+
- Everything else that cannot be enumerated - the whole code system, a `base` filter on its own,
|
|
342
|
+
`registered = false` - answers 422 `not-supported` with `CODESYSTEM_NOT_ENUMERABLE`, the same as
|
|
343
|
+
HGVS, instead of the 500 exception a filter used to raise. Expanding every registered media type
|
|
344
|
+
at once is a couple of thousand codes, so without a `count` it answers `too-costly` like any
|
|
345
|
+
other oversized expansion
|
|
346
|
+
- Mime types support a `registered` filter (`true`/`false`), testing whether a media type is in
|
|
347
|
+
the IANA registry. The provider downloads
|
|
348
|
+
https://www.iana.org/assignments/media-types/media-types.xml into the terminology cache at
|
|
349
|
+
startup and keeps only the names; a failed download falls back to the cached copy, and if there
|
|
350
|
+
is no copy at all, using the filter reports that rather than answering. The download is written
|
|
351
|
+
to a temporary file and only replaces the cached copy once it has parsed, so a truncated
|
|
352
|
+
transfer cannot leave behind a registry that looks valid but is short
|
|
353
|
+
- `$subsumes` on mime types understands parameters. A parameter narrows the media type, so
|
|
354
|
+
`text/plain` subsumes `text/plain; charset=utf-8`, and two spellings of the same type are
|
|
355
|
+
equivalent - type, subtype and parameter names are matched case-insensitively, as are charset
|
|
356
|
+
values, and quoted values are unquoted. Type and subtype themselves still have no hierarchy,
|
|
357
|
+
so a structured syntax suffix does not make `application/fhir+xml` a kind of `application/xml`
|
|
358
|
+
- **Breaking (LOINC import):** the accessory files are now required, not optional. An import
|
|
359
|
+
missing ComponentHierarchyBySystem previously succeeded and produced a cache with an empty
|
|
360
|
+
closure, so the server answered `not-subsumed` to everything with no way to tell that apart
|
|
361
|
+
from a real answer; the same argument applies to the part links and answer lists. Only the
|
|
362
|
+
linguistic variants stay optional, since a main-only import is a reasonable thing to want and
|
|
363
|
+
their absence costs designations rather than correctness
|
|
364
|
+
|
|
365
|
+
- The mime type tests are their own test mode (`mimetypes`) rather than part of `general`, and
|
|
366
|
+
FHIRsmith's test run declares it. fhir-core gains it in all four places the mode set is
|
|
367
|
+
declared: the runner default (`TxTestHTTPHandler`) and the local and external terminology
|
|
368
|
+
service test runs, which will be red until tx-dev implements the filters
|
|
369
|
+
|
|
370
|
+
### Removed
|
|
371
|
+
|
|
372
|
+
- `POST /packages/update-package`, along with `forceUpdatePackage` and
|
|
373
|
+
`deleteVersionsByIdVersion`. Its authentication was conditional on `updateToken`, which was set
|
|
374
|
+
in no configuration file and not even mentioned in the template, so in practice the endpoint let
|
|
375
|
+
anyone hand the server a URL to fetch and store - server-side request forgery against anything
|
|
376
|
+
the host can reach, and a write into the package registry. It existed to push out a corrected
|
|
377
|
+
package after a bad publish; that is rare enough to do by hand
|
|
73
378
|
|
|
74
379
|
### Tx Conformance Statement
|
|
75
380
|
|
package/README.md
CHANGED
|
@@ -283,7 +283,7 @@ GitHub Actions will automatically:
|
|
|
283
283
|
### Tx Conformance Statement
|
|
284
284
|
{copy content from text-cases-summary.txt}
|
|
285
285
|
```
|
|
286
|
-
2. Update `package.json` to have the same release version
|
|
286
|
+
2. Update `package.json` & `package-lock.json` to have the same release version
|
|
287
287
|
|
|
288
288
|
3. Commit your changes:
|
|
289
289
|
```bash
|
package/config-template.json
CHANGED
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
"enabled": false,
|
|
59
59
|
"database": "/absolute/path/to/packages.db",
|
|
60
60
|
"mirrorPath": "/absolute/path/to/mirror",
|
|
61
|
+
// shared secret for POST /packages/crawl, which triggers a full registry crawl on
|
|
62
|
+
// demand. Send it in the x-crawl-token header. Omit this and the endpoint is
|
|
63
|
+
// disabled entirely - the scheduled crawler below is unaffected either way.
|
|
64
|
+
"crawlToken": "change-this-to-a-secure-random-string",
|
|
61
65
|
"crawler": {
|
|
62
66
|
"enabled": true,
|
|
63
67
|
"schedule": "0 * * * *"
|
|
@@ -74,7 +78,16 @@
|
|
|
74
78
|
"publisher": {
|
|
75
79
|
"enabled": true,
|
|
76
80
|
"database": "/absolute/path/to/publisher.db",
|
|
81
|
+
// required. Without it the publisher generates a random secret at startup, logs a
|
|
82
|
+
// warning, and logins stop working across restarts.
|
|
77
83
|
"sessionSecret": "change-this-to-a-secure-random-string",
|
|
84
|
+
// session cookies are marked Secure by default, so they only travel over HTTPS.
|
|
85
|
+
// Set false only to run the publisher over plain HTTP - otherwise the browser will
|
|
86
|
+
// not send the cookie back and logins will appear to do nothing. Behind a TLS
|
|
87
|
+
// proxy, make sure it sets X-Forwarded-Proto.
|
|
88
|
+
"cookieSecure": true,
|
|
89
|
+
// failed-login attempts allowed per IP per 15 minutes (default 20)
|
|
90
|
+
"loginAttemptsPerWindow": 20,
|
|
78
91
|
"workspaceRoot": "/absolute/path/to/workspaces",
|
|
79
92
|
"igPublisherTimeoutMinutes": 60,
|
|
80
93
|
// where to put files the IG Publisher leaves in the web folder that are too big for GitHub
|