fhirsmith 0.14.1 → 0.15.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 +58 -0
- package/README.md +3 -2
- package/config-template.json +9 -0
- package/library/changelog.js +57 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/utilities.js +21 -0
- package/package.json +10 -4
- package/packages/openapi.js +13 -0
- package/packages/openapi.yaml +603 -0
- package/packages/packages-template.html +4 -2
- package/packages/packages.js +286 -81
- package/packages/readme.md +120 -0
- package/registry/api.js +117 -41
- package/registry/crawler.js +41 -0
- package/registry/fhirsmith-releases.js +167 -0
- package/registry/model.js +13 -0
- package/registry/openapi.js +13 -0
- package/registry/openapi.yaml +311 -0
- package/registry/readme.md +107 -208
- package/registry/registry-template.html +4 -1
- package/registry/registry.js +198 -19
- package/server.js +19 -5
- package/testing/openapi-schemas.config.js +58 -0
- package/testing/openapi-schemas.json +1920 -0
- package/testing/openapi.js +15 -0
- package/testing/openapi.yaml +273 -0
- package/testing/readme.md +43 -1
- package/testing/search.js +1 -1
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +24 -0
- package/translations/Messages.properties +1 -0
- package/tx/README.md +78 -4
- package/tx/cm/cm-api.js +25 -10
- package/tx/cm/cm-database.js +10 -5
- package/tx/cm/cm-package.js +61 -15
- package/tx/cs/cs-provider-api.js +22 -7
- package/tx/cs/cs-provider-list.js +10 -9
- package/tx/cs/cs-rxnorm.js +23 -6
- package/tx/html/tx-template.html +2 -0
- package/tx/library/canonical-resource.js +50 -1
- package/tx/library/codesystem.js +1 -0
- package/tx/library/conceptmap.js +1 -0
- package/tx/library/resource-ids.js +107 -0
- package/tx/library/valueset.js +16 -0
- package/tx/library.js +113 -26
- package/tx/ocl/cm-ocl.cjs +23 -15
- package/tx/ocl/cs-ocl.cjs +9 -10
- package/tx/ocl/vs-ocl.cjs +17 -17
- package/tx/openapi-operations.js +363 -0
- package/tx/openapi-schemas.config.js +118 -0
- package/tx/openapi-schemas.json +7903 -0
- package/tx/openapi.js +289 -0
- package/tx/openapi.yaml +151 -0
- package/tx/operation-context.js +15 -0
- package/tx/params.js +3 -12
- package/tx/provider.js +90 -14
- package/tx/tx-html.js +6 -0
- package/tx/tx.fhir.org.yml +16 -8
- package/tx/tx.js +62 -1
- package/tx/vs/vs-api.js +25 -10
- package/tx/vs/vs-database.js +14 -9
- package/tx/vs/vs-package.js +58 -31
- package/tx/vs/vs-vsac.js +20 -5
- package/tx/workers/expand.js +35 -16
- package/tx/workers/metadata.js +2 -14
- package/tx/workers/read.js +3 -18
- package/tx/workers/validate.js +65 -19
- package/tx/workers/worker.js +29 -8
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- package/xig/xig.js +2 -4
package/tx/workers/validate.js
CHANGED
|
@@ -285,7 +285,10 @@ class ValueSetChecker {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
seeValueSet() {
|
|
288
|
-
this.
|
|
288
|
+
const key = this.valueSet.contextKey !== undefined ? this.valueSet.contextKey : this.valueSet.vurl;
|
|
289
|
+
if (key) {
|
|
290
|
+
this.worker.opContext.seeContext(key);
|
|
291
|
+
}
|
|
289
292
|
if (this.valueSet.jsonObj.compose && this.valueSet.jsonObj.compose.extension) {
|
|
290
293
|
for (let ext of this.valueSet.jsonObj.compose.extension) {
|
|
291
294
|
if (ext.url === 'http://hl7.org/fhir/StructureDefinition/valueset-expansion-parameter' || ext.url === 'http://hl7.org/fhir/tools/StructureDefinition/valueset-expansion-parameter') {
|
|
@@ -305,8 +308,22 @@ class ValueSetChecker {
|
|
|
305
308
|
async prepare() {
|
|
306
309
|
if (this.valueSet === null) {
|
|
307
310
|
throw new Issue('error', 'not-found', null, null, 'Error Error: vs = nil', null, 422);
|
|
308
|
-
}
|
|
309
|
-
|
|
311
|
+
}
|
|
312
|
+
// circular reference detection: this value set is in the chain being processed until
|
|
313
|
+
// it's prepared (which prepares everything it imports)
|
|
314
|
+
this.seeValueSet();
|
|
315
|
+
try {
|
|
316
|
+
await this.prepareSeen();
|
|
317
|
+
} finally {
|
|
318
|
+
const key = this.valueSet.contextKey !== undefined ? this.valueSet.contextKey : this.valueSet.vurl;
|
|
319
|
+
if (key) {
|
|
320
|
+
this.worker.opContext.unseeContext(key);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async prepareSeen() {
|
|
326
|
+
{
|
|
310
327
|
this.worker.opContext.addNote(this.valueSet, 'Analysing ' + this.valueSet.vurl + ' for validation purposes', this.indentCount);
|
|
311
328
|
if (this.indentCount === 0) {
|
|
312
329
|
this.worker.opContext.addNote(this.valueSet, 'Parameters: ' + this.params.summary(), this.indentCount);
|
|
@@ -569,9 +586,14 @@ class ValueSetChecker {
|
|
|
569
586
|
if (msg) {
|
|
570
587
|
op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note'));
|
|
571
588
|
}
|
|
572
|
-
inactive
|
|
573
|
-
inactive
|
|
574
|
-
|
|
589
|
+
// in a CodeableConcept a later coding must not clear the inactive status an earlier
|
|
590
|
+
// coding found, or the warning silently disappears when the inactive coding comes first
|
|
591
|
+
if (!inactive.value) {
|
|
592
|
+
inactive.value = await cs.isInactive(ctxt.context);
|
|
593
|
+
inactive.path = path;
|
|
594
|
+
inactive.code = code;
|
|
595
|
+
vstatus.value = await cs.getStatus(ctxt.context);
|
|
596
|
+
}
|
|
575
597
|
}
|
|
576
598
|
if (displays !== null) {
|
|
577
599
|
await this.worker.listDisplaysFromCodeSystem(displays, cs, ctxt.context);
|
|
@@ -925,6 +947,7 @@ class ValueSetChecker {
|
|
|
925
947
|
let pdisp;
|
|
926
948
|
let psys;
|
|
927
949
|
let pcode;
|
|
950
|
+
let ppath; // the path of the coding that is returned
|
|
928
951
|
let pver;
|
|
929
952
|
|
|
930
953
|
|
|
@@ -1063,6 +1086,7 @@ class ValueSetChecker {
|
|
|
1063
1086
|
if (pcode === undefined) {
|
|
1064
1087
|
psys = c.system;
|
|
1065
1088
|
pcode = c.code;
|
|
1089
|
+
ppath = path;
|
|
1066
1090
|
if (ver.value) {
|
|
1067
1091
|
pver = ver.value;
|
|
1068
1092
|
}
|
|
@@ -1185,6 +1209,7 @@ class ValueSetChecker {
|
|
|
1185
1209
|
if (!this.params.membershipOnly && !inactive.value && await prov.isInactive(ctxt.context)) {
|
|
1186
1210
|
inactive.value = true;
|
|
1187
1211
|
inactive.path = path;
|
|
1212
|
+
inactive.code = c.code;
|
|
1188
1213
|
const st = await prov.getStatus(ctxt.context);
|
|
1189
1214
|
if (st) {
|
|
1190
1215
|
vstatus.value = st;
|
|
@@ -1340,9 +1365,14 @@ class ValueSetChecker {
|
|
|
1340
1365
|
}
|
|
1341
1366
|
|
|
1342
1367
|
if (inactive.value) {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1368
|
+
// the inactive and status parameters describe the code being returned. In a CodeableConcept
|
|
1369
|
+
// the inactive coding may be a different one (or none was returned at all): it still gets
|
|
1370
|
+
// its warning below, but the parameters are not claimed for the returned code
|
|
1371
|
+
if (mode !== 'codeableConcept' || (pcode && inactive.path === ppath)) {
|
|
1372
|
+
result.addParamBool('inactive', inactive.value);
|
|
1373
|
+
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
1374
|
+
result.addParamCode('status', vstatus.value);
|
|
1375
|
+
}
|
|
1346
1376
|
}
|
|
1347
1377
|
let mpath = inactive.path;
|
|
1348
1378
|
if (!mpath) {
|
|
@@ -1354,7 +1384,9 @@ class ValueSetChecker {
|
|
|
1354
1384
|
mid1 = 'INACTIVE_CONCEPT_FOUND_ADD';
|
|
1355
1385
|
mm1 = 'inactive';
|
|
1356
1386
|
}
|
|
1357
|
-
|
|
1387
|
+
// report the code that is actually inactive - in a CodeableConcept that isn't necessarily
|
|
1388
|
+
// the first coding (tcode), which is what this message used to name
|
|
1389
|
+
let m = this.worker.i18n.translate(mid1, this.params.HTTPLanguages, [vstatus.value, inactive.code || tcode, mm1]);
|
|
1358
1390
|
msg(m);
|
|
1359
1391
|
op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
|
|
1360
1392
|
} else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
|
|
@@ -1548,6 +1580,7 @@ class ValueSetChecker {
|
|
|
1548
1580
|
if (!this.params.membershipOnly && role !== 'not in') {
|
|
1549
1581
|
inactive.value = true;
|
|
1550
1582
|
inactive.path = path;
|
|
1583
|
+
inactive.code = code;
|
|
1551
1584
|
if (inactive.value) {
|
|
1552
1585
|
vstatus.value = await cs.getStatus(loc.context);
|
|
1553
1586
|
}
|
|
@@ -1558,6 +1591,7 @@ class ValueSetChecker {
|
|
|
1558
1591
|
if (role !== 'not in') {
|
|
1559
1592
|
inactive.value = true;
|
|
1560
1593
|
inactive.path = path;
|
|
1594
|
+
inactive.code = code;
|
|
1561
1595
|
vstatus.value = await cs.getStatus(loc.context);
|
|
1562
1596
|
let msg = this.worker.i18n.translate('STATUS_CODE_WARNING_CODE', this.params.HTTPLanguages, ['not active', code]);
|
|
1563
1597
|
messages.push(msg);
|
|
@@ -1566,9 +1600,12 @@ class ValueSetChecker {
|
|
|
1566
1600
|
} else {
|
|
1567
1601
|
result = true;
|
|
1568
1602
|
if (role !== 'not in') {
|
|
1569
|
-
inactive.value
|
|
1570
|
-
|
|
1571
|
-
|
|
1603
|
+
if (!inactive.value) {
|
|
1604
|
+
inactive.value = await cs.isInactive(loc.context);
|
|
1605
|
+
inactive.path = path;
|
|
1606
|
+
inactive.code = code;
|
|
1607
|
+
vstatus.value = await cs.getStatus(loc.context);
|
|
1608
|
+
}
|
|
1572
1609
|
if (vcc !== null) {
|
|
1573
1610
|
if (!vcc.coding) {
|
|
1574
1611
|
vcc.coding = [];
|
|
@@ -1605,6 +1642,7 @@ class ValueSetChecker {
|
|
|
1605
1642
|
if (!this.params.membershipOnly) {
|
|
1606
1643
|
inactive.value = true;
|
|
1607
1644
|
inactive.path = path;
|
|
1645
|
+
inactive.code = code;
|
|
1608
1646
|
vstatus.value = await cs.getStatus(loc);
|
|
1609
1647
|
}
|
|
1610
1648
|
} else {
|
|
@@ -1618,9 +1656,12 @@ class ValueSetChecker {
|
|
|
1618
1656
|
} else if (Extensions.has(cc,'http://hl7.org/fhir/StructureDefinition/valueset-deprecated')) {
|
|
1619
1657
|
op.addIssue(new Issue('warning', 'business-rule', addToPath(path, 'code'), 'CONCEPT_DEPRECATED_IN_VALUESET', this.worker.i18n.translate('CONCEPT_DEPRECATED_IN_VALUESET', this.params.HTTPLanguages, [cs.system(), code, 'deprecated', vs.vurl]), 'code-comment'));
|
|
1620
1658
|
}
|
|
1621
|
-
inactive.value
|
|
1622
|
-
|
|
1623
|
-
|
|
1659
|
+
if (!inactive.value) {
|
|
1660
|
+
inactive.value = await cs.isInactive(loc);
|
|
1661
|
+
inactive.path = path;
|
|
1662
|
+
inactive.code = code;
|
|
1663
|
+
vstatus.value = await cs.getStatus(loc);
|
|
1664
|
+
}
|
|
1624
1665
|
result = true;
|
|
1625
1666
|
return result;
|
|
1626
1667
|
}
|
|
@@ -1669,6 +1710,7 @@ class ValueSetChecker {
|
|
|
1669
1710
|
if (!this.params.membershipOnly) {
|
|
1670
1711
|
inactive.value = true;
|
|
1671
1712
|
inactive.path = path;
|
|
1713
|
+
inactive.code = code;
|
|
1672
1714
|
vstatus.value = await cs.getStatus(loc);
|
|
1673
1715
|
}
|
|
1674
1716
|
} else {
|
|
@@ -1681,6 +1723,7 @@ class ValueSetChecker {
|
|
|
1681
1723
|
if (await cs.isInactive(loc)) {
|
|
1682
1724
|
inactive.value = true;
|
|
1683
1725
|
inactive.path = path;
|
|
1726
|
+
inactive.code = code;
|
|
1684
1727
|
// only replace a status we already have with a real one - a filter context
|
|
1685
1728
|
// doesn't always carry the status (SNOMED, LOINC), and the assembly reads
|
|
1686
1729
|
// vstatus for the message wording as well as for the status parameter
|
|
@@ -1718,9 +1761,12 @@ class ValueSetChecker {
|
|
|
1718
1761
|
}
|
|
1719
1762
|
} else {
|
|
1720
1763
|
result = true;
|
|
1721
|
-
inactive.value
|
|
1722
|
-
|
|
1723
|
-
|
|
1764
|
+
if (!inactive.value) {
|
|
1765
|
+
inactive.value = await cs.isInactive(loc.context);
|
|
1766
|
+
inactive.path = path;
|
|
1767
|
+
inactive.code = code;
|
|
1768
|
+
vstatus.value = await cs.getStatus(loc.context);
|
|
1769
|
+
}
|
|
1724
1770
|
await this.worker.listDisplaysFromCodeSystem(displays, cs, loc.context);
|
|
1725
1771
|
return result;
|
|
1726
1772
|
}
|
package/tx/workers/worker.js
CHANGED
|
@@ -379,14 +379,22 @@ class TerminologyWorker {
|
|
|
379
379
|
return null;
|
|
380
380
|
}
|
|
381
381
|
if (url.startsWith("#")) {
|
|
382
|
+
// A local reference: per FHIR, #id is resolved against the contained resources of
|
|
383
|
+
// the resource being processed - which, when that is itself a contained value set,
|
|
384
|
+
// means its container's (contained resources are all siblings in one list)
|
|
382
385
|
if (source) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
const container = source.container || source;
|
|
387
|
+
const json = container.jsonObj || container;
|
|
388
|
+
for (const contained of json.contained || []) {
|
|
389
|
+
if (contained && contained.id === url.substring(1)) {
|
|
390
|
+
if (contained.resourceType !== 'ValueSet') {
|
|
391
|
+
// the resource wrappers already reject this; this is the second line of defence
|
|
392
|
+
throw new Issue('error', 'not-supported', null, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
|
|
393
|
+
`The reference '${url}' is to a contained ${contained.resourceType}, not a ValueSet`, 'not-supported', 400);
|
|
394
|
+
}
|
|
388
395
|
const ret = this.wrapRawResource(contained);
|
|
389
396
|
ret.isContained = true;
|
|
397
|
+
ret.container = container.jsonObj ? container : this.wrapRawResource(container);
|
|
390
398
|
return ret;
|
|
391
399
|
}
|
|
392
400
|
}
|
|
@@ -477,11 +485,24 @@ class TerminologyWorker {
|
|
|
477
485
|
* Note: this is the caller's parameters, not this.params - the worker's own
|
|
478
486
|
* params is null when expanding an imported ValueSet
|
|
479
487
|
*/
|
|
488
|
+
/** The circular reference key seeValueSet tracks for a value set */
|
|
489
|
+
valueSetContextKey(vs) {
|
|
490
|
+
return vs.contextKey !== undefined ? vs.contextKey : (vs.url ? (vs.url + (vs.version ? '|' + vs.version : '')) : null);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Finished processing a value set: see OperationContext.unseeContext */
|
|
494
|
+
unseeValueSet(vs) {
|
|
495
|
+
const key = this.valueSetContextKey(vs);
|
|
496
|
+
if (key) {
|
|
497
|
+
this.opContext.unseeContext(key);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
480
501
|
seeValueSet(vs, params) {
|
|
481
502
|
// Build canonical URL from url and version
|
|
482
|
-
const
|
|
483
|
-
if (
|
|
484
|
-
this.opContext.seeContext(
|
|
503
|
+
const key = this.valueSetContextKey(vs);
|
|
504
|
+
if (key) {
|
|
505
|
+
this.opContext.seeContext(key);
|
|
485
506
|
}
|
|
486
507
|
// Check for expansion parameter extensions on compose
|
|
487
508
|
if (vs.jsonObj.compose && vs.jsonObj.compose.extension) {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
4
|
+
//
|
|
5
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
// The published FHIR specification has a page per resource for its JSON schema
|
|
9
|
+
// (e.g. testreport.schema.json.html), but not the schema itself as a file, so links to
|
|
10
|
+
// testreport.schema.json fail. This walks a tree of published specifications and, for each
|
|
11
|
+
// X.schema.json.html, takes the content of its <pre> element and writes it as X.schema.json
|
|
12
|
+
// beside it.
|
|
13
|
+
//
|
|
14
|
+
// node utilities/extract-schema-json.js <dir> [<dir> ...] [--dry-run] [--force] [--verbose]
|
|
15
|
+
//
|
|
16
|
+
// --dry-run report what would be written, but write nothing
|
|
17
|
+
// --force overwrite an existing X.schema.json whose content differs (by default it's
|
|
18
|
+
// left alone and reported)
|
|
19
|
+
// --verbose list every file written or skipped
|
|
20
|
+
// --no-recurse only look in the named directories themselves, not below them
|
|
21
|
+
//
|
|
22
|
+
// It can be stopped and run again: files already written are reported as up to date.
|
|
23
|
+
//
|
|
24
|
+
// A page is skipped (and reported) if it doesn't have exactly one <pre>, or if the content
|
|
25
|
+
// isn't valid JSON. Symbolic links are not followed. Exit status is 1 if anything was
|
|
26
|
+
// skipped for a problem, so it can be run in a script.
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const SUFFIX = '.schema.json.html';
|
|
32
|
+
|
|
33
|
+
const NAMED_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: '\'', nbsp: ' ' };
|
|
34
|
+
|
|
35
|
+
function decodeEntities(text) {
|
|
36
|
+
return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (m, e) => {
|
|
37
|
+
if (e[0] === '#') {
|
|
38
|
+
const code = e[1] === 'x' || e[1] === 'X' ? parseInt(e.substring(2), 16) : parseInt(e.substring(1), 10);
|
|
39
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : m;
|
|
40
|
+
}
|
|
41
|
+
const v = NAMED_ENTITIES[e.toLowerCase()];
|
|
42
|
+
return v === undefined ? m : v;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The text with the markup removed. Repeated until nothing changes, so that removing one tag
|
|
48
|
+
* can't leave another behind (<a<b>>).
|
|
49
|
+
*/
|
|
50
|
+
function stripTags(html) {
|
|
51
|
+
let text = html;
|
|
52
|
+
let previous;
|
|
53
|
+
do {
|
|
54
|
+
previous = text;
|
|
55
|
+
text = text.replace(/<[^<>]*>/g, '');
|
|
56
|
+
} while (text !== previous);
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The schema in a page: the text of its one <pre> element, with any markup removed and
|
|
62
|
+
* entities decoded.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} html
|
|
65
|
+
* @returns {{json?: string, problem?: string}}
|
|
66
|
+
*/
|
|
67
|
+
function extractSchema(html) {
|
|
68
|
+
const pres = [...html.matchAll(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi)];
|
|
69
|
+
if (pres.length !== 1) {
|
|
70
|
+
return { problem: pres.length === 0 ? 'no <pre> element' : `${pres.length} <pre> elements` };
|
|
71
|
+
}
|
|
72
|
+
// entities are decoded after the markup is gone: a decoded < is the schema's own text
|
|
73
|
+
const text = decodeEntities(stripTags(pres[0][1])).trim() + '\n';
|
|
74
|
+
try {
|
|
75
|
+
JSON.parse(text);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { problem: 'the <pre> content is not valid JSON: ' + e.message };
|
|
78
|
+
}
|
|
79
|
+
return { json: text };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function* walk(dir, recurse = true) {
|
|
83
|
+
let handle;
|
|
84
|
+
try {
|
|
85
|
+
handle = await fs.promises.opendir(dir);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error(`Cannot read ${dir}: ${e.message}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for await (const entry of handle) {
|
|
91
|
+
const p = path.join(dir, entry.name);
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (recurse) {
|
|
94
|
+
yield* walk(p);
|
|
95
|
+
}
|
|
96
|
+
} else if (entry.isFile() && entry.name.endsWith(SUFFIX)) {
|
|
97
|
+
yield p;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function run(roots, options = {}) {
|
|
103
|
+
const counts = { pages: 0, written: 0, unchanged: 0, different: 0, problems: 0 };
|
|
104
|
+
const log = (msg) => options.verbose && console.log(msg);
|
|
105
|
+
for (const root of roots) {
|
|
106
|
+
for await (const page of walk(root, !options.noRecurse)) {
|
|
107
|
+
counts.pages++;
|
|
108
|
+
const target = page.slice(0, -'.html'.length);
|
|
109
|
+
const { json, problem } = extractSchema(await fs.promises.readFile(page, 'utf8'));
|
|
110
|
+
if (problem) {
|
|
111
|
+
counts.problems++;
|
|
112
|
+
console.warn(`${page}: ${problem}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let existing = null;
|
|
116
|
+
try {
|
|
117
|
+
existing = await fs.promises.readFile(target, 'utf8');
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// not there yet
|
|
120
|
+
}
|
|
121
|
+
if (existing === json) {
|
|
122
|
+
counts.unchanged++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (existing !== null && !options.force) {
|
|
126
|
+
counts.different++;
|
|
127
|
+
console.warn(`${target}: exists with different content - left alone (--force to overwrite)`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!options.dryRun) {
|
|
131
|
+
await fs.promises.writeFile(target, json);
|
|
132
|
+
}
|
|
133
|
+
counts.written++;
|
|
134
|
+
log(`${options.dryRun ? 'would write' : 'wrote'} ${target}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return counts;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function main(args) {
|
|
141
|
+
const options = {
|
|
142
|
+
dryRun: args.includes('--dry-run'),
|
|
143
|
+
force: args.includes('--force'),
|
|
144
|
+
verbose: args.includes('--verbose'),
|
|
145
|
+
noRecurse: args.includes('--no-recurse')
|
|
146
|
+
};
|
|
147
|
+
const roots = args.filter(a => !a.startsWith('--'));
|
|
148
|
+
if (roots.length === 0) {
|
|
149
|
+
console.error('usage: extract-schema-json.js <dir> [<dir> ...] [--dry-run] [--force] [--verbose] [--no-recurse]');
|
|
150
|
+
process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
const start = Date.now();
|
|
153
|
+
const c = await run(roots, options);
|
|
154
|
+
console.log(`${c.pages} schema pages: ${c.written} ${options.dryRun ? 'to write' : 'written'}, ` +
|
|
155
|
+
`${c.unchanged} already up to date, ${c.different} existing and different (left alone), ` +
|
|
156
|
+
`${c.problems} with problems (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
157
|
+
process.exit(c.problems > 0 ? 1 : 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (require.main === module) {
|
|
161
|
+
main(process.argv.slice(2)).catch((e) => {
|
|
162
|
+
console.error(e);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { extractSchema, decodeEntities, run };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
4
|
+
//
|
|
5
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
// Generates a module's FHIR component schemas for its OpenAPI description, from the
|
|
9
|
+
// StructureDefinitions in a FHIR package (see library/fhir-openapi-schema.js).
|
|
10
|
+
//
|
|
11
|
+
// node utilities/generate-openapi-schemas.js <module> [-package <dir>]
|
|
12
|
+
//
|
|
13
|
+
// reads <module>/openapi-schemas.config.js and writes <module>/openapi-schemas.json. The
|
|
14
|
+
// package is found in the server's terminology cache unless -package names the unpacked
|
|
15
|
+
// package directory.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { generateSchemas } = require('../library/fhir-openapi-schema');
|
|
20
|
+
|
|
21
|
+
function packageDir(id) {
|
|
22
|
+
const folders = require('../library/folder-setup');
|
|
23
|
+
return folders.filePath('terminology-cache', id, 'package');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function main(args) {
|
|
27
|
+
const module = args[0];
|
|
28
|
+
if (!module) {
|
|
29
|
+
console.error('usage: generate-openapi-schemas.js <module> [-package <dir>]');
|
|
30
|
+
process.exit(2);
|
|
31
|
+
}
|
|
32
|
+
const moduleDir = path.join(__dirname, '..', module);
|
|
33
|
+
const config = require(path.join(moduleDir, 'openapi-schemas.config.js'));
|
|
34
|
+
const i = args.indexOf('-package');
|
|
35
|
+
const dir = i >= 0 ? args[i + 1] : packageDir(config.package);
|
|
36
|
+
if (!fs.existsSync(dir)) {
|
|
37
|
+
console.error(`The package ${config.package} isn't at ${dir}. Load it into the terminology cache, or use -package <dir>`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const out = path.join(moduleDir, 'openapi-schemas.json');
|
|
41
|
+
fs.writeFileSync(out, JSON.stringify(generateSchemas(config, dir), null, 2) + '\n');
|
|
42
|
+
console.log(`Wrote ${out}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (require.main === module) {
|
|
46
|
+
main(process.argv.slice(2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { packageDir };
|
package/xig/xig.js
CHANGED
|
@@ -397,8 +397,7 @@ async function gatherPageStatistics() {
|
|
|
397
397
|
downloadDate: downloadDate,
|
|
398
398
|
totalResources: tableCounts.resources || 0,
|
|
399
399
|
totalPackages: tableCounts.packages || 0,
|
|
400
|
-
processingTime: processingTime
|
|
401
|
-
version: getMetadata('fhir-version') || '4.0.1'
|
|
400
|
+
processingTime: processingTime
|
|
402
401
|
};
|
|
403
402
|
|
|
404
403
|
} catch (error) {
|
|
@@ -411,8 +410,7 @@ async function gatherPageStatistics() {
|
|
|
411
410
|
downloadDate: 'Error',
|
|
412
411
|
totalResources: 0,
|
|
413
412
|
totalPackages: 0,
|
|
414
|
-
processingTime: processingTime
|
|
415
|
-
version: '4.0.1'
|
|
413
|
+
processingTime: processingTime
|
|
416
414
|
};
|
|
417
415
|
}
|
|
418
416
|
}
|