@medplum/fhir-router 2.0.22 → 2.0.24

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.
@@ -54,7 +54,7 @@
54
54
  const rewritten = this.rewriteIdsInObject(entry);
55
55
  // If the resource 'id' element is specified, we want to replace teh `urn:uuid:*` string and
56
56
  // remove the `resourceType` prefix
57
- if (entry?.resource?.id) {
57
+ if (entry.resource?.id) {
58
58
  rewritten.resource.id = this.rewriteIdsInString(entry.resource.id, true);
59
59
  }
60
60
  try {
@@ -82,7 +82,7 @@
82
82
  const baseUrl = `https://example.com/${entry.resource.resourceType}`;
83
83
  const searchUrl = new URL('?' + request.ifNoneExist, baseUrl);
84
84
  const searchBundle = await this.repo.search(core.parseSearchUrl(searchUrl));
85
- const entries = searchBundle?.entry;
85
+ const entries = searchBundle.entry;
86
86
  if (entries.length > 1) {
87
87
  return buildBundleResponse(core.badRequest('Multiple matches'));
88
88
  }
@@ -138,7 +138,7 @@
138
138
  return JSON.parse(Buffer.from(patchResource.data, 'base64').toString('utf8'));
139
139
  }
140
140
  addReplacementId(fullUrl, resource) {
141
- if (fullUrl?.startsWith('urn:uuid:')) {
141
+ if (fullUrl.startsWith('urn:uuid:')) {
142
142
  this.ids[fullUrl] = resource;
143
143
  }
144
144
  }
@@ -13375,152 +13375,188 @@ spurious results.`);
13375
13375
  'http://hl7.org/fhirpath/System.String': GraphQLString,
13376
13376
  'http://hl7.org/fhirpath/System.Time': GraphQLString,
13377
13377
  };
13378
- const outputTypeCache = {
13379
- ...typeCache,
13380
- };
13381
- const inputTypeCache = {
13382
- ...typeCache,
13383
- };
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
+ }
13384
13409
  /**
13385
- * Cache of "introspection" query results.
13386
- * Common case is the standard schema query from GraphiQL and Insomnia.
13387
- * 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.
13388
13418
  */
13389
- 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
+ }
13390
13462
  /**
13391
- * Cached GraphQL schema.
13392
- * 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.
13393
13468
  */
13394
- let rootSchema;
13469
+ function getDepth(path) {
13470
+ return path.filter((p) => p === 'selections').length;
13471
+ }
13395
13472
  /**
13396
- * Handles FHIR GraphQL requests.
13397
- *
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.
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.
13403
13477
  */
13404
- async function graphqlHandler(req, repo, router) {
13405
- const { query, operationName, variables } = req.body;
13406
- if (!query) {
13407
- return [core.badRequest('Must provide query.')];
13408
- }
13409
- let document;
13410
- try {
13411
- document = parse(query);
13412
- }
13413
- catch (err) {
13414
- return [core.badRequest('GraphQL syntax error.')];
13415
- }
13416
- const schema = getRootSchema();
13417
- const validationRules = [...specifiedRules, MaxDepthRule];
13418
- const validationErrors = validate(schema, document, validationRules);
13419
- if (validationErrors.length > 0) {
13420
- return [invalidRequest(validationErrors)];
13421
- }
13422
- const introspection = isIntrospectionQuery(query);
13423
- if (introspection && !router.options?.introspectionEnabled) {
13424
- return [core.forbidden];
13425
- }
13426
- const dataLoader = new DataLoader((keys) => repo.readReferences(keys));
13427
- let result = introspection && introspectionResults.get(query);
13428
- if (!result) {
13429
- result = await execute({
13430
- schema,
13431
- document,
13432
- contextValue: { repo, dataLoader },
13433
- operationName,
13434
- variableValues: variables,
13435
- });
13436
- }
13437
- return [core.allOk, result];
13478
+ function isFieldRequested(info, fieldName) {
13479
+ return info.fieldNodes.some((fieldNode) => fieldNode.selectionSet?.selections.some((selection) => {
13480
+ return selection.kind === Kind.FIELD && selection.name.value === fieldName;
13481
+ }));
13438
13482
  }
13439
13483
  /**
13440
- * Returns true if the query is a GraphQL introspection query.
13441
- *
13442
- * Introspection queries ask for the schema, which is expensive.
13443
- *
13444
- * See: https://graphql.org/learn/introspection/
13445
- * @param query The GraphQL query.
13446
- * @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.
13447
13487
  */
13448
- function isIntrospectionQuery(query) {
13449
- 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
+ };
13450
13497
  }
13451
- function getRootSchema() {
13452
- if (!rootSchema) {
13453
- 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;
13454
13507
  }
13455
- return rootSchema;
13508
+ return result;
13456
13509
  }
13457
- function buildRootSchema() {
13458
- // First, create placeholder types
13459
- // We need this first for circular dependencies
13460
- for (const resourceType of core.getResourceTypes()) {
13461
- outputTypeCache[resourceType] = buildGraphQLOutputType(resourceType);
13462
- }
13463
- // 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) {
13464
13519
  const fields = {};
13465
- const mutationFields = {};
13466
- for (const resourceType of core.getResourceTypes()) {
13467
- const graphQLOutputType = getGraphQLOutputType(resourceType);
13468
- // Get resource by ID
13469
- fields[resourceType] = {
13470
- type: graphQLOutputType,
13471
- args: {
13472
- id: {
13473
- type: new GraphQLNonNull(GraphQLID),
13474
- description: resourceType + ' ID',
13475
- },
13476
- },
13477
- resolve: resolveById,
13478
- };
13479
- // Search resource by search parameters
13480
- fields[resourceType + 'List'] = {
13481
- type: new GraphQLList(graphQLOutputType),
13482
- args: buildSearchArgs(resourceType),
13483
- resolve: resolveBySearch,
13484
- };
13485
- // FHIR GraphQL Connection API
13486
- fields[resourceType + 'Connection'] = {
13487
- type: buildConnectionType(resourceType, graphQLOutputType),
13488
- args: buildSearchArgs(resourceType),
13489
- resolve: resolveByConnectionApi,
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,
13520
+ // Add resourceType field for root resource
13521
+ if (core.isResourceType(resourceType)) {
13522
+ const propertyFieldConfig = {
13523
+ description: 'The type of resource',
13524
+ type: GraphQLString,
13511
13525
  };
13526
+ fields['resourceType'] = propertyFieldConfig;
13512
13527
  }
13513
- return new GraphQLSchema({
13514
- query: new GraphQLObjectType({
13515
- name: 'QueryType',
13516
- fields,
13517
- }),
13518
- mutation: new GraphQLObjectType({
13519
- name: 'MutationType',
13520
- fields: mutationFields,
13521
- }),
13522
- });
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;
13523
13555
  }
13556
+
13557
+ const outputTypeCache = {
13558
+ ...typeCache,
13559
+ };
13524
13560
  function getGraphQLOutputType(inputType) {
13525
13561
  let result = outputTypeCache[inputType];
13526
13562
  if (!result) {
@@ -13529,14 +13565,6 @@ spurious results.`);
13529
13565
  }
13530
13566
  return result;
13531
13567
  }
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
13568
  function buildGraphQLOutputType(resourceType) {
13541
13569
  if (resourceType === 'ResourceList') {
13542
13570
  return new GraphQLUnionType({
@@ -13554,33 +13582,12 @@ spurious results.`);
13554
13582
  fields: () => buildGraphQLOutputFields(resourceType),
13555
13583
  });
13556
13584
  }
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
13585
  function buildGraphQLOutputFields(resourceType) {
13566
13586
  const fields = {};
13567
13587
  buildOutputPropertyFields(resourceType, fields);
13568
13588
  buildReverseLookupFields(resourceType, fields);
13569
13589
  return fields;
13570
13590
  }
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
13591
  function buildOutputPropertyFields(resourceType, fields) {
13585
13592
  const schema = core.getResourceTypeSchema(resourceType);
13586
13593
  const properties = schema.properties;
@@ -13604,16 +13611,6 @@ spurious results.`);
13604
13611
  }
13605
13612
  }
13606
13613
  }
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);
13614
- }
13615
- }
13616
- }
13617
13614
  function buildOutputPropertyField(fields, key, elementDefinition, elementDefinitionType) {
13618
13615
  let typeName = elementDefinitionType.code;
13619
13616
  if (typeName === 'Element' || typeName === 'BackboneElement') {
@@ -13630,21 +13627,6 @@ spurious results.`);
13630
13627
  const propertyName = key.replace('[x]', core.capitalize(elementDefinitionType.code));
13631
13628
  fields[propertyName] = fieldConfig;
13632
13629
  }
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
- }
13648
13630
  /**
13649
13631
  * Builds field arguments for a list property.
13650
13632
  *
@@ -13750,7 +13732,7 @@ spurious results.`);
13750
13732
  let count = 0;
13751
13733
  if (childSearchParams) {
13752
13734
  for (const [code, searchParam] of Object.entries(childSearchParams)) {
13753
- if (searchParam.target && searchParam.target.includes(resourceType)) {
13735
+ if (searchParam.target?.includes(resourceType)) {
13754
13736
  enumValues[fhirParamToGraphQLField(code)] = { value: code };
13755
13737
  count++;
13756
13738
  }
@@ -13774,46 +13756,221 @@ spurious results.`);
13774
13756
  }
13775
13757
  }
13776
13758
  }
13777
- function buildSearchArgs(resourceType) {
13778
- const args = {
13779
- _count: {
13780
- type: GraphQLInt,
13781
- description: 'Specify how many elements to return from a repeating list.',
13782
- },
13783
- _offset: {
13784
- type: GraphQLInt,
13785
- description: 'Specify the offset to start at for a repeating element.',
13786
- },
13787
- _sort: {
13788
- type: GraphQLString,
13789
- description: 'Specify the sort order by comma-separated list of sort rules in priority order.',
13790
- },
13791
- _id: {
13792
- type: GraphQLString,
13793
- description: 'Select resources based on the logical id of the resource.',
13794
- },
13795
- _lastUpdated: {
13796
- type: GraphQLString,
13797
- description: 'Select resources based on the last time they were changed.',
13798
- },
13799
- };
13800
- const searchParams = core.getSearchParameters(resourceType);
13801
- if (searchParams) {
13802
- for (const [code, searchParam] of Object.entries(searchParams)) {
13803
- // GraphQL does not support dashes in argument names
13804
- // So convert dashes to underscores
13805
- args[fhirParamToGraphQLField(code)] = {
13806
- type: GraphQLString,
13807
- description: searchParam.description,
13808
- };
13809
- }
13759
+ function getOutputPropertyType(elementDefinition, typeName) {
13760
+ let graphqlType = getGraphQLOutputType(typeName);
13761
+ if (elementDefinition.max === '*') {
13762
+ graphqlType = new GraphQLList(graphqlType);
13763
+ }
13764
+ if (elementDefinition.min !== 0 && !elementDefinition.path?.endsWith('[x]')) {
13765
+ graphqlType = new GraphQLNonNull(graphqlType);
13766
+ }
13767
+ return graphqlType;
13768
+ }
13769
+ /**
13770
+ * GraphQL resolver for fields.
13771
+ * In the common case, this is just a matter of returning the field value from the source object.
13772
+ * If the field is a list and the user specifies list arguments, then we can apply those arguments here.
13773
+ * @param source The source. This is the object that contains the field.
13774
+ * @param args The GraphQL search arguments.
13775
+ * @param _ctx The GraphQL context.
13776
+ * @param info The GraphQL resolve info. This includes the field name.
13777
+ * @returns Promise to read the resoure for the query.
13778
+ */
13779
+ async function resolveField(source, args, _ctx, info) {
13780
+ const fieldValue = source?.[info.fieldName];
13781
+ if (!args || !fieldValue) {
13782
+ return fieldValue;
13783
+ }
13784
+ const { _offset, _count, fhirpath, ...rest } = args;
13785
+ let array = fieldValue;
13786
+ for (const [key, value] of Object.entries(rest)) {
13787
+ array = array.filter((item) => item[key] === value);
13788
+ }
13789
+ if (fhirpath) {
13790
+ array = array.filter((item) => core.toJsBoolean(core.evalFhirPathTyped(fhirpath, [core.toTypedValue(item)])));
13791
+ }
13792
+ if (_offset) {
13793
+ array = array.slice(_offset);
13794
+ }
13795
+ if (_count) {
13796
+ array = array.slice(0, _count);
13797
+ }
13798
+ return array;
13799
+ }
13800
+ /**
13801
+ * GraphQL data loader for Reference requests.
13802
+ * This is a special data loader for following Reference objects.
13803
+ * @param source The source/root. This should always be null for our top level readers.
13804
+ * @param _args The GraphQL search arguments.
13805
+ * @param ctx The GraphQL context.
13806
+ * @returns Promise to read the resoure(s) for the query.
13807
+ */
13808
+ async function resolveByReference(source, _args, ctx) {
13809
+ try {
13810
+ return await ctx.dataLoader.load(source);
13811
+ }
13812
+ catch (err) {
13813
+ throw new core.OperationOutcomeError(core.normalizeOperationOutcome(err), err);
13814
+ }
13815
+ }
13816
+ /**
13817
+ * GraphQL type resolver for resources.
13818
+ * When loading a resource via reference, GraphQL needs to know the type of the resource.
13819
+ * @param resource The loaded resource.
13820
+ * @returns The GraphQL type of the resource.
13821
+ */
13822
+ function resolveTypeByReference(resource) {
13823
+ const resourceType = resource?.resourceType;
13824
+ if (!resourceType) {
13825
+ return undefined;
13826
+ }
13827
+ return getGraphQLOutputType(resourceType).name;
13828
+ }
13829
+
13830
+ /**
13831
+ * Cache of "introspection" query results.
13832
+ * Common case is the standard schema query from GraphiQL and Insomnia.
13833
+ * The result is big and somewhat computationally expensive.
13834
+ */
13835
+ const introspectionResults = new core.LRUCache();
13836
+ /**
13837
+ * Cached GraphQL schema.
13838
+ * This should be initialized at server startup.
13839
+ */
13840
+ let rootSchema;
13841
+ /**
13842
+ * Handles FHIR GraphQL requests.
13843
+ *
13844
+ * See: https://www.hl7.org/fhir/graphql.html
13845
+ * @param req The request details.
13846
+ * @param repo The current user FHIR repository.
13847
+ * @param router The router for router options.
13848
+ * @returns The response.
13849
+ */
13850
+ async function graphqlHandler(req, repo, router) {
13851
+ const { query, operationName, variables } = req.body;
13852
+ if (!query) {
13853
+ return [core.badRequest('Must provide query.')];
13854
+ }
13855
+ let document;
13856
+ try {
13857
+ document = parse(query);
13858
+ }
13859
+ catch (err) {
13860
+ return [core.badRequest('GraphQL syntax error.')];
13861
+ }
13862
+ const schema = getRootSchema();
13863
+ const validationRules = [...specifiedRules, MaxDepthRule];
13864
+ const validationErrors = validate(schema, document, validationRules);
13865
+ if (validationErrors.length > 0) {
13866
+ return [invalidRequest(validationErrors)];
13867
+ }
13868
+ const introspection = isIntrospectionQuery(query);
13869
+ if (introspection && !router.options?.introspectionEnabled) {
13870
+ return [core.forbidden];
13871
+ }
13872
+ const dataLoader = new DataLoader((keys) => repo.readReferences(keys));
13873
+ let result = introspection && introspectionResults.get(query);
13874
+ if (!result) {
13875
+ result = await execute({
13876
+ schema,
13877
+ document,
13878
+ contextValue: { repo, dataLoader },
13879
+ operationName,
13880
+ variableValues: variables,
13881
+ });
13882
+ }
13883
+ return [core.allOk, result];
13884
+ }
13885
+ /**
13886
+ * Returns true if the query is a GraphQL introspection query.
13887
+ *
13888
+ * Introspection queries ask for the schema, which is expensive.
13889
+ *
13890
+ * See: https://graphql.org/learn/introspection/
13891
+ * @param query The GraphQL query.
13892
+ * @returns True if the query is an introspection query.
13893
+ */
13894
+ function isIntrospectionQuery(query) {
13895
+ return query.includes('query IntrospectionQuery') || query.includes('__schema') || query.includes('__type');
13896
+ }
13897
+ function getRootSchema() {
13898
+ if (!rootSchema) {
13899
+ rootSchema = buildRootSchema();
13900
+ }
13901
+ return rootSchema;
13902
+ }
13903
+ function buildRootSchema() {
13904
+ // First, create placeholder types
13905
+ // We need this first for circular dependencies
13906
+ for (const resourceType of core.getResourceTypes()) {
13907
+ outputTypeCache[resourceType] = buildGraphQLOutputType(resourceType);
13810
13908
  }
13811
- return args;
13909
+ // Next, fill in all of the type properties
13910
+ const fields = {};
13911
+ const mutationFields = {};
13912
+ for (const resourceType of core.getResourceTypes()) {
13913
+ const graphQLOutputType = getGraphQLOutputType(resourceType);
13914
+ // Get resource by ID
13915
+ fields[resourceType] = {
13916
+ type: graphQLOutputType,
13917
+ args: {
13918
+ id: {
13919
+ type: new GraphQLNonNull(GraphQLID),
13920
+ description: resourceType + ' ID',
13921
+ },
13922
+ },
13923
+ resolve: resolveById,
13924
+ };
13925
+ // Search resource by search parameters
13926
+ fields[resourceType + 'List'] = {
13927
+ type: new GraphQLList(graphQLOutputType),
13928
+ args: buildSearchArgs(resourceType),
13929
+ resolve: resolveBySearch,
13930
+ };
13931
+ // FHIR GraphQL Connection API
13932
+ fields[resourceType + 'Connection'] = {
13933
+ type: buildConnectionType(resourceType, graphQLOutputType),
13934
+ args: buildSearchArgs(resourceType),
13935
+ resolve: resolveByConnectionApi,
13936
+ };
13937
+ // Mutation API
13938
+ mutationFields[resourceType + 'Create'] = {
13939
+ type: graphQLOutputType,
13940
+ args: buildCreateArgs(resourceType),
13941
+ resolve: resolveByCreate,
13942
+ };
13943
+ mutationFields[resourceType + 'Update'] = {
13944
+ type: graphQLOutputType,
13945
+ args: buildUpdateArgs(resourceType),
13946
+ resolve: resolveByUpdate,
13947
+ };
13948
+ mutationFields[resourceType + 'Delete'] = {
13949
+ type: graphQLOutputType,
13950
+ args: {
13951
+ id: {
13952
+ type: new GraphQLNonNull(GraphQLID),
13953
+ description: resourceType + ' ID',
13954
+ },
13955
+ },
13956
+ resolve: resolveByDelete,
13957
+ };
13958
+ }
13959
+ return new GraphQLSchema({
13960
+ query: new GraphQLObjectType({
13961
+ name: 'QueryType',
13962
+ fields,
13963
+ }),
13964
+ mutation: new GraphQLObjectType({
13965
+ name: 'MutationType',
13966
+ fields: mutationFields,
13967
+ }),
13968
+ });
13812
13969
  }
13813
13970
  function buildCreateArgs(resourceType) {
13814
13971
  const args = {
13815
13972
  res: {
13816
- type: getGraphQLInputType(resourceType, 'Create'),
13973
+ type: new GraphQLNonNull(getGraphQLInputType(resourceType, 'Create')),
13817
13974
  description: resourceType + ' Create',
13818
13975
  },
13819
13976
  };
@@ -13826,19 +13983,12 @@ spurious results.`);
13826
13983
  description: resourceType + ' ID',
13827
13984
  },
13828
13985
  res: {
13829
- type: getGraphQLInputType(resourceType, 'Update'),
13986
+ type: new GraphQLNonNull(getGraphQLInputType(resourceType, 'Update')),
13830
13987
  description: resourceType + ' Update',
13831
13988
  },
13832
13989
  };
13833
13990
  return args;
13834
13991
  }
13835
- function getOutputPropertyType(elementDefinition, typeName) {
13836
- const graphqlType = getGraphQLOutputType(typeName);
13837
- if (elementDefinition.max === '*') {
13838
- return new GraphQLList(graphqlType);
13839
- }
13840
- return graphqlType;
13841
- }
13842
13992
  function buildConnectionType(resourceType, resourceGraphQLType) {
13843
13993
  return new GraphQLObjectType({
13844
13994
  name: resourceType + 'Connection',
@@ -13863,23 +14013,6 @@ spurious results.`);
13863
14013
  },
13864
14014
  });
13865
14015
  }
13866
- /**
13867
- * GraphQL data loader for search requests.
13868
- * The field name should always end with "List" (i.e., "Patient" search uses "PatientList").
13869
- * The search args should be FHIR search parameters.
13870
- * @param source The source/root. This should always be null for our top level readers.
13871
- * @param args The GraphQL search arguments.
13872
- * @param ctx The GraphQL context.
13873
- * @param info The GraphQL resolve info. This includes the schema, and additional field details.
13874
- * @returns Promise to read the resoures for the query.
13875
- */
13876
- async function resolveBySearch(source, args, ctx, info) {
13877
- const fieldName = info.fieldName;
13878
- const resourceType = fieldName.substring(0, fieldName.length - 'List'.length);
13879
- const searchRequest = parseSearchArgs(resourceType, source, args);
13880
- const bundle = await ctx.repo.search(searchRequest);
13881
- return bundle.entry?.map((e) => e.resource);
13882
- }
13883
14016
  /**
13884
14017
  * GraphQL data loader for search requests.
13885
14018
  * The field name should always end with "List" (i.e., "Patient" search uses "PatientList").
@@ -13927,66 +14060,6 @@ spurious results.`);
13927
14060
  throw new core.OperationOutcomeError(core.normalizeOperationOutcome(err), err);
13928
14061
  }
13929
14062
  }
13930
- /**
13931
- * GraphQL data loader for Reference requests.
13932
- * This is a special data loader for following Reference objects.
13933
- * @param source The source/root. This should always be null for our top level readers.
13934
- * @param _args The GraphQL search arguments.
13935
- * @param ctx The GraphQL context.
13936
- * @returns Promise to read the resoure(s) for the query.
13937
- */
13938
- async function resolveByReference(source, _args, ctx) {
13939
- try {
13940
- return await ctx.dataLoader.load(source);
13941
- }
13942
- catch (err) {
13943
- throw new core.OperationOutcomeError(core.normalizeOperationOutcome(err), err);
13944
- }
13945
- }
13946
- /**
13947
- * GraphQL type resolver for resources.
13948
- * When loading a resource via reference, GraphQL needs to know the type of the resource.
13949
- * @param resource The loaded resource.
13950
- * @returns The GraphQL type of the resource.
13951
- */
13952
- function resolveTypeByReference(resource) {
13953
- const resourceType = resource?.resourceType;
13954
- if (!resourceType) {
13955
- return undefined;
13956
- }
13957
- return getGraphQLOutputType(resourceType).name;
13958
- }
13959
- /**
13960
- * GraphQL resolver for fields.
13961
- * In the common case, this is just a matter of returning the field value from the source object.
13962
- * If the field is a list and the user specifies list arguments, then we can apply those arguments here.
13963
- * @param source The source. This is the object that contains the field.
13964
- * @param args The GraphQL search arguments.
13965
- * @param _ctx The GraphQL context.
13966
- * @param info The GraphQL resolve info. This includes the field name.
13967
- * @returns Promise to read the resoure for the query.
13968
- */
13969
- async function resolveField(source, args, _ctx, info) {
13970
- const fieldValue = source?.[info.fieldName];
13971
- if (!args || !fieldValue) {
13972
- return fieldValue;
13973
- }
13974
- const { _offset, _count, fhirpath, ...rest } = args;
13975
- let array = fieldValue;
13976
- for (const [key, value] of Object.entries(rest)) {
13977
- array = array.filter((item) => item[key] === value);
13978
- }
13979
- if (fhirpath) {
13980
- array = array.filter((item) => core.toJsBoolean(core.evalFhirPathTyped(fhirpath, [core.toTypedValue(item)])));
13981
- }
13982
- if (_offset) {
13983
- array = array.slice(_offset);
13984
- }
13985
- if (_count) {
13986
- array = array.slice(0, _count);
13987
- }
13988
- return array;
13989
- }
13990
14063
  /**
13991
14064
  * GraphQL resolver function for create requests.
13992
14065
  * The field name should end with "Create" (i.e., "PatientCreate" for updating a Patient).
@@ -14049,37 +14122,6 @@ spurious results.`);
14049
14122
  const resourceType = fieldName.substring(0, fieldName.length - 'Delete'.length);
14050
14123
  await ctx.repo.deleteResource(resourceType, args.id);
14051
14124
  }
14052
- function parseSearchArgs(resourceType, source, args) {
14053
- let referenceFilter = undefined;
14054
- if (source) {
14055
- // _reference is a required field for reverse lookup searches
14056
- // The GraphQL parser will validate that it is there.
14057
- const reference = args['_reference'];
14058
- delete args['_reference'];
14059
- referenceFilter = {
14060
- code: reference,
14061
- operator: core.Operator.EQUALS,
14062
- value: core.getReferenceString(source),
14063
- };
14064
- }
14065
- // Reverse the transform of dashes to underscores, back to dashes
14066
- args = Object.fromEntries(Object.entries(args).map(([key, value]) => [graphQLFieldToFhirParam(key), value]));
14067
- // Parse the search request
14068
- const searchRequest = core.parseSearchRequest(resourceType, args);
14069
- // If a reverse lookup filter was specified,
14070
- // add it to the search request.
14071
- if (referenceFilter) {
14072
- const existingFilters = searchRequest.filters || [];
14073
- searchRequest.filters = [referenceFilter, ...existingFilters];
14074
- }
14075
- return searchRequest;
14076
- }
14077
- function fhirParamToGraphQLField(code) {
14078
- return code.replaceAll('-', '_');
14079
- }
14080
- function graphQLFieldToFhirParam(code) {
14081
- return code.startsWith('_') ? code : code.replaceAll('_', '-');
14082
- }
14083
14125
  /**
14084
14126
  * Custom GraphQL rule that enforces max depth constraint.
14085
14127
  * @param context The validation context.
@@ -14105,42 +14147,6 @@ spurious results.`);
14105
14147
  }
14106
14148
  },
14107
14149
  });
14108
- /**
14109
- * Returns the depth of the GraphQL node in a query.
14110
- * We use "selections" as the representation of depth.
14111
- * As a rough approximation, it's the number of indentations in a well formatted query.
14112
- * @param path The GraphQL node path.
14113
- * @returns The "depth" of the node.
14114
- */
14115
- function getDepth(path) {
14116
- return path.filter((p) => p === 'selections').length;
14117
- }
14118
- /**
14119
- * Returns true if the field is requested in the GraphQL query.
14120
- * @param info The GraphQL resolve info. This includes the field name.
14121
- * @param fieldName The field name to check.
14122
- * @returns True if the field is requested in the GraphQL query.
14123
- */
14124
- function isFieldRequested(info, fieldName) {
14125
- return info.fieldNodes.some((fieldNode) => fieldNode.selectionSet?.selections.some((selection) => {
14126
- return selection.kind === 'Field' && selection.name.value === fieldName;
14127
- }));
14128
- }
14129
- /**
14130
- * Returns an OperationOutcome for GraphQL errors.
14131
- * @param errors Array of GraphQL errors.
14132
- * @returns OperationOutcome with the GraphQL errors as OperationOutcome issues.
14133
- */
14134
- function invalidRequest(errors) {
14135
- return {
14136
- resourceType: 'OperationOutcome',
14137
- issue: errors.map((error) => ({
14138
- severity: 'error',
14139
- code: 'invalid',
14140
- details: { text: error.message },
14141
- })),
14142
- };
14143
- }
14144
14150
 
14145
14151
  class Router {
14146
14152
  constructor() {
@@ -14350,10 +14356,10 @@ spurious results.`);
14350
14356
  if (!result.meta) {
14351
14357
  result.meta = {};
14352
14358
  }
14353
- if (!result.meta?.versionId) {
14359
+ if (!result.meta.versionId) {
14354
14360
  result.meta.versionId = generateId();
14355
14361
  }
14356
- if (!result.meta?.lastUpdated) {
14362
+ if (!result.meta.lastUpdated) {
14357
14363
  result.meta.lastUpdated = new Date().toISOString();
14358
14364
  }
14359
14365
  const { resourceType, id } = result;
@@ -14475,7 +14481,7 @@ spurious results.`);
14475
14481
  }
14476
14482
  }
14477
14483
  const sortComparator = (a, b, sortRule) => {
14478
- const searchParam = core.globalSchema.types[a.resourceType]?.searchParams?.[sortRule.code];
14484
+ const searchParam = core.globalSchema.types[a.resourceType].searchParams?.[sortRule.code];
14479
14485
  const expression = searchParam?.expression;
14480
14486
  if (!expression) {
14481
14487
  return 0;