@medplum/fhir-router 2.0.20 → 2.0.22

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.
@@ -8,7 +8,6 @@
8
8
  * Processes a FHIR batch request.
9
9
  *
10
10
  * See: https://www.hl7.org/fhir/http.html#transaction
11
- *
12
11
  * @param router The FHIR router.
13
12
  * @param repo The FHIR repository.
14
13
  * @param bundle The input bundle.
@@ -36,8 +35,6 @@
36
35
  }
37
36
  /**
38
37
  * Processes a FHIR batch request.
39
- * @param repo The FHIR repository.
40
- * @param bundle The input bundle.
41
38
  * @returns The bundle response.
42
39
  */
43
40
  async processBatch() {
@@ -13378,6 +13375,12 @@ spurious results.`);
13378
13375
  'http://hl7.org/fhirpath/System.String': GraphQLString,
13379
13376
  'http://hl7.org/fhirpath/System.Time': GraphQLString,
13380
13377
  };
13378
+ const outputTypeCache = {
13379
+ ...typeCache,
13380
+ };
13381
+ const inputTypeCache = {
13382
+ ...typeCache,
13383
+ };
13381
13384
  /**
13382
13385
  * Cache of "introspection" query results.
13383
13386
  * Common case is the standard schema query from GraphiQL and Insomnia.
@@ -13393,8 +13396,12 @@ spurious results.`);
13393
13396
  * Handles FHIR GraphQL requests.
13394
13397
  *
13395
13398
  * See: https://www.hl7.org/fhir/graphql.html
13399
+ * @param req The request details.
13400
+ * @param repo The current user FHIR repository.
13401
+ * @param router The router for router options.
13402
+ * @returns The response.
13396
13403
  */
13397
- async function graphqlHandler(req, repo) {
13404
+ async function graphqlHandler(req, repo, router) {
13398
13405
  const { query, operationName, variables } = req.body;
13399
13406
  if (!query) {
13400
13407
  return [core.badRequest('Must provide query.')];
@@ -13413,7 +13420,7 @@ spurious results.`);
13413
13420
  return [invalidRequest(validationErrors)];
13414
13421
  }
13415
13422
  const introspection = isIntrospectionQuery(query);
13416
- if (introspection) {
13423
+ if (introspection && !router.options?.introspectionEnabled) {
13417
13424
  return [core.forbidden];
13418
13425
  }
13419
13426
  const dataLoader = new DataLoader((keys) => repo.readReferences(keys));
@@ -13435,7 +13442,6 @@ spurious results.`);
13435
13442
  * Introspection queries ask for the schema, which is expensive.
13436
13443
  *
13437
13444
  * See: https://graphql.org/learn/introspection/
13438
- *
13439
13445
  * @param query The GraphQL query.
13440
13446
  * @returns True if the query is an introspection query.
13441
13447
  */
@@ -13452,15 +13458,16 @@ spurious results.`);
13452
13458
  // First, create placeholder types
13453
13459
  // We need this first for circular dependencies
13454
13460
  for (const resourceType of core.getResourceTypes()) {
13455
- typeCache[resourceType] = buildGraphQLType(resourceType);
13461
+ outputTypeCache[resourceType] = buildGraphQLOutputType(resourceType);
13456
13462
  }
13457
13463
  // Next, fill in all of the type properties
13458
13464
  const fields = {};
13465
+ const mutationFields = {};
13459
13466
  for (const resourceType of core.getResourceTypes()) {
13460
- const graphQLType = getGraphQLType(resourceType);
13467
+ const graphQLOutputType = getGraphQLOutputType(resourceType);
13461
13468
  // Get resource by ID
13462
13469
  fields[resourceType] = {
13463
- type: graphQLType,
13470
+ type: graphQLOutputType,
13464
13471
  args: {
13465
13472
  id: {
13466
13473
  type: new GraphQLNonNull(GraphQLID),
@@ -13471,38 +13478,71 @@ spurious results.`);
13471
13478
  };
13472
13479
  // Search resource by search parameters
13473
13480
  fields[resourceType + 'List'] = {
13474
- type: new GraphQLList(graphQLType),
13481
+ type: new GraphQLList(graphQLOutputType),
13475
13482
  args: buildSearchArgs(resourceType),
13476
13483
  resolve: resolveBySearch,
13477
13484
  };
13478
13485
  // FHIR GraphQL Connection API
13479
13486
  fields[resourceType + 'Connection'] = {
13480
- type: buildConnectionType(resourceType, graphQLType),
13487
+ type: buildConnectionType(resourceType, graphQLOutputType),
13481
13488
  args: buildSearchArgs(resourceType),
13482
13489
  resolve: resolveByConnectionApi,
13483
13490
  };
13491
+ // Mutation API
13492
+ mutationFields[resourceType + 'Create'] = {
13493
+ type: graphQLOutputType,
13494
+ args: buildCreateArgs(resourceType),
13495
+ resolve: resolveByCreate,
13496
+ };
13497
+ mutationFields[resourceType + 'Update'] = {
13498
+ type: graphQLOutputType,
13499
+ args: buildUpdateArgs(resourceType),
13500
+ resolve: resolveByUpdate,
13501
+ };
13502
+ mutationFields[resourceType + 'Delete'] = {
13503
+ type: graphQLOutputType,
13504
+ args: {
13505
+ id: {
13506
+ type: new GraphQLNonNull(GraphQLID),
13507
+ description: resourceType + ' ID',
13508
+ },
13509
+ },
13510
+ resolve: resolveByDelete,
13511
+ };
13484
13512
  }
13485
13513
  return new GraphQLSchema({
13486
13514
  query: new GraphQLObjectType({
13487
13515
  name: 'QueryType',
13488
13516
  fields,
13489
13517
  }),
13518
+ mutation: new GraphQLObjectType({
13519
+ name: 'MutationType',
13520
+ fields: mutationFields,
13521
+ }),
13490
13522
  });
13491
13523
  }
13492
- function getGraphQLType(resourceType) {
13493
- let result = typeCache[resourceType];
13524
+ function getGraphQLOutputType(inputType) {
13525
+ let result = outputTypeCache[inputType];
13494
13526
  if (!result) {
13495
- result = buildGraphQLType(resourceType);
13496
- typeCache[resourceType] = result;
13527
+ result = buildGraphQLOutputType(inputType);
13528
+ outputTypeCache[inputType] = result;
13497
13529
  }
13498
13530
  return result;
13499
13531
  }
13500
- function buildGraphQLType(resourceType) {
13532
+ function getGraphQLInputType(inputType, nameSuffix) {
13533
+ let result = inputTypeCache[inputType];
13534
+ if (!result) {
13535
+ result = buildGraphQLInputType(inputType, nameSuffix);
13536
+ inputTypeCache[inputType] = result;
13537
+ }
13538
+ return result;
13539
+ }
13540
+ function buildGraphQLOutputType(resourceType) {
13501
13541
  if (resourceType === 'ResourceList') {
13502
13542
  return new GraphQLUnionType({
13503
13543
  name: 'ResourceList',
13504
13544
  types: () => core.getResourceTypes()
13505
- .map(getGraphQLType)
13545
+ .map(getGraphQLOutputType)
13506
13546
  .filter((t) => !!t),
13507
13547
  resolveType: resolveTypeByReference,
13508
13548
  });
@@ -13511,16 +13551,37 @@ spurious results.`);
13511
13551
  return new GraphQLObjectType({
13512
13552
  name: resourceType,
13513
13553
  description: schema.description,
13514
- fields: () => buildGraphQLFields(resourceType),
13554
+ fields: () => buildGraphQLOutputFields(resourceType),
13515
13555
  });
13516
13556
  }
13517
- function buildGraphQLFields(resourceType) {
13557
+ function buildGraphQLInputType(resourceType, nameSuffix) {
13558
+ const schema = core.getResourceTypeSchema(resourceType);
13559
+ return new GraphQLInputObjectType({
13560
+ name: resourceType + nameSuffix,
13561
+ description: schema.description,
13562
+ fields: () => buildGraphQLInputFields(resourceType, nameSuffix),
13563
+ });
13564
+ }
13565
+ function buildGraphQLOutputFields(resourceType) {
13518
13566
  const fields = {};
13519
- buildPropertyFields(resourceType, fields);
13567
+ buildOutputPropertyFields(resourceType, fields);
13520
13568
  buildReverseLookupFields(resourceType, fields);
13521
13569
  return fields;
13522
13570
  }
13523
- function buildPropertyFields(resourceType, fields) {
13571
+ function buildGraphQLInputFields(resourceType, nameSuffix) {
13572
+ const fields = {};
13573
+ // Add resourceType field for root resource
13574
+ if (core.isResourceType(resourceType)) {
13575
+ const propertyFieldConfig = {
13576
+ description: 'The type of resource',
13577
+ type: GraphQLString,
13578
+ };
13579
+ fields['resourceType'] = propertyFieldConfig;
13580
+ }
13581
+ buildInputPropertyFields(resourceType, fields, nameSuffix);
13582
+ return fields;
13583
+ }
13584
+ function buildOutputPropertyFields(resourceType, fields) {
13524
13585
  const schema = core.getResourceTypeSchema(resourceType);
13525
13586
  const properties = schema.properties;
13526
13587
  if (core.isResourceTypeSchema(schema)) {
@@ -13532,25 +13593,35 @@ spurious results.`);
13532
13593
  if (resourceType === 'Reference') {
13533
13594
  fields.resource = {
13534
13595
  description: 'Reference',
13535
- type: getGraphQLType('ResourceList'),
13596
+ type: getGraphQLOutputType('ResourceList'),
13536
13597
  resolve: resolveByReference,
13537
13598
  };
13538
13599
  }
13539
13600
  for (const key of Object.keys(properties)) {
13540
13601
  const elementDefinition = core.getElementDefinition(resourceType, key);
13541
13602
  for (const type of elementDefinition.type) {
13542
- buildPropertyField(fields, key, elementDefinition, type);
13603
+ buildOutputPropertyField(fields, key, elementDefinition, type);
13604
+ }
13605
+ }
13606
+ }
13607
+ function buildInputPropertyFields(resourceType, fields, nameSuffix) {
13608
+ const schema = core.getResourceTypeSchema(resourceType);
13609
+ const properties = schema.properties;
13610
+ for (const key of Object.keys(properties)) {
13611
+ const elementDefinition = core.getElementDefinition(resourceType, key);
13612
+ for (const type of elementDefinition.type) {
13613
+ buildInputPropertyField(fields, key, elementDefinition, type, nameSuffix);
13543
13614
  }
13544
13615
  }
13545
13616
  }
13546
- function buildPropertyField(fields, key, elementDefinition, elementDefinitionType) {
13617
+ function buildOutputPropertyField(fields, key, elementDefinition, elementDefinitionType) {
13547
13618
  let typeName = elementDefinitionType.code;
13548
13619
  if (typeName === 'Element' || typeName === 'BackboneElement') {
13549
13620
  typeName = core.buildTypeName(elementDefinition.path?.split('.'));
13550
13621
  }
13551
13622
  const fieldConfig = {
13552
13623
  description: elementDefinition.short,
13553
- type: getPropertyType(elementDefinition, typeName),
13624
+ type: getOutputPropertyType(elementDefinition, typeName),
13554
13625
  resolve: resolveField,
13555
13626
  };
13556
13627
  if (elementDefinition.max === '*') {
@@ -13559,6 +13630,21 @@ spurious results.`);
13559
13630
  const propertyName = key.replace('[x]', core.capitalize(elementDefinitionType.code));
13560
13631
  fields[propertyName] = fieldConfig;
13561
13632
  }
13633
+ function buildInputPropertyField(fields, key, elementDefinition, elementDefinitionType, nameSuffix) {
13634
+ let typeName = elementDefinitionType.code;
13635
+ if (typeName === 'Element' || typeName === 'BackboneElement') {
13636
+ typeName = core.buildTypeName(elementDefinition.path?.split('.'));
13637
+ }
13638
+ const fieldConfig = {
13639
+ description: elementDefinition.short,
13640
+ type: getGraphQLInputType(typeName, nameSuffix),
13641
+ };
13642
+ if (elementDefinition.max === '*') {
13643
+ fieldConfig.type = new GraphQLList(getGraphQLInputType(typeName, nameSuffix));
13644
+ }
13645
+ const propertyName = key.replace('[x]', core.capitalize(elementDefinitionType.code));
13646
+ fields[propertyName] = fieldConfig;
13647
+ }
13562
13648
  /**
13563
13649
  * Builds field arguments for a list property.
13564
13650
  *
@@ -13569,7 +13655,6 @@ spurious results.`);
13569
13655
  * 4. All properties of the list element type.
13570
13656
  *
13571
13657
  * See: https://hl7.org/fhir/R4/graphql.html#list
13572
- *
13573
13658
  * @param fieldTypeName The type name of the field.
13574
13659
  * @returns The arguments for the field.
13575
13660
  */
@@ -13654,13 +13739,12 @@ spurious results.`);
13654
13739
  * (except that the "id" argument is prohibited here as nonsensical).
13655
13740
  *
13656
13741
  * See: https://www.hl7.org/fhir/graphql.html#reverse
13657
- *
13658
13742
  * @param resourceType The resource type to build fields for.
13659
13743
  * @param fields The fields object to add fields to.
13660
13744
  */
13661
13745
  function buildReverseLookupFields(resourceType, fields) {
13662
13746
  for (const childResourceType of core.getResourceTypes()) {
13663
- const childGraphQLType = getGraphQLType(childResourceType);
13747
+ const childGraphQLType = getGraphQLOutputType(childResourceType);
13664
13748
  const childSearchParams = core.getSearchParameters(childResourceType);
13665
13749
  const enumValues = {};
13666
13750
  let count = 0;
@@ -13726,8 +13810,30 @@ spurious results.`);
13726
13810
  }
13727
13811
  return args;
13728
13812
  }
13729
- function getPropertyType(elementDefinition, typeName) {
13730
- const graphqlType = getGraphQLType(typeName);
13813
+ function buildCreateArgs(resourceType) {
13814
+ const args = {
13815
+ res: {
13816
+ type: getGraphQLInputType(resourceType, 'Create'),
13817
+ description: resourceType + ' Create',
13818
+ },
13819
+ };
13820
+ return args;
13821
+ }
13822
+ function buildUpdateArgs(resourceType) {
13823
+ const args = {
13824
+ id: {
13825
+ type: new GraphQLNonNull(GraphQLID),
13826
+ description: resourceType + ' ID',
13827
+ },
13828
+ res: {
13829
+ type: getGraphQLInputType(resourceType, 'Update'),
13830
+ description: resourceType + ' Update',
13831
+ },
13832
+ };
13833
+ return args;
13834
+ }
13835
+ function getOutputPropertyType(elementDefinition, typeName) {
13836
+ const graphqlType = getGraphQLOutputType(typeName);
13731
13837
  if (elementDefinition.max === '*') {
13732
13838
  return new GraphQLList(graphqlType);
13733
13839
  }
@@ -13766,7 +13872,6 @@ spurious results.`);
13766
13872
  * @param ctx The GraphQL context.
13767
13873
  * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13768
13874
  * @returns Promise to read the resoures for the query.
13769
- * @implements {GraphQLFieldResolver}
13770
13875
  */
13771
13876
  async function resolveBySearch(source, args, ctx, info) {
13772
13877
  const fieldName = info.fieldName;
@@ -13784,7 +13889,6 @@ spurious results.`);
13784
13889
  * @param ctx The GraphQL context.
13785
13890
  * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13786
13891
  * @returns Promise to read the resoures for the query.
13787
- * @implements {GraphQLFieldResolver}
13788
13892
  */
13789
13893
  async function resolveByConnectionApi(source, args, ctx, info) {
13790
13894
  const fieldName = info.fieldName;
@@ -13814,7 +13918,6 @@ spurious results.`);
13814
13918
  * @param ctx The GraphQL context.
13815
13919
  * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13816
13920
  * @returns Promise to read the resoure for the query.
13817
- * @implements {GraphQLFieldResolver}
13818
13921
  */
13819
13922
  async function resolveById(_source, args, ctx, info) {
13820
13923
  try {
@@ -13831,7 +13934,6 @@ spurious results.`);
13831
13934
  * @param _args The GraphQL search arguments.
13832
13935
  * @param ctx The GraphQL context.
13833
13936
  * @returns Promise to read the resoure(s) for the query.
13834
- * @implements {GraphQLFieldResolver}
13835
13937
  */
13836
13938
  async function resolveByReference(source, _args, ctx) {
13837
13939
  try {
@@ -13846,14 +13948,13 @@ spurious results.`);
13846
13948
  * When loading a resource via reference, GraphQL needs to know the type of the resource.
13847
13949
  * @param resource The loaded resource.
13848
13950
  * @returns The GraphQL type of the resource.
13849
- * @implements {GraphQLTypeResolver}
13850
13951
  */
13851
13952
  function resolveTypeByReference(resource) {
13852
13953
  const resourceType = resource?.resourceType;
13853
13954
  if (!resourceType) {
13854
13955
  return undefined;
13855
13956
  }
13856
- return getGraphQLType(resourceType).name;
13957
+ return getGraphQLOutputType(resourceType).name;
13857
13958
  }
13858
13959
  /**
13859
13960
  * GraphQL resolver for fields.
@@ -13864,7 +13965,6 @@ spurious results.`);
13864
13965
  * @param _ctx The GraphQL context.
13865
13966
  * @param info The GraphQL resolve info. This includes the field name.
13866
13967
  * @returns Promise to read the resoure for the query.
13867
- * @implements {GraphQLFieldResolver}
13868
13968
  */
13869
13969
  async function resolveField(source, args, _ctx, info) {
13870
13970
  const fieldValue = source?.[info.fieldName];
@@ -13887,6 +13987,68 @@ spurious results.`);
13887
13987
  }
13888
13988
  return array;
13889
13989
  }
13990
+ /**
13991
+ * GraphQL resolver function for create requests.
13992
+ * The field name should end with "Create" (i.e., "PatientCreate" for updating a Patient).
13993
+ * The args should include the data to be created for the specified resource type.
13994
+ * @param _source The source/root object. In the case of creates, this is typically not used and is thus ignored.
13995
+ * @param args The GraphQL arguments, containing the new data for the resource.
13996
+ * @param ctx The GraphQL context. This includes the repository where resources are stored.
13997
+ * @param info The GraphQL resolve info. This includes the schema, field details, and other query-specific information.
13998
+ * @returns A Promise that resolves to the created resource, or undefined if the resource could not be found or updated.
13999
+ */
14000
+ async function resolveByCreate(_source, args, ctx, info) {
14001
+ const fieldName = info.fieldName;
14002
+ // 'Create.length'=== 6 && 'Update.length' === 6
14003
+ const resourceType = fieldName.substring(0, fieldName.length - 6);
14004
+ const resourceArgs = args.res;
14005
+ if (resourceArgs.resourceType !== resourceType) {
14006
+ return [core.badRequest('Invalid resourceType')];
14007
+ }
14008
+ const resource = await ctx.repo.createResource(resourceArgs);
14009
+ return resource;
14010
+ }
14011
+ // Mutation Resolvers
14012
+ /**
14013
+ * GraphQL resolver function for update requests.
14014
+ * The field name should end with "Update" (i.e., "PatientUpdate" for updating a Patient).
14015
+ * The args should include the data to be updated for the specified resource type.
14016
+ * @param _source The source/root object. In the case of updates, this is typically not used and is thus ignored.
14017
+ * @param args The GraphQL arguments, containing the new data for the resource.
14018
+ * @param ctx The GraphQL context. This includes the repository where resources are stored.
14019
+ * @param info The GraphQL resolve info. This includes the schema, field details, and other query-specific information.
14020
+ * @returns A Promise that resolves to the updated resource, or undefined if the resource could not be found or updated.
14021
+ */
14022
+ async function resolveByUpdate(_source, args, ctx, info) {
14023
+ const fieldName = info.fieldName;
14024
+ // 'Create.length'=== 6 && 'Update.length' === 6
14025
+ const resourceType = fieldName.substring(0, fieldName.length - 6);
14026
+ const resourceArgs = args.res;
14027
+ const resourceId = args.id;
14028
+ if (resourceArgs.resourceType !== resourceType) {
14029
+ return [core.badRequest('Invalid resourceType')];
14030
+ }
14031
+ if (resourceId !== resourceArgs.id) {
14032
+ return [core.badRequest('Incorrect ID')];
14033
+ }
14034
+ const resource = await ctx.repo.updateResource(resourceArgs);
14035
+ return resource;
14036
+ }
14037
+ /**
14038
+ * GraphQL resolver function for delete requests.
14039
+ * The field name should end with "Delete" (e.g., "PatientDelete" for deleting a Patient).
14040
+ * The args should include the ID of the resource to be deleted.
14041
+ * @param _source The source/root object. In the case of deletions, this is typically not used and is thus ignored.
14042
+ * @param args The GraphQL arguments, containing the ID of the resource to be deleted.
14043
+ * @param ctx The GraphQL context. This includes the repository where resources are stored.
14044
+ * @param info The GraphQL resolve info. This includes the schema, field details, and other query-specific information.
14045
+ * @returns A Promise that resolves when the resource has been deleted. No value is returned.
14046
+ */
14047
+ async function resolveByDelete(_source, args, ctx, info) {
14048
+ const fieldName = info.fieldName;
14049
+ const resourceType = fieldName.substring(0, fieldName.length - 'Delete'.length);
14050
+ await ctx.repo.deleteResource(resourceType, args.id);
14051
+ }
13890
14052
  function parseSearchArgs(resourceType, source, args) {
13891
14053
  let referenceFilter = undefined;
13892
14054
  if (source) {
@@ -14111,8 +14273,9 @@ spurious results.`);
14111
14273
  return [core.allOk, resource];
14112
14274
  }
14113
14275
  class FhirRouter {
14114
- constructor() {
14276
+ constructor(options = {}) {
14115
14277
  this.router = new Router();
14278
+ this.options = options;
14116
14279
  this.router.add('POST', '', batch);
14117
14280
  this.router.add('GET', ':resourceType', search);
14118
14281
  this.router.add('POST', ':resourceType/_search', searchByPost);
@@ -14132,7 +14295,12 @@ spurious results.`);
14132
14295
  }
14133
14296
  const { handler, params } = result;
14134
14297
  req.params = params;
14135
- return handler(req, repo, this);
14298
+ try {
14299
+ return await handler(req, repo, this);
14300
+ }
14301
+ catch (err) {
14302
+ return [core.normalizeOperationOutcome(err)];
14303
+ }
14136
14304
  }
14137
14305
  }
14138
14306
 
@@ -14145,7 +14313,6 @@ spurious results.`);
14145
14313
  * The return value is the resource, if available; otherwise, undefined.
14146
14314
  *
14147
14315
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
14148
- *
14149
14316
  * @param searchRequest The FHIR search request.
14150
14317
  * @returns Promise to the first search result or undefined.
14151
14318
  */
@@ -14161,7 +14328,6 @@ spurious results.`);
14161
14328
  * The return value is an array of resources.
14162
14329
  *
14163
14330
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
14164
- *
14165
14331
  * @param searchRequest The FHIR search request.
14166
14332
  * @returns Promise to the array of search results.
14167
14333
  */