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