fhirsmith 0.14.0 → 0.14.2

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +3 -2
  3. package/config-template.json +20 -0
  4. package/library/fhir-openapi-schema.js +441 -0
  5. package/library/html-server.js +9 -3
  6. package/library/languages.js +15 -0
  7. package/library/npm-audit.js +265 -0
  8. package/library/openapi-doc.js +557 -0
  9. package/library/same-origin.js +62 -0
  10. package/library/utilities.js +21 -0
  11. package/nginx.md +17 -9
  12. package/package.json +10 -4
  13. package/packages/openapi.js +13 -0
  14. package/packages/openapi.yaml +603 -0
  15. package/packages/packages-template.html +4 -2
  16. package/packages/packages.js +286 -81
  17. package/packages/readme.md +120 -0
  18. package/publisher/publisher-template.html +1 -0
  19. package/publisher/publisher.js +95 -42
  20. package/registry/api.js +117 -41
  21. package/registry/crawler.js +24 -0
  22. package/registry/fhirsmith-releases.js +170 -0
  23. package/registry/model.js +10 -0
  24. package/registry/openapi.js +13 -0
  25. package/registry/openapi.yaml +311 -0
  26. package/registry/readme.md +107 -208
  27. package/registry/registry-template.html +4 -1
  28. package/registry/registry.js +183 -20
  29. package/server.js +25 -6
  30. package/testing/openapi-schemas.config.js +58 -0
  31. package/testing/openapi-schemas.json +1920 -0
  32. package/testing/openapi.js +15 -0
  33. package/testing/openapi.yaml +273 -0
  34. package/testing/readme.md +75 -1
  35. package/testing/render.js +270 -29
  36. package/testing/search.js +1 -1
  37. package/testing/store.js +152 -8
  38. package/testing/testing-template.html +2 -0
  39. package/testing/testing.js +307 -9
  40. package/translations/Messages.properties +1 -0
  41. package/tx/README.md +58 -0
  42. package/tx/cm/cm-database.js +10 -1
  43. package/tx/cm/cm-package.js +8 -1
  44. package/tx/cs/cs-rxnorm.js +23 -6
  45. package/tx/cs/cs-snomed.js +3 -72
  46. package/tx/html/tx-template.html +2 -0
  47. package/tx/library/canonical-resource.js +50 -1
  48. package/tx/library/codesystem.js +1 -0
  49. package/tx/library/conceptmap.js +1 -0
  50. package/tx/library/valueset.js +16 -0
  51. package/tx/library.js +17 -2
  52. package/tx/openapi-operations.js +363 -0
  53. package/tx/openapi-schemas.config.js +118 -0
  54. package/tx/openapi-schemas.json +7903 -0
  55. package/tx/openapi.js +289 -0
  56. package/tx/openapi.yaml +151 -0
  57. package/tx/operation-context.js +15 -0
  58. package/tx/params.js +3 -12
  59. package/tx/provider.js +10 -1
  60. package/tx/sct/editions.js +70 -0
  61. package/tx/tx-html.js +6 -0
  62. package/tx/tx.js +55 -0
  63. package/tx/vs/vs-database.js +14 -5
  64. package/tx/vs/vs-package.js +10 -0
  65. package/tx/workers/expand.js +37 -18
  66. package/tx/workers/metadata.js +0 -13
  67. package/tx/workers/translate.js +25 -1
  68. package/tx/workers/validate.js +74 -24
  69. package/tx/workers/worker.js +32 -43
  70. package/utilities/extract-schema-json.js +167 -0
  71. package/utilities/generate-openapi-schemas.js +49 -0
  72. package/utilities/submit-test-report.js +1 -1
  73. package/xig/xig.js +2 -4
@@ -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 };
@@ -22,7 +22,7 @@
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
24
 
25
- const DEFAULT_SERVER = 'https://testing.fhir.org/testing';
25
+ const DEFAULT_SERVER = 'https://npm.fhir.org/testing';
26
26
  const DEFAULT_FILE = path.join(__dirname, '..', 'test-cases-report.json');
27
27
 
28
28
  function usage(msg) {
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
  }