fhirsmith 0.14.1 → 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 +35 -0
- package/README.md +3 -2
- package/config-template.json +9 -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 +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 +180 -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 +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/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/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 +35 -16
- package/tx/workers/metadata.js +0 -13
- 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
|
@@ -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
|
@@ -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> |
|
|
60
61
|
<a href="/testing" style="color: gold">Test Reports</a> |
|
|
61
62
|
<a href="/testing/summary" style="color: gold">Summary</a> |
|
|
63
|
+
<a href="/testing/openapi" style="color: gold">API</a>
|
|
62
64
|
</div> <!-- /container -->
|
|
63
65
|
</div> <!-- /container -->
|
|
64
66
|
</div>
|
package/testing/testing.js
CHANGED
|
@@ -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.
|
package/tx/cm/cm-database.js
CHANGED
|
@@ -349,7 +349,16 @@ class ConceptMapDatabase {
|
|
|
349
349
|
this.cmCount = rows.length;
|
|
350
350
|
|
|
351
351
|
for (const row of rows) {
|
|
352
|
-
|
|
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);
|
package/tx/cm/cm-package.js
CHANGED
|
@@ -63,7 +63,14 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
63
63
|
for (const entry of conceptMapEntries) {
|
|
64
64
|
const conceptMap = await this.packageLoader.loadFile(entry);
|
|
65
65
|
if (conceptMap.url) {
|
|
66
|
-
|
|
66
|
+
try {
|
|
67
|
+
conceptMaps.push(new ConceptMap(conceptMap, this.packageLoader.fhirVersion())); // get in converted to R5 format
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// one unacceptable concept map (e.g. one with contained resources, which the
|
|
70
|
+
// server doesn't support) must not stop the rest of the package loading
|
|
71
|
+
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
72
|
+
.warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ConceptMap ${conceptMap.url}: ${e.message}`);
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
76
|
|
package/tx/cs/cs-rxnorm.js
CHANGED
|
@@ -36,12 +36,27 @@ class RxNormPrep {
|
|
|
36
36
|
|
|
37
37
|
// Iterator context
|
|
38
38
|
class RxNormIteratorContext {
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* @param {string|null} query - the SQL that produces the rows, or null for an iterator with no rows
|
|
41
|
+
* @param {object} params - the query parameters
|
|
42
|
+
* @param {number} [total] - the number of rows the query will return, if known. The expander
|
|
43
|
+
* reads it to refuse an over-limit whole-code-system expansion before it is materialised
|
|
44
|
+
*/
|
|
45
|
+
constructor(query, params = {}, total = undefined) {
|
|
40
46
|
this.query = query;
|
|
41
47
|
this.params = params;
|
|
42
48
|
this.cursor = 0;
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
if (query) {
|
|
50
|
+
this.results = null;
|
|
51
|
+
this.executed = false;
|
|
52
|
+
this.total = total;
|
|
53
|
+
} else {
|
|
54
|
+
// nothing to run: an empty statement isn't a no-op in node-sqlite3, it fails with
|
|
55
|
+
// "SQLITE_MISUSE: not an error"
|
|
56
|
+
this.results = [];
|
|
57
|
+
this.executed = true;
|
|
58
|
+
this.total = 0;
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
more() {
|
|
@@ -288,10 +303,12 @@ class RxNormServices extends CodeSystemProvider {
|
|
|
288
303
|
if (!context) {
|
|
289
304
|
// Iterate all codes
|
|
290
305
|
const query = `SELECT ${this.getCodeField()}, STR FROM rxnconso WHERE SAB = ? AND TTY <> 'SY' ORDER BY ${this.getCodeField()}`;
|
|
291
|
-
return new RxNormIteratorContext(query, { sab: this.getSAB() });
|
|
306
|
+
return new RxNormIteratorContext(query, { sab: this.getSAB() }, this.totalCodeCount);
|
|
292
307
|
} else {
|
|
293
|
-
// No hierarchical iteration for specific contexts in this implementation
|
|
294
|
-
|
|
308
|
+
// No hierarchical iteration for specific contexts in this implementation. The expander
|
|
309
|
+
// asks every concept for its children (includeCodeAndDescendants), so this has to be an
|
|
310
|
+
// iterator with no rows - not one with an empty query, which sqlite rejects
|
|
311
|
+
return new RxNormIteratorContext(null, {});
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
314
|
|
package/tx/html/tx-template.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
11
11
|
|
|
12
12
|
<link rel="stylesheet" href="/fhir.css"/>
|
|
13
|
+
[%api-head%]
|
|
13
14
|
|
|
14
15
|
<!-- Bootstrap core CSS -->
|
|
15
16
|
<link rel="stylesheet" href="/assets/css/bootstrap.css"/>
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
<a href="[%endpoint-path%]/op.html" style="color: gold">Operations</a> |
|
|
92
93
|
<a href="[%endpoint-path%]/ecl" style="color: gold">ECL</a> |
|
|
93
94
|
<a href="[%endpoint-path%]/problems.html" style="color: gold">Problems</a> |
|
|
95
|
+
[%api-link%]
|
|
94
96
|
[%library-link%]
|
|
95
97
|
|
|
96
98
|
</div>
|
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
const {VersionUtilities, VersionPrecision} = require("../../library/version-utilities");
|
|
2
|
+
const {Issue} = require("./operation-outcome");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contained resources. The terminology server supports exactly one use of them: a ValueSet
|
|
6
|
+
* that contains ValueSets, which its compose refers to as #id. Any other contained resource
|
|
7
|
+
* - a contained CodeSystem or ConceptMap, anything contained in a CodeSystem or ConceptMap,
|
|
8
|
+
* or a contained ValueSet that itself contains something - is rejected, whatever path the
|
|
9
|
+
* resource arrives by (request, tx-resource, cache, package). Supporting them would mean
|
|
10
|
+
* resolving references to them everywhere, and the terminology ecosystem only requires
|
|
11
|
+
* servers to support contained value sets in value sets.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} jsonObj - the resource (R5 form)
|
|
14
|
+
* @throws {Issue} if the resource contains anything not supported
|
|
15
|
+
*/
|
|
16
|
+
function checkContained(jsonObj) {
|
|
17
|
+
const contained = jsonObj ? jsonObj.contained : undefined;
|
|
18
|
+
if (contained === undefined || contained === null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const type = jsonObj.resourceType;
|
|
22
|
+
const which = `${type}${jsonObj.id ? '/' + jsonObj.id : ''}${jsonObj.url ? ' (' + jsonObj.url + ')' : ''}`;
|
|
23
|
+
if (!Array.isArray(contained)) {
|
|
24
|
+
throw new Issue('error', 'structure', `${type}.contained`, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
|
|
25
|
+
`${which}: contained must be an array`, 'invalid-data', 400);
|
|
26
|
+
}
|
|
27
|
+
contained.forEach((c, i) => {
|
|
28
|
+
const ct = c && typeof c === 'object' ? c.resourceType : undefined;
|
|
29
|
+
let problem = null;
|
|
30
|
+
if (type !== 'ValueSet') {
|
|
31
|
+
problem = `${which} contains a ${ct || 'resource'}: this server only supports ValueSets contained in a ValueSet`;
|
|
32
|
+
} else if (ct !== 'ValueSet') {
|
|
33
|
+
problem = `${which} contains a ${ct || 'resource with no resourceType'}: this server only supports ValueSets contained in a ValueSet`;
|
|
34
|
+
} else if (Array.isArray(c.contained) && c.contained.length > 0) {
|
|
35
|
+
problem = `${which}: the contained ValueSet${c.id ? ' #' + c.id : ''} itself contains resources, which is not allowed`;
|
|
36
|
+
}
|
|
37
|
+
if (problem) {
|
|
38
|
+
throw new Issue('error', 'not-supported', `${type}.contained[${i}]`, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
|
|
39
|
+
problem, 'not-supported', 400);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
2
43
|
|
|
3
44
|
/**
|
|
4
45
|
* Base class for metadata resources to provide common interface
|
|
@@ -31,6 +72,14 @@ class CanonicalResource {
|
|
|
31
72
|
this.fhirVersion = fhirVersion;
|
|
32
73
|
}
|
|
33
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Rejects contained resources this server doesn't support - see checkContained.
|
|
77
|
+
* Subclasses call this once the resource is in R5 form.
|
|
78
|
+
*/
|
|
79
|
+
checkContained() {
|
|
80
|
+
checkContained(this.jsonObj);
|
|
81
|
+
}
|
|
82
|
+
|
|
34
83
|
get resourceType() {
|
|
35
84
|
return this.jsonObj.resourceType;
|
|
36
85
|
}
|
|
@@ -163,4 +212,4 @@ class CanonicalResource {
|
|
|
163
212
|
}
|
|
164
213
|
}
|
|
165
214
|
|
|
166
|
-
module.exports = { CanonicalResource };
|
|
215
|
+
module.exports = { CanonicalResource, checkContained };
|