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