@medplum/fhir-router 2.0.13 → 2.0.15
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/dist/cjs/index.cjs +134 -149
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.min.cjs +1 -1
- package/dist/cjs/index.min.cjs.map +1 -1
- package/dist/esm/index.min.mjs +1 -1
- package/dist/esm/index.min.mjs.map +1 -1
- package/dist/esm/index.mjs +135 -150
- package/dist/esm/index.mjs.map +1 -1
- package/dist/types/repo.d.ts +2 -1
- package/package.json +1 -1
package/dist/esm/index.mjs
CHANGED
|
@@ -1,36 +1,7 @@
|
|
|
1
|
-
import { OperationOutcomeError, badRequest, normalizeOperationOutcome, parseSearchUrl, getStatus, isOk,
|
|
1
|
+
import { OperationOutcomeError, badRequest, normalizeOperationOutcome, parseSearchUrl, getReferenceString, getStatus, isOk, allOk, LRUCache, forbidden, getResourceTypes, getResourceTypeSchema, isResourceTypeSchema, getElementDefinition, buildTypeName, capitalize, getSearchParameters, Operator, parseSearchRequest, notFound, created, deepClone, matchesSearchRequest, globalSchema, evalFhirPath } from '@medplum/core';
|
|
2
2
|
import DataLoader from 'dataloader';
|
|
3
3
|
import { applyPatch } from 'rfc6902';
|
|
4
4
|
|
|
5
|
-
/******************************************************************************
|
|
6
|
-
Copyright (c) Microsoft Corporation.
|
|
7
|
-
|
|
8
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
9
|
-
purpose with or without fee is hereby granted.
|
|
10
|
-
|
|
11
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
12
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
13
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
14
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
15
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
16
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
17
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
18
|
-
***************************************************************************** */
|
|
19
|
-
|
|
20
|
-
function __classPrivateFieldGet(receiver, state, kind, f) {
|
|
21
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
22
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
23
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
27
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
28
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
29
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
30
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
var _BatchProcessor_instances, _BatchProcessor_ids, _BatchProcessor_processBatchEntry, _BatchProcessor_validateEntry, _BatchProcessor_parsePatchBody, _BatchProcessor_addReplacementId, _BatchProcessor_rewriteIds, _BatchProcessor_rewriteIdsInArray, _BatchProcessor_rewriteIdsInObject, _BatchProcessor_rewriteIdsInString;
|
|
34
5
|
/**
|
|
35
6
|
* Processes a FHIR batch request.
|
|
36
7
|
*
|
|
@@ -56,12 +27,10 @@ class BatchProcessor {
|
|
|
56
27
|
* @param bundle The input bundle.
|
|
57
28
|
*/
|
|
58
29
|
constructor(router, repo, bundle) {
|
|
59
|
-
_BatchProcessor_instances.add(this);
|
|
60
30
|
this.router = router;
|
|
61
31
|
this.repo = repo;
|
|
62
32
|
this.bundle = bundle;
|
|
63
|
-
|
|
64
|
-
__classPrivateFieldSet(this, _BatchProcessor_ids, {}, "f");
|
|
33
|
+
this.ids = {};
|
|
65
34
|
}
|
|
66
35
|
/**
|
|
67
36
|
* Processes a FHIR batch request.
|
|
@@ -83,9 +52,9 @@ class BatchProcessor {
|
|
|
83
52
|
}
|
|
84
53
|
const resultEntries = [];
|
|
85
54
|
for (const entry of entries) {
|
|
86
|
-
const rewritten =
|
|
55
|
+
const rewritten = this.rewriteIdsInObject(entry);
|
|
87
56
|
try {
|
|
88
|
-
resultEntries.push(await
|
|
57
|
+
resultEntries.push(await this.processBatchEntry(rewritten));
|
|
89
58
|
}
|
|
90
59
|
catch (err) {
|
|
91
60
|
resultEntries.push(buildBundleResponse(normalizeOperationOutcome(err)));
|
|
@@ -97,102 +66,108 @@ class BatchProcessor {
|
|
|
97
66
|
entry: resultEntries,
|
|
98
67
|
};
|
|
99
68
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Processes a single entry from a FHIR batch request.
|
|
71
|
+
* @param entry The bundle entry.
|
|
72
|
+
* @returns The bundle entry response.
|
|
73
|
+
*/
|
|
74
|
+
async processBatchEntry(entry) {
|
|
75
|
+
this.validateEntry(entry);
|
|
76
|
+
const request = entry.request;
|
|
77
|
+
if (entry.resource?.resourceType && request.ifNoneExist) {
|
|
78
|
+
const baseUrl = `https://example.com/${entry.resource.resourceType}`;
|
|
79
|
+
const searchUrl = new URL('?' + request.ifNoneExist, baseUrl);
|
|
80
|
+
const searchBundle = await this.repo.search(parseSearchUrl(searchUrl));
|
|
81
|
+
const entries = searchBundle?.entry;
|
|
82
|
+
if (entries.length > 1) {
|
|
83
|
+
return buildBundleResponse(badRequest('Multiple matches'));
|
|
84
|
+
}
|
|
85
|
+
if (entries.length === 1) {
|
|
86
|
+
const matchingResource = entries[0].resource;
|
|
87
|
+
if (entry.fullUrl) {
|
|
88
|
+
this.addReplacementId(entry.fullUrl, matchingResource);
|
|
89
|
+
}
|
|
90
|
+
return buildBundleResponse(allOk, matchingResource);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let body = entry.resource;
|
|
94
|
+
if (request.method === 'PATCH') {
|
|
95
|
+
body = this.parsePatchBody(entry);
|
|
96
|
+
}
|
|
97
|
+
// Pass in dummy host for parsing purposes.
|
|
98
|
+
// The host is ignored.
|
|
99
|
+
const url = new URL(request.url, 'https://example.com/');
|
|
100
|
+
const result = await this.router.handleRequest({
|
|
101
|
+
method: request.method,
|
|
102
|
+
pathname: url.pathname,
|
|
103
|
+
params: Object.create(null),
|
|
104
|
+
query: Object.fromEntries(url.searchParams),
|
|
105
|
+
body,
|
|
106
|
+
}, this.repo);
|
|
107
|
+
if (entry.fullUrl && result.length === 2) {
|
|
108
|
+
this.addReplacementId(entry.fullUrl, result[1]);
|
|
109
|
+
}
|
|
110
|
+
return buildBundleResponse(result[0], result[1]);
|
|
111
|
+
}
|
|
112
|
+
validateEntry(entry) {
|
|
113
|
+
if (!entry.request) {
|
|
114
|
+
throw new OperationOutcomeError(badRequest('Missing entry.request'));
|
|
115
|
+
}
|
|
116
|
+
if (!entry.request.method) {
|
|
117
|
+
throw new OperationOutcomeError(badRequest('Missing entry.request.method'));
|
|
118
|
+
}
|
|
119
|
+
if (!entry.request.url) {
|
|
120
|
+
throw new OperationOutcomeError(badRequest('Missing entry.request.url'));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
parsePatchBody(entry) {
|
|
124
|
+
const patchResource = entry.resource;
|
|
125
|
+
if (!patchResource) {
|
|
126
|
+
throw new OperationOutcomeError(badRequest('Missing entry.resource'));
|
|
127
|
+
}
|
|
128
|
+
if (patchResource.resourceType !== 'Binary') {
|
|
129
|
+
throw new OperationOutcomeError(badRequest('Patch resource must be a Binary'));
|
|
117
130
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
if (!patchResource.data) {
|
|
132
|
+
throw new OperationOutcomeError(badRequest('Missing entry.resource.data'));
|
|
133
|
+
}
|
|
134
|
+
return JSON.parse(Buffer.from(patchResource.data, 'base64').toString('utf8'));
|
|
135
|
+
}
|
|
136
|
+
addReplacementId(fullUrl, resource) {
|
|
137
|
+
if (fullUrl?.startsWith('urn:uuid:')) {
|
|
138
|
+
this.ids[fullUrl] = resource;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
rewriteIds(input) {
|
|
142
|
+
if (Array.isArray(input)) {
|
|
143
|
+
return this.rewriteIdsInArray(input);
|
|
144
|
+
}
|
|
145
|
+
if (typeof input === 'string') {
|
|
146
|
+
return this.rewriteIdsInString(input);
|
|
147
|
+
}
|
|
148
|
+
if (typeof input === 'object') {
|
|
149
|
+
return this.rewriteIdsInObject(input);
|
|
150
|
+
}
|
|
151
|
+
return input;
|
|
152
|
+
}
|
|
153
|
+
rewriteIdsInArray(input) {
|
|
154
|
+
return input.map((item) => this.rewriteIds(item));
|
|
155
|
+
}
|
|
156
|
+
rewriteIdsInObject(input) {
|
|
157
|
+
return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, this.rewriteIds(v)]));
|
|
158
|
+
}
|
|
159
|
+
rewriteIdsInString(input) {
|
|
160
|
+
const matches = input.match(/urn:uuid:\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/);
|
|
161
|
+
if (matches) {
|
|
162
|
+
const fullUrl = matches[0];
|
|
163
|
+
const resource = this.ids[fullUrl];
|
|
164
|
+
if (resource) {
|
|
165
|
+
return input.replaceAll(fullUrl, getReferenceString(resource));
|
|
122
166
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (request.method === 'PATCH') {
|
|
128
|
-
body = __classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_parsePatchBody).call(this, entry);
|
|
129
|
-
}
|
|
130
|
-
// Pass in dummy host for parsing purposes.
|
|
131
|
-
// The host is ignored.
|
|
132
|
-
const url = new URL(request.url, 'https://example.com/');
|
|
133
|
-
const result = await this.router.handleRequest({
|
|
134
|
-
method: request.method,
|
|
135
|
-
pathname: url.pathname,
|
|
136
|
-
params: Object.create(null),
|
|
137
|
-
query: Object.fromEntries(url.searchParams),
|
|
138
|
-
body,
|
|
139
|
-
}, this.repo);
|
|
140
|
-
if (entry.fullUrl && result.length === 2) {
|
|
141
|
-
__classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_addReplacementId).call(this, entry.fullUrl, result[1]);
|
|
142
|
-
}
|
|
143
|
-
return buildBundleResponse(result[0], result[1]);
|
|
144
|
-
}, _BatchProcessor_validateEntry = function _BatchProcessor_validateEntry(entry) {
|
|
145
|
-
if (!entry.request) {
|
|
146
|
-
throw new OperationOutcomeError(badRequest('Missing entry.request'));
|
|
147
|
-
}
|
|
148
|
-
if (!entry.request.method) {
|
|
149
|
-
throw new OperationOutcomeError(badRequest('Missing entry.request.method'));
|
|
150
|
-
}
|
|
151
|
-
if (!entry.request.url) {
|
|
152
|
-
throw new OperationOutcomeError(badRequest('Missing entry.request.url'));
|
|
153
|
-
}
|
|
154
|
-
}, _BatchProcessor_parsePatchBody = function _BatchProcessor_parsePatchBody(entry) {
|
|
155
|
-
const patchResource = entry.resource;
|
|
156
|
-
if (!patchResource) {
|
|
157
|
-
throw new OperationOutcomeError(badRequest('Missing entry.resource'));
|
|
158
|
-
}
|
|
159
|
-
if (patchResource.resourceType !== 'Binary') {
|
|
160
|
-
throw new OperationOutcomeError(badRequest('Patch resource must be a Binary'));
|
|
161
|
-
}
|
|
162
|
-
if (!patchResource.data) {
|
|
163
|
-
throw new OperationOutcomeError(badRequest('Missing entry.resource.data'));
|
|
164
|
-
}
|
|
165
|
-
return JSON.parse(Buffer.from(patchResource.data, 'base64').toString('utf8'));
|
|
166
|
-
}, _BatchProcessor_addReplacementId = function _BatchProcessor_addReplacementId(fullUrl, resource) {
|
|
167
|
-
if (fullUrl?.startsWith('urn:uuid:')) {
|
|
168
|
-
__classPrivateFieldGet(this, _BatchProcessor_ids, "f")[fullUrl] = resource;
|
|
169
|
-
}
|
|
170
|
-
}, _BatchProcessor_rewriteIds = function _BatchProcessor_rewriteIds(input) {
|
|
171
|
-
if (Array.isArray(input)) {
|
|
172
|
-
return __classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_rewriteIdsInArray).call(this, input);
|
|
173
|
-
}
|
|
174
|
-
if (typeof input === 'string') {
|
|
175
|
-
return __classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_rewriteIdsInString).call(this, input);
|
|
176
|
-
}
|
|
177
|
-
if (typeof input === 'object') {
|
|
178
|
-
return __classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_rewriteIdsInObject).call(this, input);
|
|
179
|
-
}
|
|
180
|
-
return input;
|
|
181
|
-
}, _BatchProcessor_rewriteIdsInArray = function _BatchProcessor_rewriteIdsInArray(input) {
|
|
182
|
-
return input.map((item) => __classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_rewriteIds).call(this, item));
|
|
183
|
-
}, _BatchProcessor_rewriteIdsInObject = function _BatchProcessor_rewriteIdsInObject(input) {
|
|
184
|
-
return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, __classPrivateFieldGet(this, _BatchProcessor_instances, "m", _BatchProcessor_rewriteIds).call(this, v)]));
|
|
185
|
-
}, _BatchProcessor_rewriteIdsInString = function _BatchProcessor_rewriteIdsInString(input) {
|
|
186
|
-
const matches = input.match(/urn:uuid:\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/);
|
|
187
|
-
if (matches) {
|
|
188
|
-
const fullUrl = matches[0];
|
|
189
|
-
const resource = __classPrivateFieldGet(this, _BatchProcessor_ids, "f")[fullUrl];
|
|
190
|
-
if (resource) {
|
|
191
|
-
return input.replaceAll(fullUrl, getReferenceString(resource));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return input;
|
|
195
|
-
};
|
|
167
|
+
}
|
|
168
|
+
return input;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
196
171
|
function buildBundleResponse(outcome, resource) {
|
|
197
172
|
return {
|
|
198
173
|
response: {
|
|
@@ -13969,7 +13944,6 @@ class FhirRouter {
|
|
|
13969
13944
|
}
|
|
13970
13945
|
}
|
|
13971
13946
|
|
|
13972
|
-
var _MemoryRepository_resources, _MemoryRepository_history;
|
|
13973
13947
|
class BaseRepository {
|
|
13974
13948
|
/**
|
|
13975
13949
|
* Searches for a single FHIR resource.
|
|
@@ -14007,10 +13981,8 @@ class BaseRepository {
|
|
|
14007
13981
|
class MemoryRepository extends BaseRepository {
|
|
14008
13982
|
constructor() {
|
|
14009
13983
|
super();
|
|
14010
|
-
|
|
14011
|
-
|
|
14012
|
-
__classPrivateFieldSet(this, _MemoryRepository_resources, {}, "f");
|
|
14013
|
-
__classPrivateFieldSet(this, _MemoryRepository_history, {}, "f");
|
|
13984
|
+
this.resources = new Map();
|
|
13985
|
+
this.history = new Map();
|
|
14014
13986
|
}
|
|
14015
13987
|
async createResource(resource) {
|
|
14016
13988
|
const result = deepClone(resource);
|
|
@@ -14027,17 +13999,23 @@ class MemoryRepository extends BaseRepository {
|
|
|
14027
13999
|
result.meta.lastUpdated = new Date().toISOString();
|
|
14028
14000
|
}
|
|
14029
14001
|
const { resourceType, id } = result;
|
|
14030
|
-
|
|
14031
|
-
|
|
14002
|
+
let resources = this.resources.get(resourceType);
|
|
14003
|
+
if (!resources) {
|
|
14004
|
+
resources = new Map();
|
|
14005
|
+
this.resources.set(resourceType, resources);
|
|
14032
14006
|
}
|
|
14033
|
-
|
|
14034
|
-
|
|
14007
|
+
resources.set(id, result);
|
|
14008
|
+
let resourceTypeHistory = this.history.get(resourceType);
|
|
14009
|
+
if (!resourceTypeHistory) {
|
|
14010
|
+
resourceTypeHistory = new Map();
|
|
14011
|
+
this.history.set(resourceType, resourceTypeHistory);
|
|
14035
14012
|
}
|
|
14036
|
-
|
|
14037
|
-
|
|
14013
|
+
let resourceHistory = resourceTypeHistory.get(id);
|
|
14014
|
+
if (!resourceHistory) {
|
|
14015
|
+
resourceHistory = [];
|
|
14016
|
+
resourceTypeHistory.set(id, resourceHistory);
|
|
14038
14017
|
}
|
|
14039
|
-
|
|
14040
|
-
__classPrivateFieldGet(this, _MemoryRepository_history, "f")[resourceType][id].push(result);
|
|
14018
|
+
resourceHistory.push(result);
|
|
14041
14019
|
return deepClone(result);
|
|
14042
14020
|
}
|
|
14043
14021
|
updateResource(resource) {
|
|
@@ -14066,7 +14044,7 @@ class MemoryRepository extends BaseRepository {
|
|
|
14066
14044
|
return this.updateResource(resource);
|
|
14067
14045
|
}
|
|
14068
14046
|
async readResource(resourceType, id) {
|
|
14069
|
-
const resource =
|
|
14047
|
+
const resource = this.resources.get(resourceType)?.get(id);
|
|
14070
14048
|
if (!resource) {
|
|
14071
14049
|
throw new OperationOutcomeError(notFound);
|
|
14072
14050
|
}
|
|
@@ -14087,14 +14065,17 @@ class MemoryRepository extends BaseRepository {
|
|
|
14087
14065
|
return {
|
|
14088
14066
|
resourceType: 'Bundle',
|
|
14089
14067
|
type: 'history',
|
|
14090
|
-
entry: (
|
|
14068
|
+
entry: (this.history.get(resourceType)?.get(id) || [])
|
|
14091
14069
|
.reverse()
|
|
14092
14070
|
.map((version) => ({ resource: deepClone(version) })),
|
|
14093
14071
|
};
|
|
14094
14072
|
}
|
|
14095
14073
|
async readVersion(resourceType, id, versionId) {
|
|
14096
14074
|
await this.readResource(resourceType, id);
|
|
14097
|
-
const version =
|
|
14075
|
+
const version = this.history
|
|
14076
|
+
.get(resourceType)
|
|
14077
|
+
?.get(id)
|
|
14078
|
+
?.find((v) => v.meta?.versionId === versionId);
|
|
14098
14079
|
if (!version) {
|
|
14099
14080
|
throw new OperationOutcomeError(notFound);
|
|
14100
14081
|
}
|
|
@@ -14102,8 +14083,13 @@ class MemoryRepository extends BaseRepository {
|
|
|
14102
14083
|
}
|
|
14103
14084
|
async search(searchRequest) {
|
|
14104
14085
|
const { resourceType } = searchRequest;
|
|
14105
|
-
const resources =
|
|
14106
|
-
const result =
|
|
14086
|
+
const resources = this.resources.get(resourceType) || new Map();
|
|
14087
|
+
const result = [];
|
|
14088
|
+
for (const resource of resources.values()) {
|
|
14089
|
+
if (matchesSearchRequest(resource, searchRequest)) {
|
|
14090
|
+
result.push(resource);
|
|
14091
|
+
}
|
|
14092
|
+
}
|
|
14107
14093
|
let entry = result.map((resource) => ({ resource: deepClone(resource) }));
|
|
14108
14094
|
if (searchRequest.sortRules) {
|
|
14109
14095
|
for (const sortRule of searchRequest.sortRules) {
|
|
@@ -14124,13 +14110,12 @@ class MemoryRepository extends BaseRepository {
|
|
|
14124
14110
|
};
|
|
14125
14111
|
}
|
|
14126
14112
|
async deleteResource(resourceType, id) {
|
|
14127
|
-
if (!
|
|
14113
|
+
if (!this.resources.get(resourceType)?.get(id)) {
|
|
14128
14114
|
throw new OperationOutcomeError(notFound);
|
|
14129
14115
|
}
|
|
14130
|
-
delete
|
|
14116
|
+
this.resources.get(resourceType)?.delete(id);
|
|
14131
14117
|
}
|
|
14132
14118
|
}
|
|
14133
|
-
_MemoryRepository_resources = new WeakMap(), _MemoryRepository_history = new WeakMap();
|
|
14134
14119
|
const sortComparator = (a, b, sortRule) => {
|
|
14135
14120
|
const searchParam = globalSchema.types[a.resourceType]?.searchParams?.[sortRule.code];
|
|
14136
14121
|
const expression = searchParam?.expression;
|