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
package/tx/openapi.js ADDED
@@ -0,0 +1,289 @@
1
+ //
2
+ // Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
3
+ //
4
+ // Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
5
+ //
6
+
7
+ // The terminology server's OpenAPI description (R5 endpoints only): openapi.yaml (the
8
+ // overview, /metadata, /$versions, the shared components), plus the paths made here from
9
+ // the operations in openapi-operations.js and the read and search interactions, plus the
10
+ // FHIR resource schemas generated into openapi-schemas.json (see openapi-schemas.config.js).
11
+
12
+ const path = require('path');
13
+ const { createOpenApiDoc } = require('../library/openapi-doc');
14
+ const { OPERATIONS, COMMON } = require('./openapi-operations');
15
+
16
+ const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
17
+ const param = (name) => ({ $ref: `#/components/parameters/${name}` });
18
+
19
+ const RESOURCE_TYPES = ['CodeSystem', 'ValueSet', 'ConceptMap'];
20
+
21
+ const PRIMITIVES = new Set([
22
+ 'boolean', 'canonical', 'code', 'date', 'dateTime', 'decimal', 'id', 'instant', 'integer',
23
+ 'integer64', 'markdown', 'oid', 'positiveInt', 'string', 'time', 'unsignedInt', 'uri', 'url', 'uuid'
24
+ ]);
25
+
26
+ function isPrimitive(type) {
27
+ return PRIMITIVES.has(type);
28
+ }
29
+
30
+ /** The JSON schema of a primitive parameter, as a query parameter. */
31
+ function querySchema(type) {
32
+ switch (type) {
33
+ case 'boolean': return { type: 'boolean' };
34
+ case 'integer': return { type: 'integer' };
35
+ case 'positiveInt': return { type: 'integer', minimum: 1 };
36
+ case 'unsignedInt': return { type: 'integer', minimum: 0 };
37
+ case 'decimal': return { type: 'number' };
38
+ default: return { type: 'string', 'x-fhir-type': type };
39
+ }
40
+ }
41
+
42
+ // a table cell: backslashes escaped first, then the pipes (which would end the cell)
43
+ function cell(s) {
44
+ return String(s || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\n/g, ' ');
45
+ }
46
+
47
+ function typeCell(p) {
48
+ if (p.type === '(parts)') {
49
+ return 'parts';
50
+ }
51
+ return isPrimitive(p.type) ? `\`${p.type}\`` : p.type;
52
+ }
53
+
54
+ /** A markdown table of parameters. */
55
+ function paramTable(params, inbound) {
56
+ const rows = [];
57
+ for (const p of params) {
58
+ let doc = p.doc || '';
59
+ if (inbound && !isPrimitive(p.type)) {
60
+ doc += (doc ? ' ' : '') + '(POST, in a Parameters resource, only)';
61
+ }
62
+ rows.push(`| \`${cell(p.name)}\` | ${typeCell(p)} | ${p.min}..${p.max} | ${cell(doc)} |`);
63
+ for (const part of p.parts || []) {
64
+ rows.push(`|   \`${cell(p.name)}.${cell(part.name)}\` | ${typeCell(part)} | ${part.min}..${part.max} | ${cell(part.doc)} |`);
65
+ }
66
+ }
67
+ return '| Name | Type | Card. | Description |\n|---|---|---|---|\n' + rows.join('\n');
68
+ }
69
+
70
+ function camel(s) {
71
+ return s.replace(/-([a-z])/g, (m, c) => c.toUpperCase());
72
+ }
73
+
74
+ function errorResponses(extra = {}) {
75
+ return {
76
+ '400': { $ref: '#/components/responses/Outcome', description: 'The request is not valid' },
77
+ '404': { $ref: '#/components/responses/Outcome', description: 'Something the request names was not found' },
78
+ '422': { $ref: '#/components/responses/Outcome', description: 'The request could not be processed (e.g. the value set is not valid, or the expansion is too large)' },
79
+ '500': { $ref: '#/components/responses/Outcome', description: 'The server failed' },
80
+ ...extra
81
+ };
82
+ }
83
+
84
+ function okResponse(op) {
85
+ const schema = ref(op.returns || 'Parameters');
86
+ return {
87
+ description: op.returnsDoc || 'The out parameters (see the table)',
88
+ content: { 'application/fhir+json': { schema }, 'application/json': { schema } }
89
+ };
90
+ }
91
+
92
+ /** The paths of one operation: its levels, and its methods. */
93
+ function queryParameter(qp) {
94
+ const schema = querySchema(qp.type);
95
+ const result = {
96
+ name: qp.name,
97
+ in: 'query',
98
+ description: qp.doc,
99
+ schema: qp.max === '*' ? { type: 'array', items: schema } : schema
100
+ };
101
+ if (qp.min > 0) {
102
+ result.required = true;
103
+ }
104
+ if (qp.max === '*') {
105
+ result.style = 'form';
106
+ result.explode = true;
107
+ }
108
+ return result;
109
+ }
110
+
111
+ function formSchema(params) {
112
+ const form = { type: 'object', properties: {} };
113
+ for (const fp of params) {
114
+ form.properties[fp.name] = fp.max === '*' ? { type: 'array', items: querySchema(fp.type) } : querySchema(fp.type);
115
+ }
116
+ return form;
117
+ }
118
+
119
+ /** The common parameters: in the overview, and as components the operations refer to. */
120
+ function addCommon(spec) {
121
+ spec.info.description += '\n**Common parameters.** These are read by $expand, $validate-code, ' +
122
+ '$lookup, $subsumes, $translate and $compare, as well as their own parameters:\n\n' + paramTable(COMMON, true) + '\n';
123
+ for (const cp of COMMON.filter(x => isPrimitive(x.type))) {
124
+ spec.components.parameters[`common-${cp.name}`] = queryParameter(cp);
125
+ }
126
+ }
127
+
128
+ function addOperation(spec, op) {
129
+ const methods = op.methods || ['get', 'post'];
130
+ const queryParams = op.in.filter(p => isPrimitive(p.type));
131
+ const commonQuery = op.common ? COMMON.filter(x => isPrimitive(x.type)) : [];
132
+ let description = (op.description || '') + '\n\n**In parameters**' +
133
+ (op.common ? ' (and the common parameters - see the overview)' : '') + '\n\n' + paramTable(op.in, true);
134
+ if (op.out) {
135
+ description += '\n\n**Out parameters** (a Parameters resource)\n\n' + paramTable(op.out, false);
136
+ }
137
+ for (const level of op.levels) {
138
+ const p = level === 'system' ? `/$${op.name}`
139
+ : level === 'type' ? `/${op.resource}/$${op.name}`
140
+ : `/${op.resource}/{id}/$${op.name}`;
141
+ const item = spec.paths[p] = spec.paths[p] || {};
142
+ const baseId = camel(op.name) + (op.resource || '') + (level === 'instance' ? 'Instance' : '');
143
+ const common = [param('acceptLanguage'), param('cacheId'), param('format')];
144
+ if (level === 'instance') {
145
+ common.unshift(param('id'));
146
+ }
147
+ const levelDoc = level === 'instance' ? `\n\nThe ${op.resource} is the one with this id.` : '';
148
+ for (const method of methods) {
149
+ const o = {
150
+ tags: [op.resource || 'Server'],
151
+ operationId: method === 'get' || methods.length === 1 ? baseId : baseId + 'Post',
152
+ summary: op.summary,
153
+ description: description + levelDoc
154
+ };
155
+ if (method === 'get') {
156
+ o.parameters = [...common, ...queryParams.map(queryParameter), ...commonQuery.map(cp => param(`common-${cp.name}`))];
157
+ } else {
158
+ // parameters that are in the query even for a POST
159
+ o.parameters = [...common, ...op.in.filter(x => x.inQuery).map(qp => ({
160
+ name: qp.name, in: 'query', description: qp.doc, required: qp.min > 0, schema: querySchema(qp.type)
161
+ }))];
162
+ const bodies = op.postBody || ['Parameters'];
163
+ const schema = bodies.length === 1 ? ref(bodies[0]) : { anyOf: bodies.map(ref) };
164
+ const form = formSchema([...queryParams.filter(x => !x.inQuery), ...commonQuery]);
165
+ o.requestBody = {
166
+ required: true,
167
+ description: bodies.length === 1 ? 'The in parameters, as a Parameters resource (or a form)'
168
+ : `The in parameters, as a Parameters resource (or a form), or the ${bodies[1]} itself`,
169
+ content: {
170
+ 'application/fhir+json': { schema },
171
+ 'application/json': { schema },
172
+ 'application/x-www-form-urlencoded': { schema: form }
173
+ }
174
+ };
175
+ }
176
+ o.responses = { '200': okResponse(op), ...errorResponses(op.responses || {}) };
177
+ item[method] = o;
178
+ }
179
+ }
180
+ }
181
+
182
+ const SEARCH_DOCS = {
183
+ url: 'The canonical url (exact)',
184
+ system: 'The canonical url (exact; the same as url)',
185
+ version: 'The version (contains)',
186
+ name: 'The name (contains)',
187
+ title: 'The title (contains)',
188
+ status: 'The status (contains)',
189
+ publisher: 'The publisher (contains)',
190
+ description: 'The description (contains)',
191
+ date: 'The date (contains)',
192
+ identifier: 'An identifier (contains)',
193
+ jurisdiction: 'A jurisdiction: a code or display (contains)',
194
+ text: 'The title or description (contains)',
195
+ 'content-mode': 'CodeSystem: the content mode (contains)',
196
+ supplements: 'CodeSystem: what it supplements (contains)',
197
+ 'source-system': 'ConceptMap: the source code system',
198
+ 'target-system': 'ConceptMap: the target code system',
199
+ _offset: 'Paging: where to start (0 based)',
200
+ _count: 'Paging: how many to return (at most 200, or 2000 with _elements)',
201
+ _elements: 'The elements to return (comma separated)',
202
+ _sort: 'The element to sort by (default id)',
203
+ _summary: '`true`, `text`, `data`, `count` or `false`',
204
+ _total: '`none` to leave out the total (default accurate)'
205
+ };
206
+
207
+ /** Read, and search (GET, and POST _search), for each resource type. */
208
+ function addReadSearch(spec) {
209
+ // required here: the search worker requires tx-html, which requires this
210
+ const SEARCH_PARAMS = require('./workers/search').ALLOWED_PARAMS;
211
+ for (const type of RESOURCE_TYPES) {
212
+ const searchParams = SEARCH_PARAMS.filter(n => n !== '_format').map(n => ({
213
+ name: n,
214
+ in: 'query',
215
+ description: SEARCH_DOCS[n] || '',
216
+ schema: ['_offset', '_count'].includes(n) ? { type: 'integer', minimum: 0 } : { type: 'string' }
217
+ }));
218
+ if (type !== 'CodeSystem') {
219
+ searchParams.push({ name: 'source', in: 'query', description: 'Only those from this source (package)', schema: { type: 'string' } });
220
+ }
221
+ const bundle = ref(`${type}SearchBundle`);
222
+ const searchDoc = `Searches the ${type}s the server has. Text matches are case insensitive and partial ` +
223
+ '(contains), except url and system; all the parameters given must match.';
224
+ const searchResponses = {
225
+ '200': { description: `A searchset Bundle of ${type}s`, content: { 'application/fhir+json': { schema: bundle } } },
226
+ '500': { $ref: '#/components/responses/Outcome' }
227
+ };
228
+ spec.paths[`/${type}`] = {
229
+ get: {
230
+ tags: [type], operationId: `search${type}`, summary: `Search ${type}s`, description: searchDoc,
231
+ parameters: [...searchParams, param('format')], responses: searchResponses
232
+ }
233
+ };
234
+ const form = { type: 'object', properties: {} };
235
+ searchParams.forEach(sp => {
236
+ form.properties[sp.name] = sp.schema;
237
+ });
238
+ spec.paths[`/${type}/_search`] = {
239
+ post: {
240
+ tags: [type], operationId: `search${type}Post`, summary: `Search ${type}s (POST)`,
241
+ description: searchDoc + ' The parameters are in a form body.',
242
+ requestBody: { content: { 'application/x-www-form-urlencoded': { schema: form } } },
243
+ responses: searchResponses
244
+ }
245
+ };
246
+ spec.paths[`/${type}/{id}`] = {
247
+ get: {
248
+ tags: [type], operationId: `read${type}`, summary: `Read a ${type}`,
249
+ parameters: [param('id'), param('format')],
250
+ responses: {
251
+ '200': { description: `The ${type}`, content: { 'application/fhir+json': { schema: ref(type) } } },
252
+ '404': { $ref: '#/components/responses/Outcome', description: 'There is no such resource' }
253
+ }
254
+ }
255
+ };
256
+ }
257
+ }
258
+
259
+ function build(spec) {
260
+ addCommon(spec);
261
+ addReadSearch(spec);
262
+ for (const op of OPERATIONS) {
263
+ addOperation(spec, op);
264
+ }
265
+ }
266
+
267
+ const SPEC_PATH = path.join(__dirname, 'openapi.yaml');
268
+ const SCHEMAS_PATH = path.join(__dirname, 'openapi-schemas.json');
269
+
270
+ const docs = new Map();
271
+
272
+ /**
273
+ * The description, for an R5 endpoint (its servers url is the endpoint)
274
+ * @param {string} endpointPath - e.g. /tx/r5
275
+ */
276
+ function forEndpoint(endpointPath) {
277
+ if (!docs.has(endpointPath)) {
278
+ docs.set(endpointPath, createOpenApiDoc(SPEC_PATH, endpointPath,
279
+ { schemasPath: SCHEMAS_PATH, build, serverUrl: endpointPath }));
280
+ }
281
+ return docs.get(endpointPath);
282
+ }
283
+
284
+ /** Whether an endpoint's FHIR version is the one described (R5) */
285
+ function describes(fhirVersion) {
286
+ return String(fhirVersion).startsWith('5');
287
+ }
288
+
289
+ module.exports = { forEndpoint, describes, isPrimitive, RESOURCE_TYPES, SEARCH_DOCS };
@@ -0,0 +1,151 @@
1
+ # OpenAPI description of the FHIRsmith terminology server (an R5 endpoint, e.g. /tx/r5).
2
+ #
3
+ # Three parts, merged when the spec is served (see openapi.js):
4
+ # - this file: the overview, /metadata and /$versions, and the shared components
5
+ # - openapi-operations.js: the terminology operations, as data. openapi.js makes their
6
+ # paths (type and instance level, GET and POST) and their parameter tables, and the
7
+ # read and search paths
8
+ # - openapi-schemas.json: the FHIR R5 resource schemas, generated from the
9
+ # StructureDefinitions by utilities/generate-openapi-schemas.js (see
10
+ # openapi-schemas.config.js)
11
+ #
12
+ # tests/tx/openapi.test.js keeps this honest: every route must be described or explicitly
13
+ # excluded, the parameters must be the ones the code reads, and real responses must
14
+ # validate against the schemas.
15
+ #
16
+ # HTML pages (/, /op.html, /ecl, /problems.html, /info, /library) are not described.
17
+ # info.version is filled in from package.json, and servers from the endpoint, when the spec
18
+ # is served.
19
+
20
+ openapi: 3.1.0
21
+ info:
22
+ title: FHIRsmith Terminology Server
23
+ version: "0.0.0"
24
+ summary: A FHIR terminology server (R5)
25
+ description: |
26
+ A FHIR terminology server: code systems, value sets and concept maps, and the
27
+ terminology operations on them - $expand, $validate-code, $lookup, $subsumes,
28
+ $translate, and more. This describes an R5 endpoint; the server's R4 and R3 endpoints
29
+ work the same way, with the resources of that version.
30
+
31
+ The operations are the ones FHIR defines (see the [terminology
32
+ service](https://hl7.org/fhir/R5/terminology-service.html)), with the behaviour the
33
+ [terminology ecosystem IG](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig/) asks of
34
+ the servers in the ecosystem. Each operation lists the parameters this server reads.
35
+
36
+ **Requests.** An operation can be invoked with GET, with its parameters in the query, or
37
+ with POST, with a Parameters resource (`application/fhir+json`), or a form. Parameters
38
+ whose values are not primitives (a Coding, a resource) can only be sent in a Parameters
39
+ resource.
40
+
41
+ **Responses** are JSON by default; `application/fhir+xml` (or `_format=xml`) gets XML,
42
+ and a browser (or `_format=html`) gets an HTML page. Only the JSON is described here.
43
+ Errors are OperationOutcomes, and every issue (except information) has a tx-issue-type
44
+ code in `details.coding` and its message in `details.text`.
45
+
46
+ **Schemas.** The resource schemas describe what this server handles and returns: it
47
+ doesn't reject content outside them (an extension with a Quantity value, say), but it
48
+ isn't designed to do anything with it. They have the JSON `_x` elements (the id and
49
+ extensions of a primitive) and modifierExtension wherever FHIR allows them. Contained
50
+ resources are value sets, in a value set, only.
51
+
52
+ **Caching.** A client that sends the same resources (`tx-resource`) with many requests
53
+ can keep them on the server with `$cache-control`, and then send the cache's id in the
54
+ `X-Cache-Id` header.
55
+ license:
56
+ name: BSD-3-Clause
57
+ url: https://opensource.org/license/bsd-3-clause
58
+ externalDocs:
59
+ description: The FHIR R5 terminology service
60
+ url: https://hl7.org/fhir/R5/terminology-service.html
61
+ servers:
62
+ - url: /tx/r5
63
+
64
+ tags:
65
+ - name: Server
66
+ description: What the server does, and operations on the server as a whole
67
+ - name: CodeSystem
68
+ description: Code systems, and $lookup, $validate-code and $subsumes
69
+ - name: ValueSet
70
+ description: Value sets, and $expand, $validate-code and $compare
71
+ - name: ConceptMap
72
+ description: Concept maps, and $translate
73
+
74
+ paths:
75
+ /metadata:
76
+ get:
77
+ tags: [Server]
78
+ operationId: metadata
79
+ summary: The server's capabilities
80
+ description: |
81
+ The CapabilityStatement, or with `mode=terminology`, the TerminologyCapabilities:
82
+ the code systems (and versions) the server has, and the expansion parameters it
83
+ understands.
84
+ parameters:
85
+ - name: mode
86
+ in: query
87
+ description: '`terminology` for the TerminologyCapabilities'
88
+ schema: { type: string, enum: [terminology] }
89
+ - $ref: '#/components/parameters/format'
90
+ responses:
91
+ '200':
92
+ description: The CapabilityStatement, or the TerminologyCapabilities
93
+ content:
94
+ application/fhir+json:
95
+ schema:
96
+ anyOf:
97
+ - $ref: '#/components/schemas/CapabilityStatement'
98
+ - $ref: '#/components/schemas/TerminologyCapabilities'
99
+
100
+ /$versions:
101
+ get:
102
+ tags: [Server]
103
+ operationId: versions
104
+ summary: The FHIR versions of this endpoint
105
+ description: |
106
+ The FHIR version this endpoint uses. With `Accept: application/fhir+json` the answer
107
+ is a Parameters resource (`version` and `default`); otherwise, a simple JSON object.
108
+ responses:
109
+ '200':
110
+ description: The versions
111
+ content:
112
+ application/fhir+json:
113
+ schema: { $ref: '#/components/schemas/Parameters' }
114
+ application/json:
115
+ schema:
116
+ type: object
117
+ properties:
118
+ versions: { type: array, items: { type: string }, example: ['5.0'] }
119
+ default: { type: string, example: '5.0' }
120
+ required: [versions, default]
121
+
122
+ components:
123
+ parameters:
124
+ id:
125
+ name: id
126
+ in: path
127
+ required: true
128
+ description: The resource's id
129
+ schema: { type: string }
130
+ format:
131
+ name: _format
132
+ in: query
133
+ description: '`json`, `xml` or `html`, instead of the Accept header'
134
+ schema: { type: string, enum: [json, xml, html] }
135
+ acceptLanguage:
136
+ name: Accept-Language
137
+ in: header
138
+ description: The language(s) for displays and messages
139
+ schema: { type: string }
140
+ example: en
141
+ cacheId:
142
+ name: X-Cache-Id
143
+ in: header
144
+ description: A cache of resources, started with $cache-control
145
+ schema: { type: string }
146
+ responses:
147
+ Outcome:
148
+ description: An error, as an OperationOutcome
149
+ content:
150
+ application/fhir+json:
151
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
@@ -1084,6 +1084,21 @@ class OperationContext {
1084
1084
  this.contexts.push(vurl);
1085
1085
  }
1086
1086
 
1087
+ /**
1088
+ * Stop tracking a context, once processing of that value set is finished. The tracked
1089
+ * contexts are the chain of value sets currently being processed, not every value set
1090
+ * seen in the operation: a value set used twice (imported by two others, or twice by
1091
+ * one) is not a circularity - only one that is reached again while it's being
1092
+ * processed is.
1093
+ * @param {string} vurl - the url passed to seeContext
1094
+ */
1095
+ unseeContext(vurl) {
1096
+ const i = this.contexts.lastIndexOf(vurl);
1097
+ if (i >= 0) {
1098
+ this.contexts.splice(i, 1);
1099
+ }
1100
+ }
1101
+
1087
1102
  /**
1088
1103
  * Clear all tracked contexts
1089
1104
  */
package/tx/params.js CHANGED
@@ -372,11 +372,6 @@ class TxParameters {
372
372
  if (claim('no-cache') && asBool()) this.FUid = crypto.randomUUID();
373
373
  break;
374
374
  }
375
- case '_incomplete':
376
- case 'limitedExpansion': {
377
- if (claim('limitedExpansion')) this.limitedExpansion = asBool();
378
- break;
379
- }
380
375
  case 'includeDesignations': {
381
376
  if (claim('includeDesignations')) this.includeDesignations = asBool();
382
377
  break;
@@ -405,10 +400,6 @@ class TxParameters {
405
400
  if (claim('default-to-latest-version')) this.defaultToLatestVersion = asBool();
406
401
  break;
407
402
  }
408
- case 'incomplete-ok': {
409
- if (claim('incomplete-ok')) this.incompleteOK = asBool();
410
- break;
411
- }
412
403
  case 'diagnostics': {
413
404
  if (claim('diagnostics')) this.diagnostics = asBool();
414
405
  break;
@@ -629,11 +620,11 @@ class TxParameters {
629
620
  s = s + '$' + [...this.supplements].sort().join(',') + '|';
630
621
  }
631
622
  // Further result-affecting parameters that were previously omitted from the
632
- // key: the text filter (changes which codes expand), limited/incomplete
633
- // expansion handling, whether abstract codes are included, and diagnostics.
623
+ // key: the text filter (changes which codes expand), whether abstract codes
624
+ // are included, and diagnostics.
634
625
  // filter is free text, so JSON.stringify it to avoid delimiter collisions.
635
626
  s = s + 'f:' + JSON.stringify(this.filter || '') + '|' +
636
- b(this.limitedExpansion) + b(this.incompleteOK) + b(this.abstractOk) + b(this.diagnostics);
627
+ b(this.abstractOk) + b(this.diagnostics);
637
628
  for (let t of this.FVersionRules) {
638
629
  s = s + t.asString() + '|';
639
630
  }
package/tx/provider.js CHANGED
@@ -172,7 +172,16 @@ class Provider {
172
172
 
173
173
  const resources = await contentLoader.getResourcesByType("CodeSystem");
174
174
  for (const resource of resources) {
175
- const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
175
+ let cs;
176
+ try {
177
+ cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
178
+ } catch (e) {
179
+ // one unacceptable resource (e.g. contained resources the server doesn't support)
180
+ // must not stop the rest of the package loading
181
+ require('../library/logger').getInstance().child({ module: 'tx' })
182
+ .warn(`Package ${contentLoader.pid()}: skipping CodeSystem ${resource.id || resource.filename || ''}: ${e.message}`);
183
+ continue;
184
+ }
176
185
  cs.sourcePackage = contentLoader.pid();
177
186
  this.addCodeSystem(cs);
178
187
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The SNOMED CT editions FHIRsmith knows by name - the one table, used for the provider's
3
+ * description and name (cs-snomed.js) and for the edition named in unknown-code messages
4
+ * (SCTVersion in workers/worker.js). Keyed by the edition's module id, as it appears in a
5
+ * version URI: http://snomed.info/sct/[module]/version/[date]
6
+ *
7
+ * name - human readable edition name
8
+ * code - short code used in the provider name ("SCT US")
9
+ *
10
+ * (The importer and the registry keep their own tables: those carry import settings and
11
+ * registry codes, not display names.)
12
+ */
13
+ const EDITIONS = {
14
+ '900000000000207008': { name: 'International Edition', code: 'Intl' },
15
+ // The terminology-ecosystem test distribution, published under xsct - see the
16
+ // tx-ecosystem IG, which requires a server claiming the snomed mode to load it.
17
+ '31000003106': { name: 'Test Edition', code: 'Test' },
18
+ '449081005': { name: 'International Spanish Edition', code: 'es' },
19
+ '11000221109': { name: 'Argentinian Edition', code: 'AR-es' },
20
+ '32506021000036107': { name: 'Australian Edition (with drug extension)', code: 'AU+' },
21
+ '11000234105': { name: 'Austrian Edition', code: 'AT' },
22
+ '11000172109': { name: 'Belgian Edition', code: 'BE' },
23
+ '20621000087109': { name: 'Canadian English Edition', code: 'CA-en' },
24
+ '20611000087101': { name: 'Canadian Canadian French Edition', code: 'CA-fr' },
25
+ '554471000005108': { name: 'Danish Edition', code: 'DK' },
26
+ '11000279109': { name: 'Czech Edition', code: 'CZ' },
27
+ '11000181102': { name: 'Estonian Edition', code: 'ET' },
28
+ '11000229106': { name: 'Finnish Edition', code: 'FI' },
29
+ '11000274103': { name: 'German Edition', code: 'DE' },
30
+ '1121000189102': { name: 'Indian Edition', code: 'IN' },
31
+ '827022005': { name: 'IPS Terminology', code: 'IPS' },
32
+ '11000220105': { name: 'Irish Edition', code: 'IE' },
33
+ '11000146104': { name: 'Netherlands Edition', code: 'NL' },
34
+ '21000210109': { name: 'New Zealand Edition', code: 'NZ' },
35
+ '51000202101': { name: 'Norwegian Edition', code: 'NO' },
36
+ '11000267109': { name: 'Republic of Korea Edition (South Korea)', code: 'KR' },
37
+ '900000001000122104': { name: 'Spanish National Edition', code: 'ES-es' },
38
+ '45991000052106': { name: 'Swedish Edition', code: 'SE' },
39
+ '2011000195101': { name: 'Swiss Edition', code: 'CH' },
40
+ '83821000000107': { name: 'UK Edition', code: 'UK' },
41
+ '999000021000000109': { name: 'UK Clinical Edition', code: 'UK-Clinical' },
42
+ '5631000179106': { name: 'Uruguay Edition', code: 'UY' },
43
+ '731000124108': { name: 'US Edition', code: 'US' },
44
+ '21000325107': { name: 'Chilean Edition', code: 'CL' },
45
+ '5991000124107': { name: 'US Edition (with ICD-10-CM maps)', code: 'US+' }
46
+ };
47
+
48
+ /**
49
+ * @param {string} edition - edition module id
50
+ * @returns {string|null} the edition's name, or null if it isn't a known edition
51
+ */
52
+ function editionName(edition) {
53
+ const e = EDITIONS[edition];
54
+ return e ? e.name : null;
55
+ }
56
+
57
+ /**
58
+ * @param {string} edition - edition module id
59
+ * @returns {string|null} the edition's short code, or null if it isn't a known edition
60
+ */
61
+ function editionCode(edition) {
62
+ const e = EDITIONS[edition];
63
+ return e ? e.code : null;
64
+ }
65
+
66
+ module.exports = {
67
+ EDITIONS,
68
+ editionName,
69
+ editionCode
70
+ };
package/tx/tx-html.js CHANGED
@@ -21,6 +21,7 @@ const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
21
21
  const {debugLog} = require("./operation-context");
22
22
  const {InvalidError} = require("./library/errors");
23
23
  const {VALID_FILTER_OPS} = require("./library/renderer");
24
+ const txOpenApi = require("./openapi");
24
25
 
25
26
  const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
26
27
 
@@ -262,6 +263,11 @@ class TxHtmlRenderer {
262
263
  ? '<a href="' + escape(endpoint.path) + '/library" style="color: gold">Library</a> &nbsp;|&nbsp;'
263
264
  : ''
264
265
  };
266
+ // the OpenAPI description, on the endpoints it describes (R5)
267
+ if (txOpenApi.describes(endpoint.fhirVersion)) {
268
+ options.apiLink = '<a href="' + escape(endpoint.path) + '/openapi" style="color: gold">API</a> &nbsp;|&nbsp;';
269
+ options.apiHead = '<link rel="service-desc" type="application/json" href="' + escape(endpoint.path) + '/openapi.json"/>';
270
+ }
265
271
 
266
272
  return htmlServer.renderPage('tx', title, content, options);
267
273
  }