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