@medplum/fhir-router 2.0.21 → 2.0.23

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,131 +13375,202 @@ spurious results.`);
13378
13375
  'http://hl7.org/fhirpath/System.String': GraphQLString,
13379
13376
  'http://hl7.org/fhirpath/System.Time': GraphQLString,
13380
13377
  };
13378
+ function parseSearchArgs(resourceType, source, args) {
13379
+ let referenceFilter = undefined;
13380
+ if (source) {
13381
+ // _reference is a required field for reverse lookup searches
13382
+ // The GraphQL parser will validate that it is there.
13383
+ const reference = args['_reference'];
13384
+ delete args['_reference'];
13385
+ referenceFilter = {
13386
+ code: reference,
13387
+ operator: core.Operator.EQUALS,
13388
+ value: core.getReferenceString(source),
13389
+ };
13390
+ }
13391
+ // Reverse the transform of dashes to underscores, back to dashes
13392
+ args = Object.fromEntries(Object.entries(args).map(([key, value]) => [graphQLFieldToFhirParam(key), value]));
13393
+ // Parse the search request
13394
+ const searchRequest = core.parseSearchRequest(resourceType, args);
13395
+ // If a reverse lookup filter was specified,
13396
+ // add it to the search request.
13397
+ if (referenceFilter) {
13398
+ const existingFilters = searchRequest.filters || [];
13399
+ searchRequest.filters = [referenceFilter, ...existingFilters];
13400
+ }
13401
+ return searchRequest;
13402
+ }
13403
+ function graphQLFieldToFhirParam(code) {
13404
+ return code.startsWith('_') ? code : code.replaceAll('_', '-');
13405
+ }
13406
+ function fhirParamToGraphQLField(code) {
13407
+ return code.replaceAll('-', '_');
13408
+ }
13381
13409
  /**
13382
- * Cache of "introspection" query results.
13383
- * Common case is the standard schema query from GraphiQL and Insomnia.
13384
- * The result is big and somewhat computationally expensive.
13410
+ * GraphQL data loader for search requests.
13411
+ * The field name should always end with "List" (i.e., "Patient" search uses "PatientList").
13412
+ * The search args should be FHIR search parameters.
13413
+ * @param source The source/root. This should always be null for our top level readers.
13414
+ * @param args The GraphQL search arguments.
13415
+ * @param ctx The GraphQL context.
13416
+ * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13417
+ * @returns Promise to read the resoures for the query.
13385
13418
  */
13386
- const introspectionResults = new core.LRUCache();
13419
+ async function resolveBySearch(source, args, ctx, info) {
13420
+ const fieldName = info.fieldName;
13421
+ const resourceType = fieldName.substring(0, fieldName.length - 'List'.length);
13422
+ const searchRequest = parseSearchArgs(resourceType, source, args);
13423
+ const bundle = await ctx.repo.search(searchRequest);
13424
+ return bundle.entry?.map((e) => e.resource);
13425
+ }
13426
+ function buildSearchArgs(resourceType) {
13427
+ const args = {
13428
+ _count: {
13429
+ type: GraphQLInt,
13430
+ description: 'Specify how many elements to return from a repeating list.',
13431
+ },
13432
+ _offset: {
13433
+ type: GraphQLInt,
13434
+ description: 'Specify the offset to start at for a repeating element.',
13435
+ },
13436
+ _sort: {
13437
+ type: GraphQLString,
13438
+ description: 'Specify the sort order by comma-separated list of sort rules in priority order.',
13439
+ },
13440
+ _id: {
13441
+ type: GraphQLString,
13442
+ description: 'Select resources based on the logical id of the resource.',
13443
+ },
13444
+ _lastUpdated: {
13445
+ type: GraphQLString,
13446
+ description: 'Select resources based on the last time they were changed.',
13447
+ },
13448
+ };
13449
+ const searchParams = core.getSearchParameters(resourceType);
13450
+ if (searchParams) {
13451
+ for (const [code, searchParam] of Object.entries(searchParams)) {
13452
+ // GraphQL does not support dashes in argument names
13453
+ // So convert dashes to underscores
13454
+ args[fhirParamToGraphQLField(code)] = {
13455
+ type: GraphQLString,
13456
+ description: searchParam.description,
13457
+ };
13458
+ }
13459
+ }
13460
+ return args;
13461
+ }
13387
13462
  /**
13388
- * Cached GraphQL schema.
13389
- * This should be initialized at server startup.
13463
+ * Returns the depth of the GraphQL node in a query.
13464
+ * We use "selections" as the representation of depth.
13465
+ * As a rough approximation, it's the number of indentations in a well formatted query.
13466
+ * @param path The GraphQL node path.
13467
+ * @returns The "depth" of the node.
13390
13468
  */
13391
- let rootSchema;
13469
+ function getDepth(path) {
13470
+ return path.filter((p) => p === 'selections').length;
13471
+ }
13392
13472
  /**
13393
- * Handles FHIR GraphQL requests.
13394
- *
13395
- * See: https://www.hl7.org/fhir/graphql.html
13473
+ * Returns true if the field is requested in the GraphQL query.
13474
+ * @param info The GraphQL resolve info. This includes the field name.
13475
+ * @param fieldName The field name to check.
13476
+ * @returns True if the field is requested in the GraphQL query.
13396
13477
  */
13397
- async function graphqlHandler(req, repo) {
13398
- const { query, operationName, variables } = req.body;
13399
- if (!query) {
13400
- return [core.badRequest('Must provide query.')];
13401
- }
13402
- let document;
13403
- try {
13404
- document = parse(query);
13405
- }
13406
- catch (err) {
13407
- return [core.badRequest('GraphQL syntax error.')];
13408
- }
13409
- const schema = getRootSchema();
13410
- const validationRules = [...specifiedRules, MaxDepthRule];
13411
- const validationErrors = validate(schema, document, validationRules);
13412
- if (validationErrors.length > 0) {
13413
- return [invalidRequest(validationErrors)];
13414
- }
13415
- const introspection = isIntrospectionQuery(query);
13416
- if (introspection) {
13417
- return [core.forbidden];
13418
- }
13419
- const dataLoader = new DataLoader((keys) => repo.readReferences(keys));
13420
- let result = introspection && introspectionResults.get(query);
13421
- if (!result) {
13422
- result = await execute({
13423
- schema,
13424
- document,
13425
- contextValue: { repo, dataLoader },
13426
- operationName,
13427
- variableValues: variables,
13428
- });
13429
- }
13430
- return [core.allOk, result];
13478
+ function isFieldRequested(info, fieldName) {
13479
+ return info.fieldNodes.some((fieldNode) => fieldNode.selectionSet?.selections.some((selection) => {
13480
+ return selection.kind === 'Field' && selection.name.value === fieldName;
13481
+ }));
13431
13482
  }
13432
13483
  /**
13433
- * Returns true if the query is a GraphQL introspection query.
13434
- *
13435
- * Introspection queries ask for the schema, which is expensive.
13436
- *
13437
- * See: https://graphql.org/learn/introspection/
13438
- *
13439
- * @param query The GraphQL query.
13440
- * @returns True if the query is an introspection query.
13484
+ * Returns an OperationOutcome for GraphQL errors.
13485
+ * @param errors Array of GraphQL errors.
13486
+ * @returns OperationOutcome with the GraphQL errors as OperationOutcome issues.
13441
13487
  */
13442
- function isIntrospectionQuery(query) {
13443
- return query.includes('query IntrospectionQuery') || query.includes('__schema') || query.includes('__type');
13488
+ function invalidRequest(errors) {
13489
+ return {
13490
+ resourceType: 'OperationOutcome',
13491
+ issue: errors.map((error) => ({
13492
+ severity: 'error',
13493
+ code: 'invalid',
13494
+ details: { text: error.message },
13495
+ })),
13496
+ };
13444
13497
  }
13445
- function getRootSchema() {
13446
- if (!rootSchema) {
13447
- rootSchema = buildRootSchema();
13498
+
13499
+ const inputTypeCache = {
13500
+ ...typeCache,
13501
+ };
13502
+ function getGraphQLInputType(inputType, nameSuffix) {
13503
+ let result = inputTypeCache[inputType];
13504
+ if (!result) {
13505
+ result = buildGraphQLInputType(inputType, nameSuffix);
13506
+ inputTypeCache[inputType] = result;
13448
13507
  }
13449
- return rootSchema;
13508
+ return result;
13450
13509
  }
13451
- function buildRootSchema() {
13452
- // First, create placeholder types
13453
- // We need this first for circular dependencies
13454
- for (const resourceType of core.getResourceTypes()) {
13455
- typeCache[resourceType] = buildGraphQLType(resourceType);
13456
- }
13457
- // Next, fill in all of the type properties
13510
+ function buildGraphQLInputType(resourceType, nameSuffix) {
13511
+ const schema = core.getResourceTypeSchema(resourceType);
13512
+ return new GraphQLInputObjectType({
13513
+ name: resourceType + nameSuffix,
13514
+ description: schema.description,
13515
+ fields: () => buildGraphQLInputFields(resourceType, nameSuffix),
13516
+ });
13517
+ }
13518
+ function buildGraphQLInputFields(resourceType, nameSuffix) {
13458
13519
  const fields = {};
13459
- for (const resourceType of core.getResourceTypes()) {
13460
- const graphQLType = getGraphQLType(resourceType);
13461
- // Get resource by ID
13462
- fields[resourceType] = {
13463
- type: graphQLType,
13464
- args: {
13465
- id: {
13466
- type: new GraphQLNonNull(GraphQLID),
13467
- description: resourceType + ' ID',
13468
- },
13469
- },
13470
- resolve: resolveById,
13471
- };
13472
- // Search resource by search parameters
13473
- fields[resourceType + 'List'] = {
13474
- type: new GraphQLList(graphQLType),
13475
- args: buildSearchArgs(resourceType),
13476
- resolve: resolveBySearch,
13477
- };
13478
- // FHIR GraphQL Connection API
13479
- fields[resourceType + 'Connection'] = {
13480
- type: buildConnectionType(resourceType, graphQLType),
13481
- args: buildSearchArgs(resourceType),
13482
- resolve: resolveByConnectionApi,
13520
+ // Add resourceType field for root resource
13521
+ if (core.isResourceType(resourceType)) {
13522
+ const propertyFieldConfig = {
13523
+ description: 'The type of resource',
13524
+ type: GraphQLString,
13483
13525
  };
13526
+ fields['resourceType'] = propertyFieldConfig;
13484
13527
  }
13485
- return new GraphQLSchema({
13486
- query: new GraphQLObjectType({
13487
- name: 'QueryType',
13488
- fields,
13489
- }),
13490
- });
13528
+ buildInputPropertyFields(resourceType, fields, nameSuffix);
13529
+ return fields;
13530
+ }
13531
+ function buildInputPropertyFields(resourceType, fields, nameSuffix) {
13532
+ const schema = core.getResourceTypeSchema(resourceType);
13533
+ const properties = schema.properties;
13534
+ for (const key of Object.keys(properties)) {
13535
+ const elementDefinition = core.getElementDefinition(resourceType, key);
13536
+ for (const type of elementDefinition.type) {
13537
+ buildInputPropertyField(fields, key, elementDefinition, type, nameSuffix);
13538
+ }
13539
+ }
13540
+ }
13541
+ function buildInputPropertyField(fields, key, elementDefinition, elementDefinitionType, nameSuffix) {
13542
+ let typeName = elementDefinitionType.code;
13543
+ if (typeName === 'Element' || typeName === 'BackboneElement') {
13544
+ typeName = core.buildTypeName(elementDefinition.path?.split('.'));
13545
+ }
13546
+ const fieldConfig = {
13547
+ description: elementDefinition.short,
13548
+ type: getGraphQLInputType(typeName, nameSuffix),
13549
+ };
13550
+ if (elementDefinition.max === '*') {
13551
+ fieldConfig.type = new GraphQLList(getGraphQLInputType(typeName, nameSuffix));
13552
+ }
13553
+ const propertyName = key.replace('[x]', core.capitalize(elementDefinitionType.code));
13554
+ fields[propertyName] = fieldConfig;
13491
13555
  }
13492
- function getGraphQLType(resourceType) {
13493
- let result = typeCache[resourceType];
13556
+
13557
+ const outputTypeCache = {
13558
+ ...typeCache,
13559
+ };
13560
+ function getGraphQLOutputType(inputType) {
13561
+ let result = outputTypeCache[inputType];
13494
13562
  if (!result) {
13495
- result = buildGraphQLType(resourceType);
13496
- typeCache[resourceType] = result;
13563
+ result = buildGraphQLOutputType(inputType);
13564
+ outputTypeCache[inputType] = result;
13497
13565
  }
13498
13566
  return result;
13499
13567
  }
13500
- function buildGraphQLType(resourceType) {
13568
+ function buildGraphQLOutputType(resourceType) {
13501
13569
  if (resourceType === 'ResourceList') {
13502
13570
  return new GraphQLUnionType({
13503
13571
  name: 'ResourceList',
13504
13572
  types: () => core.getResourceTypes()
13505
- .map(getGraphQLType)
13573
+ .map(getGraphQLOutputType)
13506
13574
  .filter((t) => !!t),
13507
13575
  resolveType: resolveTypeByReference,
13508
13576
  });
@@ -13511,16 +13579,16 @@ spurious results.`);
13511
13579
  return new GraphQLObjectType({
13512
13580
  name: resourceType,
13513
13581
  description: schema.description,
13514
- fields: () => buildGraphQLFields(resourceType),
13582
+ fields: () => buildGraphQLOutputFields(resourceType),
13515
13583
  });
13516
13584
  }
13517
- function buildGraphQLFields(resourceType) {
13585
+ function buildGraphQLOutputFields(resourceType) {
13518
13586
  const fields = {};
13519
- buildPropertyFields(resourceType, fields);
13587
+ buildOutputPropertyFields(resourceType, fields);
13520
13588
  buildReverseLookupFields(resourceType, fields);
13521
13589
  return fields;
13522
13590
  }
13523
- function buildPropertyFields(resourceType, fields) {
13591
+ function buildOutputPropertyFields(resourceType, fields) {
13524
13592
  const schema = core.getResourceTypeSchema(resourceType);
13525
13593
  const properties = schema.properties;
13526
13594
  if (core.isResourceTypeSchema(schema)) {
@@ -13532,25 +13600,25 @@ spurious results.`);
13532
13600
  if (resourceType === 'Reference') {
13533
13601
  fields.resource = {
13534
13602
  description: 'Reference',
13535
- type: getGraphQLType('ResourceList'),
13603
+ type: getGraphQLOutputType('ResourceList'),
13536
13604
  resolve: resolveByReference,
13537
13605
  };
13538
13606
  }
13539
13607
  for (const key of Object.keys(properties)) {
13540
13608
  const elementDefinition = core.getElementDefinition(resourceType, key);
13541
13609
  for (const type of elementDefinition.type) {
13542
- buildPropertyField(fields, key, elementDefinition, type);
13610
+ buildOutputPropertyField(fields, key, elementDefinition, type);
13543
13611
  }
13544
13612
  }
13545
13613
  }
13546
- function buildPropertyField(fields, key, elementDefinition, elementDefinitionType) {
13614
+ function buildOutputPropertyField(fields, key, elementDefinition, elementDefinitionType) {
13547
13615
  let typeName = elementDefinitionType.code;
13548
13616
  if (typeName === 'Element' || typeName === 'BackboneElement') {
13549
13617
  typeName = core.buildTypeName(elementDefinition.path?.split('.'));
13550
13618
  }
13551
13619
  const fieldConfig = {
13552
13620
  description: elementDefinition.short,
13553
- type: getPropertyType(elementDefinition, typeName),
13621
+ type: getOutputPropertyType(elementDefinition, typeName),
13554
13622
  resolve: resolveField,
13555
13623
  };
13556
13624
  if (elementDefinition.max === '*') {
@@ -13569,7 +13637,6 @@ spurious results.`);
13569
13637
  * 4. All properties of the list element type.
13570
13638
  *
13571
13639
  * See: https://hl7.org/fhir/R4/graphql.html#list
13572
- *
13573
13640
  * @param fieldTypeName The type name of the field.
13574
13641
  * @returns The arguments for the field.
13575
13642
  */
@@ -13654,13 +13721,12 @@ spurious results.`);
13654
13721
  * (except that the "id" argument is prohibited here as nonsensical).
13655
13722
  *
13656
13723
  * See: https://www.hl7.org/fhir/graphql.html#reverse
13657
- *
13658
13724
  * @param resourceType The resource type to build fields for.
13659
13725
  * @param fields The fields object to add fields to.
13660
13726
  */
13661
13727
  function buildReverseLookupFields(resourceType, fields) {
13662
13728
  for (const childResourceType of core.getResourceTypes()) {
13663
- const childGraphQLType = getGraphQLType(childResourceType);
13729
+ const childGraphQLType = getGraphQLOutputType(childResourceType);
13664
13730
  const childSearchParams = core.getSearchParameters(childResourceType);
13665
13731
  const enumValues = {};
13666
13732
  let count = 0;
@@ -13690,49 +13756,236 @@ spurious results.`);
13690
13756
  }
13691
13757
  }
13692
13758
  }
13693
- function buildSearchArgs(resourceType) {
13694
- const args = {
13695
- _count: {
13696
- type: GraphQLInt,
13697
- description: 'Specify how many elements to return from a repeating list.',
13698
- },
13699
- _offset: {
13700
- type: GraphQLInt,
13701
- description: 'Specify the offset to start at for a repeating element.',
13702
- },
13703
- _sort: {
13704
- type: GraphQLString,
13705
- description: 'Specify the sort order by comma-separated list of sort rules in priority order.',
13706
- },
13707
- _id: {
13708
- type: GraphQLString,
13709
- description: 'Select resources based on the logical id of the resource.',
13710
- },
13711
- _lastUpdated: {
13712
- type: GraphQLString,
13713
- description: 'Select resources based on the last time they were changed.',
13714
- },
13715
- };
13716
- const searchParams = core.getSearchParameters(resourceType);
13717
- if (searchParams) {
13718
- for (const [code, searchParam] of Object.entries(searchParams)) {
13719
- // GraphQL does not support dashes in argument names
13720
- // So convert dashes to underscores
13721
- args[fhirParamToGraphQLField(code)] = {
13722
- type: GraphQLString,
13723
- description: searchParam.description,
13724
- };
13725
- }
13726
- }
13727
- return args;
13728
- }
13729
- function getPropertyType(elementDefinition, typeName) {
13730
- const graphqlType = getGraphQLType(typeName);
13759
+ function getOutputPropertyType(elementDefinition, typeName) {
13760
+ const graphqlType = getGraphQLOutputType(typeName);
13731
13761
  if (elementDefinition.max === '*') {
13732
13762
  return new GraphQLList(graphqlType);
13733
13763
  }
13734
13764
  return graphqlType;
13735
13765
  }
13766
+ /**
13767
+ * GraphQL resolver for fields.
13768
+ * In the common case, this is just a matter of returning the field value from the source object.
13769
+ * If the field is a list and the user specifies list arguments, then we can apply those arguments here.
13770
+ * @param source The source. This is the object that contains the field.
13771
+ * @param args The GraphQL search arguments.
13772
+ * @param _ctx The GraphQL context.
13773
+ * @param info The GraphQL resolve info. This includes the field name.
13774
+ * @returns Promise to read the resoure for the query.
13775
+ */
13776
+ async function resolveField(source, args, _ctx, info) {
13777
+ const fieldValue = source?.[info.fieldName];
13778
+ if (!args || !fieldValue) {
13779
+ return fieldValue;
13780
+ }
13781
+ const { _offset, _count, fhirpath, ...rest } = args;
13782
+ let array = fieldValue;
13783
+ for (const [key, value] of Object.entries(rest)) {
13784
+ array = array.filter((item) => item[key] === value);
13785
+ }
13786
+ if (fhirpath) {
13787
+ array = array.filter((item) => core.toJsBoolean(core.evalFhirPathTyped(fhirpath, [core.toTypedValue(item)])));
13788
+ }
13789
+ if (_offset) {
13790
+ array = array.slice(_offset);
13791
+ }
13792
+ if (_count) {
13793
+ array = array.slice(0, _count);
13794
+ }
13795
+ return array;
13796
+ }
13797
+ /**
13798
+ * GraphQL data loader for Reference requests.
13799
+ * This is a special data loader for following Reference objects.
13800
+ * @param source The source/root. This should always be null for our top level readers.
13801
+ * @param _args The GraphQL search arguments.
13802
+ * @param ctx The GraphQL context.
13803
+ * @returns Promise to read the resoure(s) for the query.
13804
+ */
13805
+ async function resolveByReference(source, _args, ctx) {
13806
+ try {
13807
+ return await ctx.dataLoader.load(source);
13808
+ }
13809
+ catch (err) {
13810
+ throw new core.OperationOutcomeError(core.normalizeOperationOutcome(err), err);
13811
+ }
13812
+ }
13813
+ /**
13814
+ * GraphQL type resolver for resources.
13815
+ * When loading a resource via reference, GraphQL needs to know the type of the resource.
13816
+ * @param resource The loaded resource.
13817
+ * @returns The GraphQL type of the resource.
13818
+ */
13819
+ function resolveTypeByReference(resource) {
13820
+ const resourceType = resource?.resourceType;
13821
+ if (!resourceType) {
13822
+ return undefined;
13823
+ }
13824
+ return getGraphQLOutputType(resourceType).name;
13825
+ }
13826
+
13827
+ /**
13828
+ * Cache of "introspection" query results.
13829
+ * Common case is the standard schema query from GraphiQL and Insomnia.
13830
+ * The result is big and somewhat computationally expensive.
13831
+ */
13832
+ const introspectionResults = new core.LRUCache();
13833
+ /**
13834
+ * Cached GraphQL schema.
13835
+ * This should be initialized at server startup.
13836
+ */
13837
+ let rootSchema;
13838
+ /**
13839
+ * Handles FHIR GraphQL requests.
13840
+ *
13841
+ * See: https://www.hl7.org/fhir/graphql.html
13842
+ * @param req The request details.
13843
+ * @param repo The current user FHIR repository.
13844
+ * @param router The router for router options.
13845
+ * @returns The response.
13846
+ */
13847
+ async function graphqlHandler(req, repo, router) {
13848
+ const { query, operationName, variables } = req.body;
13849
+ if (!query) {
13850
+ return [core.badRequest('Must provide query.')];
13851
+ }
13852
+ let document;
13853
+ try {
13854
+ document = parse(query);
13855
+ }
13856
+ catch (err) {
13857
+ return [core.badRequest('GraphQL syntax error.')];
13858
+ }
13859
+ const schema = getRootSchema();
13860
+ const validationRules = [...specifiedRules, MaxDepthRule];
13861
+ const validationErrors = validate(schema, document, validationRules);
13862
+ if (validationErrors.length > 0) {
13863
+ return [invalidRequest(validationErrors)];
13864
+ }
13865
+ const introspection = isIntrospectionQuery(query);
13866
+ if (introspection && !router.options?.introspectionEnabled) {
13867
+ return [core.forbidden];
13868
+ }
13869
+ const dataLoader = new DataLoader((keys) => repo.readReferences(keys));
13870
+ let result = introspection && introspectionResults.get(query);
13871
+ if (!result) {
13872
+ result = await execute({
13873
+ schema,
13874
+ document,
13875
+ contextValue: { repo, dataLoader },
13876
+ operationName,
13877
+ variableValues: variables,
13878
+ });
13879
+ }
13880
+ return [core.allOk, result];
13881
+ }
13882
+ /**
13883
+ * Returns true if the query is a GraphQL introspection query.
13884
+ *
13885
+ * Introspection queries ask for the schema, which is expensive.
13886
+ *
13887
+ * See: https://graphql.org/learn/introspection/
13888
+ * @param query The GraphQL query.
13889
+ * @returns True if the query is an introspection query.
13890
+ */
13891
+ function isIntrospectionQuery(query) {
13892
+ return query.includes('query IntrospectionQuery') || query.includes('__schema') || query.includes('__type');
13893
+ }
13894
+ function getRootSchema() {
13895
+ if (!rootSchema) {
13896
+ rootSchema = buildRootSchema();
13897
+ }
13898
+ return rootSchema;
13899
+ }
13900
+ function buildRootSchema() {
13901
+ // First, create placeholder types
13902
+ // We need this first for circular dependencies
13903
+ for (const resourceType of core.getResourceTypes()) {
13904
+ outputTypeCache[resourceType] = buildGraphQLOutputType(resourceType);
13905
+ }
13906
+ // Next, fill in all of the type properties
13907
+ const fields = {};
13908
+ const mutationFields = {};
13909
+ for (const resourceType of core.getResourceTypes()) {
13910
+ const graphQLOutputType = getGraphQLOutputType(resourceType);
13911
+ // Get resource by ID
13912
+ fields[resourceType] = {
13913
+ type: graphQLOutputType,
13914
+ args: {
13915
+ id: {
13916
+ type: new GraphQLNonNull(GraphQLID),
13917
+ description: resourceType + ' ID',
13918
+ },
13919
+ },
13920
+ resolve: resolveById,
13921
+ };
13922
+ // Search resource by search parameters
13923
+ fields[resourceType + 'List'] = {
13924
+ type: new GraphQLList(graphQLOutputType),
13925
+ args: buildSearchArgs(resourceType),
13926
+ resolve: resolveBySearch,
13927
+ };
13928
+ // FHIR GraphQL Connection API
13929
+ fields[resourceType + 'Connection'] = {
13930
+ type: buildConnectionType(resourceType, graphQLOutputType),
13931
+ args: buildSearchArgs(resourceType),
13932
+ resolve: resolveByConnectionApi,
13933
+ };
13934
+ // Mutation API
13935
+ mutationFields[resourceType + 'Create'] = {
13936
+ type: graphQLOutputType,
13937
+ args: buildCreateArgs(resourceType),
13938
+ resolve: resolveByCreate,
13939
+ };
13940
+ mutationFields[resourceType + 'Update'] = {
13941
+ type: graphQLOutputType,
13942
+ args: buildUpdateArgs(resourceType),
13943
+ resolve: resolveByUpdate,
13944
+ };
13945
+ mutationFields[resourceType + 'Delete'] = {
13946
+ type: graphQLOutputType,
13947
+ args: {
13948
+ id: {
13949
+ type: new GraphQLNonNull(GraphQLID),
13950
+ description: resourceType + ' ID',
13951
+ },
13952
+ },
13953
+ resolve: resolveByDelete,
13954
+ };
13955
+ }
13956
+ return new GraphQLSchema({
13957
+ query: new GraphQLObjectType({
13958
+ name: 'QueryType',
13959
+ fields,
13960
+ }),
13961
+ mutation: new GraphQLObjectType({
13962
+ name: 'MutationType',
13963
+ fields: mutationFields,
13964
+ }),
13965
+ });
13966
+ }
13967
+ function buildCreateArgs(resourceType) {
13968
+ const args = {
13969
+ res: {
13970
+ type: getGraphQLInputType(resourceType, 'Create'),
13971
+ description: resourceType + ' Create',
13972
+ },
13973
+ };
13974
+ return args;
13975
+ }
13976
+ function buildUpdateArgs(resourceType) {
13977
+ const args = {
13978
+ id: {
13979
+ type: new GraphQLNonNull(GraphQLID),
13980
+ description: resourceType + ' ID',
13981
+ },
13982
+ res: {
13983
+ type: getGraphQLInputType(resourceType, 'Update'),
13984
+ description: resourceType + ' Update',
13985
+ },
13986
+ };
13987
+ return args;
13988
+ }
13736
13989
  function buildConnectionType(resourceType, resourceGraphQLType) {
13737
13990
  return new GraphQLObjectType({
13738
13991
  name: resourceType + 'Connection',
@@ -13766,25 +14019,6 @@ spurious results.`);
13766
14019
  * @param ctx The GraphQL context.
13767
14020
  * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13768
14021
  * @returns Promise to read the resoures for the query.
13769
- * @implements {GraphQLFieldResolver}
13770
- */
13771
- async function resolveBySearch(source, args, ctx, info) {
13772
- const fieldName = info.fieldName;
13773
- const resourceType = fieldName.substring(0, fieldName.length - 'List'.length);
13774
- const searchRequest = parseSearchArgs(resourceType, source, args);
13775
- const bundle = await ctx.repo.search(searchRequest);
13776
- return bundle.entry?.map((e) => e.resource);
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
14022
  */
13789
14023
  async function resolveByConnectionApi(source, args, ctx, info) {
13790
14024
  const fieldName = info.fieldName;
@@ -13814,7 +14048,6 @@ spurious results.`);
13814
14048
  * @param ctx The GraphQL context.
13815
14049
  * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13816
14050
  * @returns Promise to read the resoure for the query.
13817
- * @implements {GraphQLFieldResolver}
13818
14051
  */
13819
14052
  async function resolveById(_source, args, ctx, info) {
13820
14053
  try {
@@ -13825,98 +14058,66 @@ spurious results.`);
13825
14058
  }
13826
14059
  }
13827
14060
  /**
13828
- * GraphQL data loader for Reference requests.
13829
- * This is a special data loader for following Reference objects.
13830
- * @param source The source/root. This should always be null for our top level readers.
13831
- * @param _args The GraphQL search arguments.
13832
- * @param ctx The GraphQL context.
13833
- * @returns Promise to read the resoure(s) for the query.
13834
- * @implements {GraphQLFieldResolver}
13835
- */
13836
- async function resolveByReference(source, _args, ctx) {
13837
- try {
13838
- return await ctx.dataLoader.load(source);
13839
- }
13840
- catch (err) {
13841
- throw new core.OperationOutcomeError(core.normalizeOperationOutcome(err), err);
13842
- }
13843
- }
13844
- /**
13845
- * GraphQL type resolver for resources.
13846
- * When loading a resource via reference, GraphQL needs to know the type of the resource.
13847
- * @param resource The loaded resource.
13848
- * @returns The GraphQL type of the resource.
13849
- * @implements {GraphQLTypeResolver}
14061
+ * GraphQL resolver function for create requests.
14062
+ * The field name should end with "Create" (i.e., "PatientCreate" for updating a Patient).
14063
+ * The args should include the data to be created for the specified resource type.
14064
+ * @param _source The source/root object. In the case of creates, this is typically not used and is thus ignored.
14065
+ * @param args The GraphQL arguments, containing the new data for the resource.
14066
+ * @param ctx The GraphQL context. This includes the repository where resources are stored.
14067
+ * @param info The GraphQL resolve info. This includes the schema, field details, and other query-specific information.
14068
+ * @returns A Promise that resolves to the created resource, or undefined if the resource could not be found or updated.
13850
14069
  */
13851
- function resolveTypeByReference(resource) {
13852
- const resourceType = resource?.resourceType;
13853
- if (!resourceType) {
13854
- return undefined;
14070
+ async function resolveByCreate(_source, args, ctx, info) {
14071
+ const fieldName = info.fieldName;
14072
+ // 'Create.length'=== 6 && 'Update.length' === 6
14073
+ const resourceType = fieldName.substring(0, fieldName.length - 6);
14074
+ const resourceArgs = args.res;
14075
+ if (resourceArgs.resourceType !== resourceType) {
14076
+ return [core.badRequest('Invalid resourceType')];
14077
+ }
14078
+ const resource = await ctx.repo.createResource(resourceArgs);
14079
+ return resource;
14080
+ }
14081
+ // Mutation Resolvers
14082
+ /**
14083
+ * GraphQL resolver function for update requests.
14084
+ * The field name should end with "Update" (i.e., "PatientUpdate" for updating a Patient).
14085
+ * The args should include the data to be updated for the specified resource type.
14086
+ * @param _source The source/root object. In the case of updates, this is typically not used and is thus ignored.
14087
+ * @param args The GraphQL arguments, containing the new data for the resource.
14088
+ * @param ctx The GraphQL context. This includes the repository where resources are stored.
14089
+ * @param info The GraphQL resolve info. This includes the schema, field details, and other query-specific information.
14090
+ * @returns A Promise that resolves to the updated resource, or undefined if the resource could not be found or updated.
14091
+ */
14092
+ async function resolveByUpdate(_source, args, ctx, info) {
14093
+ const fieldName = info.fieldName;
14094
+ // 'Create.length'=== 6 && 'Update.length' === 6
14095
+ const resourceType = fieldName.substring(0, fieldName.length - 6);
14096
+ const resourceArgs = args.res;
14097
+ const resourceId = args.id;
14098
+ if (resourceArgs.resourceType !== resourceType) {
14099
+ return [core.badRequest('Invalid resourceType')];
14100
+ }
14101
+ if (resourceId !== resourceArgs.id) {
14102
+ return [core.badRequest('Incorrect ID')];
13855
14103
  }
13856
- return getGraphQLType(resourceType).name;
14104
+ const resource = await ctx.repo.updateResource(resourceArgs);
14105
+ return resource;
13857
14106
  }
13858
14107
  /**
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}
14108
+ * GraphQL resolver function for delete requests.
14109
+ * The field name should end with "Delete" (e.g., "PatientDelete" for deleting a Patient).
14110
+ * The args should include the ID of the resource to be deleted.
14111
+ * @param _source The source/root object. In the case of deletions, this is typically not used and is thus ignored.
14112
+ * @param args The GraphQL arguments, containing the ID of the resource to be deleted.
14113
+ * @param ctx The GraphQL context. This includes the repository where resources are stored.
14114
+ * @param info The GraphQL resolve info. This includes the schema, field details, and other query-specific information.
14115
+ * @returns A Promise that resolves when the resource has been deleted. No value is returned.
13868
14116
  */
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
- }
13890
- function parseSearchArgs(resourceType, source, args) {
13891
- let referenceFilter = undefined;
13892
- if (source) {
13893
- // _reference is a required field for reverse lookup searches
13894
- // The GraphQL parser will validate that it is there.
13895
- const reference = args['_reference'];
13896
- delete args['_reference'];
13897
- referenceFilter = {
13898
- code: reference,
13899
- operator: core.Operator.EQUALS,
13900
- value: core.getReferenceString(source),
13901
- };
13902
- }
13903
- // Reverse the transform of dashes to underscores, back to dashes
13904
- args = Object.fromEntries(Object.entries(args).map(([key, value]) => [graphQLFieldToFhirParam(key), value]));
13905
- // Parse the search request
13906
- const searchRequest = core.parseSearchRequest(resourceType, args);
13907
- // If a reverse lookup filter was specified,
13908
- // add it to the search request.
13909
- if (referenceFilter) {
13910
- const existingFilters = searchRequest.filters || [];
13911
- searchRequest.filters = [referenceFilter, ...existingFilters];
13912
- }
13913
- return searchRequest;
13914
- }
13915
- function fhirParamToGraphQLField(code) {
13916
- return code.replaceAll('-', '_');
13917
- }
13918
- function graphQLFieldToFhirParam(code) {
13919
- return code.startsWith('_') ? code : code.replaceAll('_', '-');
14117
+ async function resolveByDelete(_source, args, ctx, info) {
14118
+ const fieldName = info.fieldName;
14119
+ const resourceType = fieldName.substring(0, fieldName.length - 'Delete'.length);
14120
+ await ctx.repo.deleteResource(resourceType, args.id);
13920
14121
  }
13921
14122
  /**
13922
14123
  * Custom GraphQL rule that enforces max depth constraint.
@@ -13943,42 +14144,6 @@ spurious results.`);
13943
14144
  }
13944
14145
  },
13945
14146
  });
13946
- /**
13947
- * Returns the depth of the GraphQL node in a query.
13948
- * We use "selections" as the representation of depth.
13949
- * As a rough approximation, it's the number of indentations in a well formatted query.
13950
- * @param path The GraphQL node path.
13951
- * @returns The "depth" of the node.
13952
- */
13953
- function getDepth(path) {
13954
- return path.filter((p) => p === 'selections').length;
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
- }
13967
- /**
13968
- * Returns an OperationOutcome for GraphQL errors.
13969
- * @param errors Array of GraphQL errors.
13970
- * @returns OperationOutcome with the GraphQL errors as OperationOutcome issues.
13971
- */
13972
- function invalidRequest(errors) {
13973
- return {
13974
- resourceType: 'OperationOutcome',
13975
- issue: errors.map((error) => ({
13976
- severity: 'error',
13977
- code: 'invalid',
13978
- details: { text: error.message },
13979
- })),
13980
- };
13981
- }
13982
14147
 
13983
14148
  class Router {
13984
14149
  constructor() {
@@ -14111,8 +14276,9 @@ spurious results.`);
14111
14276
  return [core.allOk, resource];
14112
14277
  }
14113
14278
  class FhirRouter {
14114
- constructor() {
14279
+ constructor(options = {}) {
14115
14280
  this.router = new Router();
14281
+ this.options = options;
14116
14282
  this.router.add('POST', '', batch);
14117
14283
  this.router.add('GET', ':resourceType', search);
14118
14284
  this.router.add('POST', ':resourceType/_search', searchByPost);
@@ -14150,7 +14316,6 @@ spurious results.`);
14150
14316
  * The return value is the resource, if available; otherwise, undefined.
14151
14317
  *
14152
14318
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
14153
- *
14154
14319
  * @param searchRequest The FHIR search request.
14155
14320
  * @returns Promise to the first search result or undefined.
14156
14321
  */
@@ -14166,7 +14331,6 @@ spurious results.`);
14166
14331
  * The return value is an array of resources.
14167
14332
  *
14168
14333
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
14169
- *
14170
14334
  * @param searchRequest The FHIR search request.
14171
14335
  * @returns Promise to the array of search results.
14172
14336
  */