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.
- package/CHANGELOG.md +64 -0
- package/README.md +3 -2
- package/config-template.json +20 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/languages.js +15 -0
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/same-origin.js +62 -0
- package/library/utilities.js +21 -0
- package/nginx.md +17 -9
- 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/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +95 -42
- package/registry/api.js +117 -41
- package/registry/crawler.js +24 -0
- package/registry/fhirsmith-releases.js +170 -0
- package/registry/model.js +10 -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 +183 -20
- package/server.js +25 -6
- 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 +75 -1
- package/testing/render.js +270 -29
- package/testing/search.js +1 -1
- package/testing/store.js +152 -8
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +307 -9
- package/translations/Messages.properties +1 -0
- package/tx/README.md +58 -0
- package/tx/cm/cm-database.js +10 -1
- package/tx/cm/cm-package.js +8 -1
- package/tx/cs/cs-rxnorm.js +23 -6
- package/tx/cs/cs-snomed.js +3 -72
- 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/valueset.js +16 -0
- package/tx/library.js +17 -2
- 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 +10 -1
- package/tx/sct/editions.js +70 -0
- package/tx/tx-html.js +6 -0
- package/tx/tx.js +55 -0
- package/tx/vs/vs-database.js +14 -5
- package/tx/vs/vs-package.js +10 -0
- package/tx/workers/expand.js +37 -18
- package/tx/workers/metadata.js +0 -13
- package/tx/workers/translate.js +25 -1
- package/tx/workers/validate.js +74 -24
- package/tx/workers/worker.js +32 -43
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- package/utilities/submit-test-report.js +1 -1
- 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://
|
|
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
|
}
|