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.
Files changed (74) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +3 -2
  3. package/config-template.json +9 -0
  4. package/library/changelog.js +57 -0
  5. package/library/fhir-openapi-schema.js +441 -0
  6. package/library/html-server.js +9 -3
  7. package/library/npm-audit.js +265 -0
  8. package/library/openapi-doc.js +557 -0
  9. package/library/utilities.js +21 -0
  10. package/package.json +10 -4
  11. package/packages/openapi.js +13 -0
  12. package/packages/openapi.yaml +603 -0
  13. package/packages/packages-template.html +4 -2
  14. package/packages/packages.js +286 -81
  15. package/packages/readme.md +120 -0
  16. package/registry/api.js +117 -41
  17. package/registry/crawler.js +41 -0
  18. package/registry/fhirsmith-releases.js +167 -0
  19. package/registry/model.js +13 -0
  20. package/registry/openapi.js +13 -0
  21. package/registry/openapi.yaml +311 -0
  22. package/registry/readme.md +107 -208
  23. package/registry/registry-template.html +4 -1
  24. package/registry/registry.js +198 -19
  25. package/server.js +19 -5
  26. package/testing/openapi-schemas.config.js +58 -0
  27. package/testing/openapi-schemas.json +1920 -0
  28. package/testing/openapi.js +15 -0
  29. package/testing/openapi.yaml +273 -0
  30. package/testing/readme.md +43 -1
  31. package/testing/search.js +1 -1
  32. package/testing/testing-template.html +2 -0
  33. package/testing/testing.js +24 -0
  34. package/translations/Messages.properties +1 -0
  35. package/tx/README.md +78 -4
  36. package/tx/cm/cm-api.js +25 -10
  37. package/tx/cm/cm-database.js +10 -5
  38. package/tx/cm/cm-package.js +61 -15
  39. package/tx/cs/cs-provider-api.js +22 -7
  40. package/tx/cs/cs-provider-list.js +10 -9
  41. package/tx/cs/cs-rxnorm.js +23 -6
  42. package/tx/html/tx-template.html +2 -0
  43. package/tx/library/canonical-resource.js +50 -1
  44. package/tx/library/codesystem.js +1 -0
  45. package/tx/library/conceptmap.js +1 -0
  46. package/tx/library/resource-ids.js +107 -0
  47. package/tx/library/valueset.js +16 -0
  48. package/tx/library.js +113 -26
  49. package/tx/ocl/cm-ocl.cjs +23 -15
  50. package/tx/ocl/cs-ocl.cjs +9 -10
  51. package/tx/ocl/vs-ocl.cjs +17 -17
  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 +90 -14
  60. package/tx/tx-html.js +6 -0
  61. package/tx/tx.fhir.org.yml +16 -8
  62. package/tx/tx.js +62 -1
  63. package/tx/vs/vs-api.js +25 -10
  64. package/tx/vs/vs-database.js +14 -9
  65. package/tx/vs/vs-package.js +58 -31
  66. package/tx/vs/vs-vsac.js +20 -5
  67. package/tx/workers/expand.js +35 -16
  68. package/tx/workers/metadata.js +2 -14
  69. package/tx/workers/read.js +3 -18
  70. package/tx/workers/validate.js +65 -19
  71. package/tx/workers/worker.js +29 -8
  72. package/utilities/extract-schema-json.js +167 -0
  73. package/utilities/generate-openapi-schemas.js +49 -0
  74. package/xig/xig.js +2 -4
@@ -0,0 +1,15 @@
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 testing module's OpenAPI description: openapi.yaml (the paths, written by hand) plus
8
+ // openapi-schemas.json (the FHIR resource schemas, generated from the R5 StructureDefinitions
9
+ // by utilities/generate-openapi-schemas.js - see openapi-schemas.config.js).
10
+
11
+ const path = require('path');
12
+ const { createOpenApiDoc } = require('../library/openapi-doc');
13
+
14
+ module.exports = createOpenApiDoc(path.join(__dirname, 'openapi.yaml'), '/testing',
15
+ { schemasPath: path.join(__dirname, 'openapi-schemas.json') });
@@ -0,0 +1,273 @@
1
+ # OpenAPI description of the FHIRsmith testing module (/testing): a FHIR API for TestReports.
2
+ #
3
+ # The paths are maintained here by hand. The FHIR resource schemas (TestReport,
4
+ # OperationOutcome, Bundle, the datatypes they use, and TestReportSearchBundle) are generated
5
+ # from the R5 StructureDefinitions into openapi-schemas.json, and merged into
6
+ # components.schemas when the spec is served - see openapi-schemas.config.js.
7
+ #
8
+ # tests/testing/openapi.test.js keeps this honest: every route must be described here or
9
+ # explicitly excluded, the search parameters must be the ones search.js understands, and
10
+ # the TestReport schema must require everything validateReport() in testing.js requires.
11
+ #
12
+ # Administrative routes (DELETE, the login and admin pages) are deliberately not described.
13
+ # info.version is filled in from package.json when the spec is served.
14
+
15
+ openapi: 3.1.0
16
+ info:
17
+ title: FHIRsmith Test Reports
18
+ version: "0.0.0"
19
+ summary: A FHIR repository for TestReports
20
+ description: |
21
+ Receives FHIR TestReports - from TxTester, and from any other tool that produces them -
22
+ and makes them available for reading and searching. This is a small FHIR R5 server with
23
+ one resource type, TestReport, and three interactions: create, read and search.
24
+
25
+ * **R4 and R5** TestReports are accepted alike (they differ only in `testScript`).
26
+ * **Reports are immutable**: the server assigns the id and `meta.lastUpdated`, drops
27
+ `meta.versionId`, and each POST is a new report, even of the same report.
28
+ * **Contained resources are not accepted.**
29
+ * The server checks only what the TestReport schema marks as required (and that
30
+ `issued` is a valid dateTime and `score` a number). The schema describes a valid
31
+ report; reports that don't conform to it in other ways may still be accepted, but
32
+ shouldn't be sent.
33
+
34
+ **Content negotiation.** Read and search return an HTML page to a browser (an
35
+ `Accept` containing `text/html`); `_format=json` or `_format=html` overrides that.
36
+ license:
37
+ name: BSD-3-Clause
38
+ url: https://opensource.org/license/bsd-3-clause
39
+ externalDocs:
40
+ description: The TestReport resource (FHIR R5)
41
+ url: https://hl7.org/fhir/R5/testreport.html
42
+ servers:
43
+ - url: /testing
44
+
45
+ paths:
46
+ /TestReport:
47
+ post:
48
+ tags: [TestReport]
49
+ operationId: createTestReport
50
+ summary: Submit a report
51
+ description: |
52
+ Stores the report and returns it with its new id and `meta.lastUpdated`. Reports
53
+ are limited in size (500 KB by default) and submissions are rate limited.
54
+
55
+ If the server is configured with a token, every submission must carry it, in the
56
+ `Authorization` header by default (`Bearer <token>`; the header is configurable).
57
+ security:
58
+ - {}
59
+ - submitToken: []
60
+ parameters:
61
+ - name: Prefer
62
+ in: header
63
+ description: '`return=minimal` (no body), `return=representation` (the default), or `return=OperationOutcome`.'
64
+ schema: { type: string }
65
+ example: return=minimal
66
+ requestBody:
67
+ required: true
68
+ content:
69
+ application/fhir+json:
70
+ schema: { $ref: '#/components/schemas/TestReport' }
71
+ application/json:
72
+ schema: { $ref: '#/components/schemas/TestReport' }
73
+ responses:
74
+ '201':
75
+ description: Stored. The body is the stored report, nothing, or an OperationOutcome, as `Prefer` asks.
76
+ headers:
77
+ Location:
78
+ description: The new report's URL
79
+ schema: { type: string, format: uri }
80
+ Last-Modified:
81
+ schema: { type: string }
82
+ content:
83
+ application/fhir+json:
84
+ schema:
85
+ anyOf:
86
+ - $ref: '#/components/schemas/TestReport'
87
+ - $ref: '#/components/schemas/OperationOutcome'
88
+ '400': { $ref: '#/components/responses/Invalid' }
89
+ '401':
90
+ description: A token is required and wasn't supplied, or is wrong
91
+ content:
92
+ application/fhir+json:
93
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
94
+ '413':
95
+ description: The report is too big
96
+ content:
97
+ application/fhir+json:
98
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
99
+ '415':
100
+ description: The body isn't JSON
101
+ content:
102
+ application/fhir+json:
103
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
104
+ '429':
105
+ description: Too many submissions; try again later
106
+ content:
107
+ application/fhir+json:
108
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
109
+
110
+ get:
111
+ tags: [TestReport]
112
+ operationId: searchTestReports
113
+ summary: Search reports
114
+ description: |
115
+ Returns a searchset Bundle, newest received first unless `_sort` says otherwise.
116
+ `total` is always present.
117
+
118
+ Comma separated values are ORed; repeated parameters are ANDed. The search
119
+ parameters aren't yet formally defined as SearchParameter resources; their
120
+ behaviour is as described here. Modifiers are written after the name
121
+ (`name:exact=...`).
122
+
123
+ Unknown parameters are ignored, with a warning OperationOutcome entry
124
+ (`search.mode = outcome`) in the Bundle - unless the request has
125
+ `Prefer: handling=strict`, when they're a 400. Values that can't be understood (a
126
+ bad date or number) are always a 400.
127
+ parameters:
128
+ - name: _id
129
+ in: query
130
+ description: The report's id.
131
+ schema: { type: string }
132
+ - name: name
133
+ in: query
134
+ description: '`name` starts with this, case-insensitively. Modifiers: `:exact`, `:contains`, `:missing`.'
135
+ schema: { type: string }
136
+ example: tx-ecosystem
137
+ - name: tester
138
+ in: query
139
+ description: '`tester` starts with this, case-insensitively. Modifiers: `:exact`, `:contains`, `:missing`.'
140
+ schema: { type: string }
141
+ - name: status
142
+ in: query
143
+ description: '`status`, exactly (a `system|` prefix is ignored). Modifiers: `:not`, `:missing`.'
144
+ schema: { type: string }
145
+ example: completed
146
+ - name: result
147
+ in: query
148
+ description: '`result`, exactly. Modifiers: `:not`, `:missing`.'
149
+ schema: { type: string }
150
+ example: fail
151
+ - name: testscript
152
+ in: query
153
+ description: '`testScript` (the R5 canonical, or the R4 reference), exactly. Modifiers: `:below` (starts with), `:missing`.'
154
+ schema: { type: string }
155
+ - name: participant
156
+ in: query
157
+ description: 'The `uri` of any participant, exactly. Modifier: `:below` (starts with).'
158
+ schema: { type: string }
159
+ example: http://tx.fhir.org/r4
160
+ - name: score
161
+ in: query
162
+ description: '`score`, with a prefix (`eq` `ne` `gt` `lt` `ge` `le` `sa` `eb` `ap`); `eq` has implicit precision (`90` means 89.5 to 90.5). Modifier: `:missing`.'
163
+ schema: { type: string }
164
+ example: ge90
165
+ - name: issued
166
+ in: query
167
+ description: '`issued`, with a prefix; dates are ranges (`issued=2026-09` is all of September).'
168
+ schema: { type: string }
169
+ example: ge2026-09
170
+ - name: _lastUpdated
171
+ in: query
172
+ description: When the report was received, with a prefix.
173
+ schema: { type: string }
174
+ - name: _sort
175
+ in: query
176
+ description: Any of the parameters above except `_id`, comma separated, `-` for descending.
177
+ schema: { type: string }
178
+ example: -issued
179
+ - name: _count
180
+ in: query
181
+ description: Page size; defaults to 50, at most 500. `0` returns just the total.
182
+ schema: { type: integer, minimum: 0, maximum: 500 }
183
+ - name: _offset
184
+ in: query
185
+ description: Where the page starts. The Bundle has first, previous, next and last links.
186
+ schema: { type: integer, minimum: 0 }
187
+ - name: _summary
188
+ in: query
189
+ description: '`count` returns just the total.'
190
+ schema: { type: string, enum: [count] }
191
+ - name: _format
192
+ in: query
193
+ description: '`json` or `html`, overriding the `Accept` header.'
194
+ schema: { type: string }
195
+ - name: _total
196
+ in: query
197
+ description: Accepted, and has no effect - the total is always returned.
198
+ schema: { type: string }
199
+ - name: _pretty
200
+ in: query
201
+ description: Accepted, and has no effect - responses are always indented.
202
+ schema: { type: string }
203
+ - name: Prefer
204
+ in: header
205
+ description: '`handling=strict` makes unknown parameters an error.'
206
+ schema: { type: string }
207
+ responses:
208
+ '200':
209
+ description: The matching reports
210
+ content:
211
+ application/fhir+json:
212
+ schema: { $ref: '#/components/schemas/TestReportSearchBundle' }
213
+ text/html:
214
+ schema: { type: string }
215
+ '400': { $ref: '#/components/responses/Invalid' }
216
+
217
+ /TestReport/{id}:
218
+ get:
219
+ tags: [TestReport]
220
+ operationId: readTestReport
221
+ summary: Read a report
222
+ parameters:
223
+ - name: id
224
+ in: path
225
+ required: true
226
+ schema: { type: string }
227
+ responses:
228
+ '200':
229
+ description: The report, or a rendering of it for a browser
230
+ headers:
231
+ Last-Modified:
232
+ schema: { type: string }
233
+ content:
234
+ application/fhir+json:
235
+ schema: { $ref: '#/components/schemas/TestReport' }
236
+ text/html:
237
+ schema: { type: string }
238
+ '404':
239
+ description: No such report
240
+ content:
241
+ application/fhir+json:
242
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
243
+
244
+ /metadata:
245
+ get:
246
+ tags: [server]
247
+ operationId: capabilities
248
+ summary: The server's CapabilityStatement
249
+ description: |
250
+ Lists the interactions and search parameters. The search parameters have no
251
+ definitions (see the search operation for what they do).
252
+ responses:
253
+ '200':
254
+ description: A CapabilityStatement
255
+ content:
256
+ application/fhir+json:
257
+ schema: { $ref: '#/components/schemas/AnyResource' }
258
+
259
+ components:
260
+ securitySchemes:
261
+ submitToken:
262
+ type: http
263
+ scheme: bearer
264
+ description: |
265
+ Required for submitting reports if the server is configured with a token. Sent in the
266
+ `Authorization` header by default; a server can be configured to use another header.
267
+
268
+ responses:
269
+ Invalid:
270
+ description: The request isn't acceptable - the issues say why
271
+ content:
272
+ application/fhir+json:
273
+ schema: { $ref: '#/components/schemas/OperationOutcome' }
package/testing/readme.md CHANGED
@@ -13,7 +13,9 @@ makes them available through a FHIR API and a set of web pages. It runs at `/tes
13
13
  R4 and R5 TestReports are treated the same. A report is accepted if it is JSON, has
14
14
  `resourceType: TestReport`, and has `name`, `status`, `result`, `tester`, `issued` (a valid
15
15
  dateTime) and at least one `participant` with a `uri`. `score`, if present, must be a number.
16
- Nothing else is checked.
16
+ Contained resources are not accepted. Nothing else is checked - but the TestReport schema in
17
+ the OpenAPI description (below) says what a valid report looks like, and reports should
18
+ conform to it.
17
19
 
18
20
  The server gives the report a new id (any id sent is ignored), sets `meta.lastUpdated` to the
19
21
  time it was received, and drops `meta.versionId`: reports can't be changed once received, so
@@ -65,6 +67,46 @@ Unknown parameters are ignored, with a warning OperationOutcome in the Bundle, u
65
67
  request has `Prefer: handling=strict`, in which case they're a 400. Values that can't be
66
68
  understood (a bad date or number) are always a 400.
67
69
 
70
+ ## OpenAPI description
71
+
72
+ The FHIR API (create, read, search, metadata) is described by an OpenAPI 3.1 spec:
73
+
74
+ | URL | |
75
+ |---|---|
76
+ | `/testing/openapi` | Browsable reference (HTML), with a "try it" form for each GET operation |
77
+ | `/testing/openapi.json` | The spec as JSON |
78
+ | `/testing/openapi.yaml` | The hand-written part, as YAML ([openapi.yaml](openapi.yaml)) |
79
+
80
+ Every response carries a `Link: </testing/openapi.json>; rel="service-desc"` header (RFC 8631),
81
+ and the HTML pages carry the matching `<link>` element. Deleting reports and the
82
+ administration pages are deliberately not described.
83
+
84
+ The spec is in two parts:
85
+
86
+ * [openapi.yaml](openapi.yaml) - the paths, parameters and responses, written by hand
87
+ * [openapi-schemas.json](openapi-schemas.json) - the FHIR schemas (TestReport, Bundle,
88
+ OperationOutcome, the datatypes they use, and `TestReportSearchBundle`), generated from the R5
89
+ StructureDefinitions by `library/fhir-openapi-schema.js`, as configured in
90
+ [openapi-schemas.config.js](openapi-schemas.config.js). The loader merges them into the spec.
91
+
92
+ The generated schemas are closed (no properties FHIR doesn't define), include the `_x`
93
+ siblings of primitive elements (so extensions on primitives, like `_name`, are allowed), and
94
+ use the codes of required bindings as enums. Recursion is cut at two boundaries: an extension's
95
+ value isn't described, and a resource inside another (`Bundle.entry.resource`) is "any resource"
96
+ unless the config narrows it. `contained` is left out, so it isn't allowed. The config's overlay
97
+ adds what this server requires beyond the base resource, and lets `testScript` be an R4
98
+ Reference.
99
+
100
+ After changing the config, regenerate (this needs `hl7.fhir.r5.core#5.0.0` in the
101
+ terminology cache):
102
+
103
+ node utilities/generate-openapi-schemas.js testing
104
+
105
+ `tests/testing/openapi.test.js` fails if the generated file is out of date, if a route or search
106
+ parameter isn't described (or excluded), or if the TestReport schema doesn't require everything
107
+ `validateReport()` requires. It also validates what the server actually returns (a stored
108
+ report, a search Bundle, an OperationOutcome) against the schemas, using ajv.
109
+
68
110
  ## Web pages
69
111
 
70
112
  * `/testing` - the list of reports, with filters (these map onto the search parameters above),
package/testing/search.js CHANGED
@@ -374,5 +374,5 @@ function capabilitySearchParams() {
374
374
 
375
375
  module.exports = {
376
376
  parseSearch, dateRange, splitValues, capabilitySearchParams,
377
- PARAMS, DEFAULT_COUNT, MAX_COUNT
377
+ PARAMS, CONTROL, DEFAULT_COUNT, MAX_COUNT
378
378
  };
@@ -9,6 +9,7 @@
9
9
  <meta content="http://hl7.org/fhir" name="author"/>
10
10
  <meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
11
11
 
12
+ <link rel="service-desc" type="application/json" href="/testing/openapi.json"/>
12
13
  <link rel="stylesheet" href="/fhir.css"/>
13
14
 
14
15
 
@@ -59,6 +60,7 @@
59
60
  <a href="/" style="color: gold"> Server Home</a> &nbsp;|&nbsp;
60
61
  <a href="/testing" style="color: gold">Test Reports</a> &nbsp;|&nbsp;
61
62
  <a href="/testing/summary" style="color: gold">Summary</a> &nbsp;|&nbsp;
63
+ <a href="/testing/openapi" style="color: gold">API</a>
62
64
  </div> <!-- /container -->
63
65
  </div> <!-- /container -->
64
66
  </div>
@@ -46,6 +46,7 @@ const { requireSameOrigin } = require('../library/same-origin');
46
46
  const packageJson = require('../package.json');
47
47
  const { TestReportStore } = require('./store');
48
48
  const { parseSearch, dateRange, capabilitySearchParams } = require('./search');
49
+ const testingOpenApi = require('./openapi');
49
50
  const {
50
51
  renderList, renderSummary, renderReport, renderLogin, renderLinks, renderUsers, listQueryToSearch
51
52
  } = require('./render');
@@ -94,6 +95,11 @@ function validateReport(r) {
94
95
  if (r.meta !== undefined && (r.meta === null || typeof r.meta !== 'object' || Array.isArray(r.meta))) {
95
96
  problems.push('TestReport.meta must be an object');
96
97
  }
98
+ // contained resources aren't accepted: nothing in a report needs them, and allowing them
99
+ // would mean describing (and rendering) any resource at all
100
+ if (r.contained !== undefined) {
101
+ problems.push('TestReport.contained is not allowed: reports may not contain resources');
102
+ }
97
103
  return problems;
98
104
  }
99
105
 
@@ -223,6 +229,24 @@ class TestingModule {
223
229
  }));
224
230
  }
225
231
 
232
+ // RFC 8631: where to find the machine-readable description of this API
233
+ r.use((req, res, next) => {
234
+ res.setHeader('Link', `<${req.baseUrl}/openapi.json>; rel="service-desc", <${req.baseUrl}/openapi>; rel="service-doc"`);
235
+ next();
236
+ });
237
+
238
+ // the OpenAPI description: /openapi.json, /openapi.yaml, and /openapi (an HTML reference
239
+ // for browsers, the JSON otherwise)
240
+ r.get('/openapi.json', (req, res) => this.handle(req, res, 'openapi', () => res.type('application/json').send(testingOpenApi.getJson())));
241
+ r.get('/openapi.yaml', (req, res) => this.handle(req, res, 'openapi', () =>
242
+ res.set('Content-Type', 'application/yaml').send(testingOpenApi.getYaml())));
243
+ r.get('/openapi', (req, res) => this.handle(req, res, 'openapi', () => {
244
+ if (!wantsHtml(req)) {
245
+ return res.type('application/json').send(testingOpenApi.getJson());
246
+ }
247
+ return this.sendHtml(res, 'Test Report API', testingOpenApi.renderHtml(), Date.now());
248
+ }));
249
+
226
250
  r.get('/', ...this.web, (req, res) => this.handle(req, res, 'list', () => this.htmlList(req, res)));
227
251
  r.get('/summary', ...this.web, (req, res) => this.handle(req, res, 'summary', () => this.htmlSummary(req, res)));
228
252
  r.get('/metadata', (req, res) => this.handle(req, res, 'metadata', () => this.metadata(req, res)));
@@ -1288,6 +1288,7 @@ VALUESET_INCLUDE_CSVER_NOT_FOUND = No matching contained code system found for s
1288
1288
  VALUESET_INCLUDE_CSVER_SUPPLEMENT = The value set references CodeSystem ''{0}'' version ''{2}'' which is a supplement. It must reference the underlying CodeSystem ''{1}'' and use the http://hl7.org/fhir/StructureDefinition/valueset-supplement extension for the supplement
1289
1289
  VALUESET_INCLUDE_INVALID_CONCEPT_CODE = The code ''{1}'' is not valid in the system {0} ({2})
1290
1290
  VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER = The code ''{2}'' is not valid in the system {0} version {1} ({2})
1291
+ VALUESET_NO_COMPOSE = The value set {0} has no compose and no expansion, so it cannot be expanded
1291
1292
  VALUESET_INCLUDE_SYSTEM_ABSOLUTE = URI values in ValueSet.compose.include.system must be absolute
1292
1293
  VALUESET_INCLUDE_SYSTEM_ABSOLUTE_FRAG = URI values in ValueSet.compose.include.system must be absolute. To reference a contained code system, use the full CodeSystem URL and reference it using the http://hl7.org/fhir/StructureDefinition/valueset-system extension
1293
1294
  VALUESET_INCLUDE_WRONG_CS_OID = It is not valid to refer to a CodeSystem by an identifier like this ''{0}'' - use ''{1}''
package/tx/README.md CHANGED
@@ -148,6 +148,64 @@ The file is read from disk per request, so editing it is reflected immediately -
148
148
  this means the published file can be ahead of what the running server actually loaded, if it
149
149
  has been edited since startup.
150
150
 
151
+ ## OpenAPI description
152
+
153
+ Each R5 endpoint is described by an OpenAPI 3.1 spec (the R4 and R3 endpoints work the same
154
+ way, with that version's resources, and aren't described separately):
155
+
156
+ | URL | |
157
+ |---|---|
158
+ | `/{path}/openapi` | Browsable reference (HTML, in the tx page template), with a "try it" form for each GET operation |
159
+ | `/{path}/openapi.json` | The spec as JSON |
160
+ | `/{path}/openapi.yaml` | The spec as YAML |
161
+
162
+ `servers` is the endpoint. Every response from an R5 endpoint carries a
163
+ `Link: </{path}/openapi.json>; rel="service-desc", </{path}/openapi>; rel="service-doc"` header
164
+ (RFC 8631), the tx pages carry the matching `<link>` element and an **API** item in the
165
+ navigation bar, and the server home page links to it. The HTML pages (`/`, `/op.html`, `/ecl`,
166
+ `/problems.html`, `/info`, `/library`) aren't described.
167
+
168
+ The spec is in three parts, merged when it's served ([openapi.js](openapi.js)):
169
+
170
+ * [openapi.yaml](openapi.yaml) - the overview, `/metadata`, `/$versions`, and the shared
171
+ components, written by hand
172
+ * [openapi-operations.js](openapi-operations.js) - the terminology operations, as data: their
173
+ in and out parameters, with types, cardinalities and descriptions. openapi.js makes the paths
174
+ from them (type and instance level; GET with the primitive parameters in the query, POST with
175
+ a Parameters resource or a form) and a table of the parameters for each. The parameters
176
+ TxParameters reads for most operations (tx-resource, cache-id, the version rules,
177
+ displayLanguage, ...) are listed once, in the overview. The read and search paths are made
178
+ there too, from the search worker's parameters
179
+ * [openapi-schemas.json](openapi-schemas.json) - the FHIR R5 schemas, generated from the
180
+ StructureDefinitions by `library/fhir-openapi-schema.js`, as configured in
181
+ [openapi-schemas.config.js](openapi-schemas.config.js)
182
+
183
+ The schemas are documentation: they describe what the server handles and returns, but the
184
+ server doesn't reject content outside them. They describe:
185
+
186
+ * CodeSystem, ValueSet, ConceptMap, OperationOutcome, Parameters, Bundle, CapabilityStatement
187
+ and TerminologyCapabilities, closed, with the `_x` siblings of primitive elements and
188
+ `modifierExtension` wherever FHIR allows them
189
+ * contained resources only in a ValueSet, and only ValueSets
190
+ * extension and parameter values: the primitive types (not `base64Binary`), `Coding` and
191
+ `CodeableConcept`
192
+ * resources in a Bundle or a Parameters: CodeSystem, ValueSet, ConceptMap, OperationOutcome -
193
+ and Parameters, in a Parameters, for `$batch-validate-code` and `profile`. Never a Bundle
194
+ * OperationOutcome issues with `details.text`, and (except for information) a tx-issue-type
195
+ coding
196
+
197
+ After changing the operations, `openapi.js` picks them up. After changing the schema config,
198
+ regenerate (this needs `hl7.fhir.r5.core#5.0.0` in the terminology cache):
199
+
200
+ node utilities/generate-openapi-schemas.js tx
201
+
202
+ `tests/tx/openapi.test.js` fails if a route isn't described (or excluded, with a reason), if a
203
+ worker or TxParameters reads a parameter that isn't described - or a described one isn't read
204
+ anywhere - or if the generated schemas are out of date. It also validates what the server
205
+ actually returns (metadata, read and search, `$expand`, `$validate-code`, `$lookup`,
206
+ `$subsumes`, `$batch-validate-code`, `$versions`, `$cache-control`, an error) against the
207
+ schemas, using ajv. So a new parameter needs adding to openapi-operations.js.
208
+
151
209
  ## Library Configuration
152
210
 
153
211
  The library - the source to load - is configured using a YAML file.
@@ -179,12 +237,28 @@ shouldn't be - too big. Note that that files in the list below are considered to
179
237
  Each source entry follows the format:
180
238
 
181
239
  ```
182
- type[!]:details
240
+ type[!]:details[=prefix]
183
241
  ```
184
242
 
185
243
  - **type**: The source type (see Source Types below)
186
244
  - **!** (optional): Append to type to mark this source as the default for its code system. When multiple versions of the same code system are loaded, the default is used when no version is specified.
187
245
  - **details**: Type-specific configuration (filename, package name, or internal provider name)
246
+ - **prefix** (optional): The id prefix for the resources the source provides - see [Resource Ids](#resource-ids). Everything after the last `=` in the entry is the prefix, so a `url` source whose url contains a `=` must give one.
247
+
248
+ ### Resource Ids
249
+
250
+ Every CodeSystem, ValueSet and ConceptMap is served with an id in the id space of the source it came from: the source's prefix, `-`, and the id the resource has in the source. With
251
+
252
+ ```yaml
253
+ - npm:hl7.terminology=tho
254
+ ```
255
+
256
+ THO's v3-ActCode code system is `CodeSystem/tho-v3-ActCode`. No resource is served on the id it has in its source - different sources can use the same ids (hl7.terminology and the FHIR core packages both have a CodeSystem `v3-MaritalStatus`), so an unprefixed id can't say which one is meant, and it finds nothing.
257
+
258
+ - A prefix may contain letters, digits and `.` (no `-`), up to 32 characters, and each prefix may only be given once
259
+ - If a source isn't given a prefix, it gets a number. Numbers depend on the order of the sources (and on which sources provide which kinds of resource), so they change when the YAML changes. Give every source that provides resources a prefix if its ids should stay the same - i.e. on any server where people link to resources by id
260
+ - `core` is the FHIR core package each endpoint loads for its FHIR version (`CodeSystem/core-administrative-gender`), and `x` is the code systems the server implements natively (`CodeSystem/x-...`); neither can be given as a prefix
261
+ - Sources that only provide code systems natively (`internal`, `snomed`, `loinc` etc.) don't serve resources by id, so a prefix does nothing for them
188
262
 
189
263
  ### Source Types
190
264
 
@@ -406,9 +480,9 @@ sources:
406
480
  - snomed:sct_us_20250901.cache
407
481
 
408
482
  # FHIR packages
409
- - npm:hl7.terminology
410
- - npm:hl7.fhir.us.core
411
- - npm:us.nlm.vsac
483
+ - npm:hl7.terminology=tho
484
+ - npm:hl7.fhir.us.core=us
485
+ - npm:us.nlm.vsac=vsac
412
486
  ```
413
487
 
414
488
  Also see tx.fhir.org.yml for the production configuration for tx.fhir.org
package/tx/cm/cm-api.js CHANGED
@@ -1,22 +1,37 @@
1
+ const { assignIdSpace } = require('../library/resource-ids');
2
+
1
3
  /**
2
4
  * Abstract base class for Concept Map providers
3
5
  * Defines the interface that all Concept Map providers must implement
4
6
  */
5
7
  class AbstractConceptMapProvider {
6
8
  /**
7
- * {int} Unique number assigned to this provider
9
+ * {string} The prefix of this provider's id space (set by assignIds) - given for its source in the library
10
+ * YAML, autonumbered, or 'core' for an endpoint's core package. Every concept map this
11
+ * provider provides has an id that starts with spaceId + '-' (see tx/library/resource-ids.js)
8
12
  */
9
13
  spaceId;
10
14
 
11
15
  /**
12
- * ensure that the ids on the Concept Maps are unique, if they are
13
- * in the global namespace
16
+ * Put this provider in an id space: set spaceId, and prefix the ids of the concept maps it
17
+ * provides with spaceId + '-'. A provider is put in one id space, once - asking for a
18
+ * different one later is an error (see assignIdSpace)
14
19
  *
15
- * @param {Set<String>} ids
20
+ * @param {string} spaceId
16
21
  */
17
- // eslint-disable-next-line no-unused-vars
18
- assignIds(ids) {
19
- throw new Error('assignIds must be implemented by AbstractConceptMapProvider subclass');
22
+ assignIds(spaceId) {
23
+ if (assignIdSpace(this, spaceId)) {
24
+ this.prefixIds();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Prefix the ids of the concept maps this provider provides with spaceId + '-', once spaceId
30
+ * is set (by assignIds). Concept maps the provider loads later must be given ids
31
+ * in the same space.
32
+ */
33
+ prefixIds() {
34
+ throw new Error('prefixIds must be implemented by AbstractConceptMapProvider subclass');
20
35
  }
21
36
 
22
37
  /**
@@ -32,11 +47,11 @@ class AbstractConceptMapProvider {
32
47
  }
33
48
 
34
49
  /**
35
- * Fetches a specific Concept Map by id. ConceptMap providers must enforce that Concept Map ids are unique
36
- * either globally (as enforced by assignIds) or in their space
50
+ * Fetches a specific Concept Map by id. The id is in this provider's space (spaceId + '-' +
51
+ * the concept map's own id); an id that isn't - including an unprefixed one - finds nothing
37
52
  *
38
53
  * @param {string} id - The id of the Concept Map
39
- * @returns {Promise<ConceptMap>} The requested Concept Map
54
+ * @returns {Promise<ConceptMap>} The requested Concept Map, or null
40
55
  * @throws {Error} Must be implemented by subclasses
41
56
  */
42
57
  // eslint-disable-next-line no-unused-vars
@@ -349,7 +349,16 @@ class ConceptMapDatabase {
349
349
  this.cmCount = rows.length;
350
350
 
351
351
  for (const row of rows) {
352
- const conceptMap = new ConceptMap(JSON.parse(row.content));
352
+ let conceptMap;
353
+ try {
354
+ conceptMap = new ConceptMap(JSON.parse(row.content));
355
+ } catch (e) {
356
+ // one unacceptable concept map must not take the rest with it
357
+ require('../../library/logger').getInstance().child({ module: 'tx' })
358
+ .warn(`ConceptMap database: skipping ConceptMap ${row.url || row.id}: ${e.message}`);
359
+ this.cmCount--;
360
+ continue;
361
+ }
353
362
 
354
363
  // Store by URL and id alone
355
364
  conceptMapMap.set(row.url, conceptMap);
@@ -724,10 +733,6 @@ class ConceptMapDatabase {
724
733
  return { query, params };
725
734
  }
726
735
 
727
- // eslint-disable-next-line no-unused-vars
728
- assignIds(ids) {
729
- // nothing - we don't do any assigning.
730
- }
731
736
  }
732
737
 
733
738
  module.exports = {