@medplum/fhir-router 2.0.16 → 2.0.18
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 +201 -11
- 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 +202 -12
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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,
|
|
1
|
+
import { OperationOutcomeError, badRequest, normalizeOperationOutcome, parseSearchUrl, getReferenceString, resolveId, getStatus, isOk, allOk, LRUCache, forbidden, getResourceTypes, getResourceTypeSchema, isResourceTypeSchema, getElementDefinition, buildTypeName, capitalize, isLowerCase, globalSchema, getSearchParameters, DEFAULT_SEARCH_COUNT, toJsBoolean, evalFhirPathTyped, toTypedValue, Operator, parseSearchRequest, notFound, created, deepClone, matchesSearchRequest, evalFhirPath } from '@medplum/core';
|
|
2
2
|
import DataLoader from 'dataloader';
|
|
3
3
|
import { applyPatch } from 'rfc6902';
|
|
4
4
|
|
|
@@ -53,6 +53,11 @@ class BatchProcessor {
|
|
|
53
53
|
const resultEntries = [];
|
|
54
54
|
for (const entry of entries) {
|
|
55
55
|
const rewritten = this.rewriteIdsInObject(entry);
|
|
56
|
+
// If the resource 'id' element is specified, we want to replace teh `urn:uuid:*` string and
|
|
57
|
+
// remove the `resourceType` prefix
|
|
58
|
+
if (entry?.resource?.id) {
|
|
59
|
+
rewritten.resource.id = this.rewriteIdsInString(entry.resource.id, true);
|
|
60
|
+
}
|
|
56
61
|
try {
|
|
57
62
|
resultEntries.push(await this.processBatchEntry(rewritten));
|
|
58
63
|
}
|
|
@@ -156,13 +161,17 @@ class BatchProcessor {
|
|
|
156
161
|
rewriteIdsInObject(input) {
|
|
157
162
|
return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, this.rewriteIds(v)]));
|
|
158
163
|
}
|
|
159
|
-
rewriteIdsInString(input) {
|
|
164
|
+
rewriteIdsInString(input, removeResourceType = false) {
|
|
160
165
|
const matches = input.match(/urn:uuid:\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/);
|
|
161
166
|
if (matches) {
|
|
162
167
|
const fullUrl = matches[0];
|
|
163
168
|
const resource = this.ids[fullUrl];
|
|
164
169
|
if (resource) {
|
|
165
|
-
|
|
170
|
+
let referenceString = getReferenceString(resource);
|
|
171
|
+
if (removeResourceType) {
|
|
172
|
+
referenceString = resolveId({ reference: referenceString });
|
|
173
|
+
}
|
|
174
|
+
return referenceString ? input.replaceAll(fullUrl, referenceString) : input;
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
return input;
|
|
@@ -13464,6 +13473,12 @@ function buildRootSchema() {
|
|
|
13464
13473
|
args: buildSearchArgs(resourceType),
|
|
13465
13474
|
resolve: resolveBySearch,
|
|
13466
13475
|
};
|
|
13476
|
+
// FHIR GraphQL Connection API
|
|
13477
|
+
fields[resourceType + 'Connection'] = {
|
|
13478
|
+
type: buildConnectionType(resourceType, graphQLType),
|
|
13479
|
+
args: buildSearchArgs(resourceType),
|
|
13480
|
+
resolve: resolveByConnectionApi,
|
|
13481
|
+
};
|
|
13467
13482
|
}
|
|
13468
13483
|
return new GraphQLSchema({
|
|
13469
13484
|
query: new GraphQLObjectType({
|
|
@@ -13522,17 +13537,95 @@ function buildPropertyFields(resourceType, fields) {
|
|
|
13522
13537
|
for (const key of Object.keys(properties)) {
|
|
13523
13538
|
const elementDefinition = getElementDefinition(resourceType, key);
|
|
13524
13539
|
for (const type of elementDefinition.type) {
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13540
|
+
buildPropertyField(fields, key, elementDefinition, type);
|
|
13541
|
+
}
|
|
13542
|
+
}
|
|
13543
|
+
}
|
|
13544
|
+
function buildPropertyField(fields, key, elementDefinition, elementDefinitionType) {
|
|
13545
|
+
let typeName = elementDefinitionType.code;
|
|
13546
|
+
if (typeName === 'Element' || typeName === 'BackboneElement') {
|
|
13547
|
+
typeName = buildTypeName(elementDefinition.path?.split('.'));
|
|
13548
|
+
}
|
|
13549
|
+
const fieldConfig = {
|
|
13550
|
+
description: elementDefinition.short,
|
|
13551
|
+
type: getPropertyType(elementDefinition, typeName),
|
|
13552
|
+
resolve: resolveField,
|
|
13553
|
+
};
|
|
13554
|
+
if (elementDefinition.max === '*') {
|
|
13555
|
+
fieldConfig.args = buildListPropertyFieldArgs(typeName);
|
|
13556
|
+
}
|
|
13557
|
+
const propertyName = key.replace('[x]', capitalize(elementDefinitionType.code));
|
|
13558
|
+
fields[propertyName] = fieldConfig;
|
|
13559
|
+
}
|
|
13560
|
+
/**
|
|
13561
|
+
* Builds field arguments for a list property.
|
|
13562
|
+
*
|
|
13563
|
+
* The FHIR GraphQL specification defines the following arguments for list properties:
|
|
13564
|
+
* 1. _count: Specify how many elements to return from a repeating list.
|
|
13565
|
+
* 2. _offset: Specify the offset to start at for a repeating element.
|
|
13566
|
+
* 3. fhirpath: A FHIRPath statement selecting which of the subnodes is to be included.
|
|
13567
|
+
* 4. All properties of the list element type.
|
|
13568
|
+
*
|
|
13569
|
+
* See: https://hl7.org/fhir/R4/graphql.html#list
|
|
13570
|
+
*
|
|
13571
|
+
* @param fieldTypeName The type name of the field.
|
|
13572
|
+
* @returns The arguments for the field.
|
|
13573
|
+
*/
|
|
13574
|
+
function buildListPropertyFieldArgs(fieldTypeName) {
|
|
13575
|
+
const fieldArgs = {
|
|
13576
|
+
_count: {
|
|
13577
|
+
type: GraphQLInt,
|
|
13578
|
+
description: 'Specify how many elements to return from a repeating list.',
|
|
13579
|
+
},
|
|
13580
|
+
_offset: {
|
|
13581
|
+
type: GraphQLInt,
|
|
13582
|
+
description: 'Specify the offset to start at for a repeating element.',
|
|
13583
|
+
},
|
|
13584
|
+
};
|
|
13585
|
+
if (!isLowerCase(fieldTypeName.charAt(0))) {
|
|
13586
|
+
// If this is a backbone element, add "fhirpath" and all properties as arguments
|
|
13587
|
+
fieldArgs.fhirpath = {
|
|
13588
|
+
type: GraphQLString,
|
|
13589
|
+
description: 'A FHIRPath statement selecting which of the subnodes is to be included',
|
|
13590
|
+
};
|
|
13591
|
+
// Add all "string" and "code" properties as arguments
|
|
13592
|
+
const fieldTypeSchema = globalSchema.types[fieldTypeName];
|
|
13593
|
+
if (fieldTypeSchema.properties) {
|
|
13594
|
+
for (const fieldKey of Object.keys(fieldTypeSchema.properties)) {
|
|
13595
|
+
const fieldElementDefinition = getElementDefinition(fieldTypeName, fieldKey);
|
|
13596
|
+
for (const type of fieldElementDefinition.type) {
|
|
13597
|
+
buildListPropertyFieldArg(fieldArgs, fieldKey, fieldElementDefinition, type);
|
|
13598
|
+
}
|
|
13528
13599
|
}
|
|
13529
|
-
|
|
13600
|
+
}
|
|
13601
|
+
}
|
|
13602
|
+
return fieldArgs;
|
|
13603
|
+
}
|
|
13604
|
+
/**
|
|
13605
|
+
* Builds a field argument for a list property.
|
|
13606
|
+
* @param fieldArgs The output argument map.
|
|
13607
|
+
* @param fieldKey The key of the field.
|
|
13608
|
+
* @param elementDefinition The FHIR element definition of the field.
|
|
13609
|
+
* @param elementDefinitionType The FHIR element definition type of the field.
|
|
13610
|
+
*/
|
|
13611
|
+
function buildListPropertyFieldArg(fieldArgs, fieldKey, elementDefinition, elementDefinitionType) {
|
|
13612
|
+
const baseType = elementDefinitionType.code;
|
|
13613
|
+
const fieldName = fieldKey.replace('[x]', capitalize(baseType));
|
|
13614
|
+
switch (baseType) {
|
|
13615
|
+
case 'canonical':
|
|
13616
|
+
case 'code':
|
|
13617
|
+
case 'id':
|
|
13618
|
+
case 'oid':
|
|
13619
|
+
case 'string':
|
|
13620
|
+
case 'uri':
|
|
13621
|
+
case 'url':
|
|
13622
|
+
case 'uuid':
|
|
13623
|
+
case 'http://hl7.org/fhirpath/System.String':
|
|
13624
|
+
fieldArgs[fieldName] = {
|
|
13625
|
+
type: GraphQLString,
|
|
13530
13626
|
description: elementDefinition.short,
|
|
13531
|
-
type: getPropertyType(elementDefinition, typeName),
|
|
13532
13627
|
};
|
|
13533
|
-
|
|
13534
|
-
fields[propertyName] = fieldConfig;
|
|
13535
|
-
}
|
|
13628
|
+
break;
|
|
13536
13629
|
}
|
|
13537
13630
|
}
|
|
13538
13631
|
/**
|
|
@@ -13638,6 +13731,30 @@ function getPropertyType(elementDefinition, typeName) {
|
|
|
13638
13731
|
}
|
|
13639
13732
|
return graphqlType;
|
|
13640
13733
|
}
|
|
13734
|
+
function buildConnectionType(resourceType, resourceGraphQLType) {
|
|
13735
|
+
return new GraphQLObjectType({
|
|
13736
|
+
name: resourceType + 'Connection',
|
|
13737
|
+
fields: {
|
|
13738
|
+
count: { type: GraphQLInt },
|
|
13739
|
+
offset: { type: GraphQLInt },
|
|
13740
|
+
pageSize: { type: GraphQLInt },
|
|
13741
|
+
first: { type: GraphQLString },
|
|
13742
|
+
previous: { type: GraphQLString },
|
|
13743
|
+
next: { type: GraphQLString },
|
|
13744
|
+
last: { type: GraphQLString },
|
|
13745
|
+
edges: {
|
|
13746
|
+
type: new GraphQLList(new GraphQLObjectType({
|
|
13747
|
+
name: resourceType + 'ConnectionEdge',
|
|
13748
|
+
fields: {
|
|
13749
|
+
mode: { type: GraphQLString },
|
|
13750
|
+
score: { type: GraphQLFloat },
|
|
13751
|
+
resource: { type: resourceGraphQLType },
|
|
13752
|
+
},
|
|
13753
|
+
})),
|
|
13754
|
+
},
|
|
13755
|
+
},
|
|
13756
|
+
});
|
|
13757
|
+
}
|
|
13641
13758
|
/**
|
|
13642
13759
|
* GraphQL data loader for search requests.
|
|
13643
13760
|
* The field name should always end with "List" (i.e., "Patient" search uses "PatientList").
|
|
@@ -13651,11 +13768,41 @@ function getPropertyType(elementDefinition, typeName) {
|
|
|
13651
13768
|
*/
|
|
13652
13769
|
async function resolveBySearch(source, args, ctx, info) {
|
|
13653
13770
|
const fieldName = info.fieldName;
|
|
13654
|
-
const resourceType = fieldName.substring(0, fieldName.length -
|
|
13771
|
+
const resourceType = fieldName.substring(0, fieldName.length - 'List'.length);
|
|
13655
13772
|
const searchRequest = parseSearchArgs(resourceType, source, args);
|
|
13656
13773
|
const bundle = await ctx.repo.search(searchRequest);
|
|
13657
13774
|
return bundle.entry?.map((e) => e.resource);
|
|
13658
13775
|
}
|
|
13776
|
+
/**
|
|
13777
|
+
* GraphQL data loader for search requests.
|
|
13778
|
+
* The field name should always end with "List" (i.e., "Patient" search uses "PatientList").
|
|
13779
|
+
* The search args should be FHIR search parameters.
|
|
13780
|
+
* @param source The source/root. This should always be null for our top level readers.
|
|
13781
|
+
* @param args The GraphQL search arguments.
|
|
13782
|
+
* @param ctx The GraphQL context.
|
|
13783
|
+
* @param info The GraphQL resolve info. This includes the schema, and additional field details.
|
|
13784
|
+
* @returns Promise to read the resoures for the query.
|
|
13785
|
+
* @implements {GraphQLFieldResolver}
|
|
13786
|
+
*/
|
|
13787
|
+
async function resolveByConnectionApi(source, args, ctx, info) {
|
|
13788
|
+
const fieldName = info.fieldName;
|
|
13789
|
+
const resourceType = fieldName.substring(0, fieldName.length - 'Connection'.length);
|
|
13790
|
+
const searchRequest = parseSearchArgs(resourceType, source, args);
|
|
13791
|
+
if (isFieldRequested(info, 'count')) {
|
|
13792
|
+
searchRequest.total = 'accurate';
|
|
13793
|
+
}
|
|
13794
|
+
const bundle = await ctx.repo.search(searchRequest);
|
|
13795
|
+
return {
|
|
13796
|
+
count: bundle.total,
|
|
13797
|
+
offset: searchRequest.offset || 0,
|
|
13798
|
+
pageSize: searchRequest.count || DEFAULT_SEARCH_COUNT,
|
|
13799
|
+
edges: bundle.entry?.map((e) => ({
|
|
13800
|
+
mode: e.search?.mode,
|
|
13801
|
+
score: e.search?.score,
|
|
13802
|
+
resource: e.resource,
|
|
13803
|
+
})),
|
|
13804
|
+
};
|
|
13805
|
+
}
|
|
13659
13806
|
/**
|
|
13660
13807
|
* GraphQL data loader for ID requests.
|
|
13661
13808
|
* The field name should always by the resource type.
|
|
@@ -13706,6 +13853,38 @@ function resolveTypeByReference(resource) {
|
|
|
13706
13853
|
}
|
|
13707
13854
|
return getGraphQLType(resourceType).name;
|
|
13708
13855
|
}
|
|
13856
|
+
/**
|
|
13857
|
+
* GraphQL resolver for fields.
|
|
13858
|
+
* In the common case, this is just a matter of returning the field value from the source object.
|
|
13859
|
+
* If the field is a list and the user specifies list arguments, then we can apply those arguments here.
|
|
13860
|
+
* @param source The source. This is the object that contains the field.
|
|
13861
|
+
* @param args The GraphQL search arguments.
|
|
13862
|
+
* @param _ctx The GraphQL context.
|
|
13863
|
+
* @param info The GraphQL resolve info. This includes the field name.
|
|
13864
|
+
* @returns Promise to read the resoure for the query.
|
|
13865
|
+
* @implements {GraphQLFieldResolver}
|
|
13866
|
+
*/
|
|
13867
|
+
async function resolveField(source, args, _ctx, info) {
|
|
13868
|
+
const fieldValue = source?.[info.fieldName];
|
|
13869
|
+
if (!args || !fieldValue) {
|
|
13870
|
+
return fieldValue;
|
|
13871
|
+
}
|
|
13872
|
+
const { _offset, _count, fhirpath, ...rest } = args;
|
|
13873
|
+
let array = fieldValue;
|
|
13874
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
13875
|
+
array = array.filter((item) => item[key] === value);
|
|
13876
|
+
}
|
|
13877
|
+
if (fhirpath) {
|
|
13878
|
+
array = array.filter((item) => toJsBoolean(evalFhirPathTyped(fhirpath, [toTypedValue(item)])));
|
|
13879
|
+
}
|
|
13880
|
+
if (_offset) {
|
|
13881
|
+
array = array.slice(_offset);
|
|
13882
|
+
}
|
|
13883
|
+
if (_count) {
|
|
13884
|
+
array = array.slice(0, _count);
|
|
13885
|
+
}
|
|
13886
|
+
return array;
|
|
13887
|
+
}
|
|
13709
13888
|
function parseSearchArgs(resourceType, source, args) {
|
|
13710
13889
|
let referenceFilter = undefined;
|
|
13711
13890
|
if (source) {
|
|
@@ -13772,6 +13951,17 @@ const MaxDepthRule = (context) => ({
|
|
|
13772
13951
|
function getDepth(path) {
|
|
13773
13952
|
return path.filter((p) => p === 'selections').length;
|
|
13774
13953
|
}
|
|
13954
|
+
/**
|
|
13955
|
+
* Returns true if the field is requested in the GraphQL query.
|
|
13956
|
+
* @param info The GraphQL resolve info. This includes the field name.
|
|
13957
|
+
* @param fieldName The field name to check.
|
|
13958
|
+
* @returns True if the field is requested in the GraphQL query.
|
|
13959
|
+
*/
|
|
13960
|
+
function isFieldRequested(info, fieldName) {
|
|
13961
|
+
return info.fieldNodes.some((fieldNode) => fieldNode.selectionSet?.selections.some((selection) => {
|
|
13962
|
+
return selection.kind === 'Field' && selection.name.value === fieldName;
|
|
13963
|
+
}));
|
|
13964
|
+
}
|
|
13775
13965
|
/**
|
|
13776
13966
|
* Returns an OperationOutcome for GraphQL errors.
|
|
13777
13967
|
* @param errors Array of GraphQL errors.
|