@medplum/core 2.0.18 → 2.0.19

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.
@@ -1098,6 +1098,18 @@
1098
1098
  }
1099
1099
  return Math.round(a * Math.pow(10, precision));
1100
1100
  }
1101
+ /**
1102
+ * Finds the first resource in the input array that matches the specified code and system.
1103
+ * @param resources - The array of resources to search.
1104
+ * @param code - The code to search for.
1105
+ * @param system - The system to search for.
1106
+ * @returns The first resource in the input array that matches the specified code and system, or undefined if no such resource is found.
1107
+ */
1108
+ function findResourceByCode(resources, code, system) {
1109
+ return resources.find((r) => typeof code === 'string'
1110
+ ? getCodeBySystem(r.code || {}, system) === code
1111
+ : getCodeBySystem(r.code || {}, system) === getCodeBySystem(code, system));
1112
+ }
1101
1113
 
1102
1114
  /**
1103
1115
  * Returns a cryptographically secure random string.
@@ -6404,7 +6416,8 @@
6404
6416
  const globalSchema = baseSchema;
6405
6417
 
6406
6418
  // PKCE auth based on:
6407
- const MEDPLUM_VERSION = "2.0.18-e7b9bd9c" ;
6419
+ // https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/
6420
+ const MEDPLUM_VERSION = "2.0.19-40e6e27d" ;
6408
6421
  const DEFAULT_BASE_URL = 'https://api.medplum.com/';
6409
6422
  const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
6410
6423
  const DEFAULT_CACHE_TIME = 60000; // 60 seconds
@@ -7906,6 +7919,26 @@
7906
7919
  const response = await this.fetch(url.toString(), options);
7907
7920
  return response.blob();
7908
7921
  }
7922
+ /**
7923
+ * Upload media to the server and create a Media instance for the uploaded content.
7924
+ * @param contents The contents of the media file, as a string, Uint8Array, File, or Blob.
7925
+ * @param contentType The media type of the content
7926
+ * @param filename The name of the file to be uploaded, or undefined if not applicable
7927
+ * @param additionalFields Additional fields for Media
7928
+ * @returns Promise that resolves to the created Media
7929
+ */
7930
+ async uploadMedia(contents, contentType, filename, additionalFields) {
7931
+ const binary = await this.createBinary(contents, filename, contentType);
7932
+ return this.createResource({
7933
+ ...additionalFields,
7934
+ resourceType: 'Media',
7935
+ content: {
7936
+ contentType: contentType,
7937
+ url: 'Binary/' + binary.id,
7938
+ title: filename,
7939
+ },
7940
+ });
7941
+ }
7909
7942
  //
7910
7943
  // Private helpers
7911
7944
  //
@@ -7981,7 +8014,10 @@
7981
8014
  return undefined;
7982
8015
  }
7983
8016
  if (response.status === 404) {
7984
- throw new OperationOutcomeError(normalizeOperationOutcome(notFound));
8017
+ const contentType = response.headers.get('content-type');
8018
+ if (!contentType?.includes('application/fhir+json')) {
8019
+ throw new OperationOutcomeError(notFound);
8020
+ }
7985
8021
  }
7986
8022
  let obj = undefined;
7987
8023
  try {
@@ -11394,162 +11430,577 @@
11394
11430
  return new StructureMapParser(parser).parse();
11395
11431
  }
11396
11432
 
11397
- const MAPPING_LANGUAGE_OPERATORS = [...FHIRPATH_OPERATORS, 'eq', 'ne', 'co'];
11398
- function tokenize(str) {
11399
- return new Tokenizer(str, FHIRPATH_KEYWORDS, MAPPING_LANGUAGE_OPERATORS, {
11400
- dateTimeLiterals: true,
11401
- symbolRegex: /[^\s\])]/,
11402
- }).tokenize();
11403
- }
11404
-
11405
- // See: https://hl7.org/fhir/search_filter.html
11433
+ const DEFAULT_SEARCH_COUNT = 20;
11406
11434
  /**
11407
- * The FhirFilterComparison class represents a comparison expression.
11435
+ * Search operators.
11436
+ * These operators represent "modifiers" and "prefixes" in FHIR search.
11437
+ * See: https://www.hl7.org/fhir/search.html
11408
11438
  */
11409
- class FhirFilterComparison {
11410
- constructor(path, operator, value) {
11411
- this.path = path;
11412
- this.operator = operator;
11413
- this.value = value;
11414
- }
11415
- }
11439
+ exports.Operator = void 0;
11440
+ (function (Operator) {
11441
+ Operator["EQUALS"] = "eq";
11442
+ Operator["NOT_EQUALS"] = "ne";
11443
+ // Numbers
11444
+ Operator["GREATER_THAN"] = "gt";
11445
+ Operator["LESS_THAN"] = "lt";
11446
+ Operator["GREATER_THAN_OR_EQUALS"] = "ge";
11447
+ Operator["LESS_THAN_OR_EQUALS"] = "le";
11448
+ // Dates
11449
+ Operator["STARTS_AFTER"] = "sa";
11450
+ Operator["ENDS_BEFORE"] = "eb";
11451
+ Operator["APPROXIMATELY"] = "ap";
11452
+ // String
11453
+ Operator["CONTAINS"] = "contains";
11454
+ Operator["EXACT"] = "exact";
11455
+ // Token
11456
+ Operator["TEXT"] = "text";
11457
+ Operator["NOT"] = "not";
11458
+ Operator["ABOVE"] = "above";
11459
+ Operator["BELOW"] = "below";
11460
+ Operator["IN"] = "in";
11461
+ Operator["NOT_IN"] = "not-in";
11462
+ Operator["OF_TYPE"] = "of-type";
11463
+ // All
11464
+ Operator["MISSING"] = "missing";
11465
+ // Reference
11466
+ Operator["IDENTIFIER"] = "identifier";
11467
+ // _include and _revinclude
11468
+ Operator["ITERATE"] = "iterate";
11469
+ })(exports.Operator || (exports.Operator = {}));
11416
11470
  /**
11417
- * The FhirFilterNegation class represents a negation expression.
11418
- * It contains a single child expression.
11471
+ * Parameter names may specify a modifier as a suffix.
11472
+ * The modifiers are separated from the parameter name by a colon.
11473
+ * See: https://www.hl7.org/fhir/search.html#modifiers
11419
11474
  */
11420
- class FhirFilterNegation {
11421
- constructor(child) {
11422
- this.child = child;
11423
- }
11424
- }
11475
+ const MODIFIER_OPERATORS = {
11476
+ contains: exports.Operator.CONTAINS,
11477
+ exact: exports.Operator.EXACT,
11478
+ above: exports.Operator.ABOVE,
11479
+ below: exports.Operator.BELOW,
11480
+ text: exports.Operator.TEXT,
11481
+ not: exports.Operator.NOT,
11482
+ in: exports.Operator.IN,
11483
+ 'not-in': exports.Operator.NOT_IN,
11484
+ 'of-type': exports.Operator.OF_TYPE,
11485
+ missing: exports.Operator.MISSING,
11486
+ identifier: exports.Operator.IDENTIFIER,
11487
+ iterate: exports.Operator.ITERATE,
11488
+ };
11425
11489
  /**
11426
- * The FhirFilterConnective class represents a connective expression.
11427
- * It contains a list of child expressions.
11490
+ * For the ordered parameter types of number, date, and quantity,
11491
+ * a prefix to the parameter value may be used to control the nature
11492
+ * of the matching.
11493
+ * See: https://www.hl7.org/fhir/search.html#prefix
11428
11494
  */
11429
- class FhirFilterConnective {
11430
- constructor(keyword, left, right) {
11431
- this.keyword = keyword;
11432
- this.left = left;
11433
- this.right = right;
11434
- }
11435
- }
11436
-
11437
- class FilterParameterParser {
11438
- constructor(parser) {
11439
- this.parser = parser;
11440
- }
11441
- parse() {
11442
- let result;
11443
- if (this.parser.peek()?.value === '(') {
11444
- this.parser.consume('(');
11445
- result = this.parse();
11446
- this.parser.consume(')');
11447
- }
11448
- else if (this.parser.peek()?.value === 'not') {
11449
- this.parser.consume('Symbol', 'not');
11450
- this.parser.consume('(');
11451
- result = new FhirFilterNegation(this.parse());
11452
- this.parser.consume(')');
11495
+ const PREFIX_OPERATORS = {
11496
+ eq: exports.Operator.EQUALS,
11497
+ ne: exports.Operator.NOT_EQUALS,
11498
+ lt: exports.Operator.LESS_THAN,
11499
+ le: exports.Operator.LESS_THAN_OR_EQUALS,
11500
+ gt: exports.Operator.GREATER_THAN,
11501
+ ge: exports.Operator.GREATER_THAN_OR_EQUALS,
11502
+ sa: exports.Operator.STARTS_AFTER,
11503
+ eb: exports.Operator.ENDS_BEFORE,
11504
+ ap: exports.Operator.APPROXIMATELY,
11505
+ };
11506
+ /**
11507
+ * Parses a search URL into a search request.
11508
+ * @param resourceType The FHIR resource type.
11509
+ * @param query The collection of query string parameters.
11510
+ * @returns A parsed SearchRequest.
11511
+ */
11512
+ function parseSearchRequest(resourceType, query) {
11513
+ const queryArray = [];
11514
+ for (const [key, value] of Object.entries(query)) {
11515
+ if (Array.isArray(value)) {
11516
+ for (let i = 0; i < value.length; i++) {
11517
+ queryArray.push([key, value[i]]);
11518
+ }
11453
11519
  }
11454
11520
  else {
11455
- result = new FhirFilterComparison(this.parser.consume('Symbol').value, this.parser.consume('Symbol').value, this.parser.consume().value);
11456
- }
11457
- const next = this.parser.peek()?.value;
11458
- if (next === 'and' || next === 'or') {
11459
- this.parser.consume('Symbol', next);
11460
- return new FhirFilterConnective(next, result, this.parse());
11521
+ queryArray.push([key, value || '']);
11461
11522
  }
11462
- return result;
11463
11523
  }
11524
+ return parseSearchImpl(resourceType, queryArray);
11464
11525
  }
11465
- const fhirPathParserBuilder = initFhirPathParserBuilder();
11466
11526
  /**
11467
- * Parses a FHIR _filter parameter expression into an AST.
11468
- * @param input The FHIR _filter parameter expression.
11469
- * @returns The AST representing the filters.
11527
+ * Parses a search URL into a search request.
11528
+ * @param url The search URL.
11529
+ * @returns A parsed SearchRequest.
11470
11530
  */
11471
- function parseFilterParameter(input) {
11472
- const parser = fhirPathParserBuilder.construct(tokenize(input));
11473
- parser.removeComments();
11474
- return new FilterParameterParser(parser).parse();
11531
+ function parseSearchUrl(url) {
11532
+ const resourceType = url.pathname.split('/').filter(Boolean).pop();
11533
+ return parseSearchImpl(resourceType, url.searchParams.entries());
11475
11534
  }
11476
-
11477
11535
  /**
11478
- * The Hl7Context class represents the parsing context for an HL7 message.
11479
- *
11480
- * MSH-1:
11481
- * https://hl7-definition.caristix.com/v2/HL7v2.6/Fields/MSH.1
11482
- *
11483
- * MSH-2:
11484
- * https://hl7-definition.caristix.com/v2/HL7v2.6/Fields/MSH.2
11485
- *
11486
- * See this tutorial on MSH, and why it's a bad idea to use anything other than the default values:
11487
- * https://www.hl7soup.com/HL7TutorialMSH.html
11536
+ * Parses a URL string into a SearchRequest.
11537
+ * @param url The URL to parse.
11538
+ * @returns Parsed search definition.
11488
11539
  */
11489
- class Hl7Context {
11490
- constructor(segmentSeparator = '\r', fieldSeparator = '|', componentSeparator = '^', repetitionSeparator = '~', escapeCharacter = '\\', subcomponentSeparator = '&') {
11491
- this.segmentSeparator = segmentSeparator;
11492
- this.fieldSeparator = fieldSeparator;
11493
- this.componentSeparator = componentSeparator;
11494
- this.repetitionSeparator = repetitionSeparator;
11495
- this.escapeCharacter = escapeCharacter;
11496
- this.subcomponentSeparator = subcomponentSeparator;
11497
- }
11498
- /**
11499
- * Returns the MSH-2 field value based on the configured separators.
11500
- * @returns The HL7 MSH-2 field value.
11501
- */
11502
- getMsh2() {
11503
- return (this.fieldSeparator +
11504
- this.componentSeparator +
11505
- this.repetitionSeparator +
11506
- this.escapeCharacter +
11507
- this.subcomponentSeparator);
11508
- }
11540
+ function parseSearchDefinition(url) {
11541
+ return parseSearchUrl(new URL(url, 'https://example.com/'));
11509
11542
  }
11510
- /**
11511
- * The Hl7Message class represents one HL7 message.
11512
- * A message is a collection of segments.
11513
- */
11514
- class Hl7Message {
11515
- /**
11516
- * Creates a new HL7 message.
11517
- * @param segments The HL7 segments.
11518
- * @param context Optional HL7 parsing context.
11519
- */
11520
- constructor(segments, context = new Hl7Context()) {
11521
- this.context = context;
11522
- this.segments = segments;
11523
- }
11524
- /**
11525
- * Returns an HL7 segment by index or by name.
11526
- * @param index The HL7 segment index or name.
11527
- * @returns The HL7 segment if found; otherwise, undefined.
11528
- */
11529
- get(index) {
11530
- if (typeof index === 'number') {
11531
- return this.segments[index];
11532
- }
11533
- return this.segments.find((s) => s.name === index);
11543
+ function parseSearchImpl(resourceType, query) {
11544
+ const searchRequest = {
11545
+ resourceType,
11546
+ };
11547
+ for (const [key, value] of query) {
11548
+ parseKeyValue(searchRequest, key, value);
11534
11549
  }
11535
- /**
11536
- * Returns all HL7 segments of a given name.
11537
- * @param name The HL7 segment name.
11538
- * @returns An array of HL7 segments with the specified name.
11539
- */
11540
- getAll(name) {
11541
- return this.segments.filter((s) => s.name === name);
11550
+ return searchRequest;
11551
+ }
11552
+ function parseKeyValue(searchRequest, key, value) {
11553
+ let code;
11554
+ let modifier;
11555
+ const colonIndex = key.indexOf(':');
11556
+ if (colonIndex >= 0) {
11557
+ code = key.substring(0, colonIndex);
11558
+ modifier = key.substring(colonIndex + 1);
11542
11559
  }
11543
- /**
11544
- * Returns the HL7 message as a string.
11545
- * @returns The HL7 message as a string.
11546
- */
11547
- toString() {
11548
- return this.segments.map((s) => s.toString()).join(this.context.segmentSeparator);
11560
+ else {
11561
+ code = key;
11562
+ modifier = '';
11549
11563
  }
11550
- /**
11551
- * Returns an HL7 "ACK" (acknowledgement) message for this message.
11552
- * @returns The HL7 "ACK" message.
11564
+ switch (code) {
11565
+ case '_sort':
11566
+ parseSortRule(searchRequest, value);
11567
+ break;
11568
+ case '_count':
11569
+ searchRequest.count = parseInt(value);
11570
+ break;
11571
+ case '_offset':
11572
+ searchRequest.offset = parseInt(value);
11573
+ break;
11574
+ case '_total':
11575
+ searchRequest.total = value;
11576
+ break;
11577
+ case '_summary':
11578
+ searchRequest.total = 'estimate';
11579
+ searchRequest.count = 0;
11580
+ break;
11581
+ case '_include': {
11582
+ const target = parseIncludeTarget(value);
11583
+ if (modifier === 'iterate') {
11584
+ target.modifier = exports.Operator.ITERATE;
11585
+ }
11586
+ if (searchRequest.include) {
11587
+ searchRequest.include.push(target);
11588
+ }
11589
+ else {
11590
+ searchRequest.include = [target];
11591
+ }
11592
+ break;
11593
+ }
11594
+ case '_revinclude': {
11595
+ const target = parseIncludeTarget(value);
11596
+ if (modifier === 'iterate') {
11597
+ target.modifier = exports.Operator.ITERATE;
11598
+ }
11599
+ if (searchRequest.revInclude) {
11600
+ searchRequest.revInclude.push(target);
11601
+ }
11602
+ else {
11603
+ searchRequest.revInclude = [target];
11604
+ }
11605
+ break;
11606
+ }
11607
+ case '_fields':
11608
+ searchRequest.fields = value.split(',');
11609
+ break;
11610
+ default: {
11611
+ const param = globalSchema.types[searchRequest.resourceType]?.searchParams?.[code];
11612
+ if (param) {
11613
+ parseParameter(searchRequest, param, modifier, value);
11614
+ }
11615
+ else {
11616
+ parseUnknownParameter(searchRequest, code, modifier, value);
11617
+ }
11618
+ }
11619
+ }
11620
+ }
11621
+ function parseSortRule(searchRequest, value) {
11622
+ for (const field of value.split(',')) {
11623
+ let code;
11624
+ let descending = false;
11625
+ if (field.startsWith('-')) {
11626
+ code = field.substring(1);
11627
+ descending = true;
11628
+ }
11629
+ else {
11630
+ code = field;
11631
+ }
11632
+ if (!searchRequest.sortRules) {
11633
+ searchRequest.sortRules = [];
11634
+ }
11635
+ searchRequest.sortRules.push({ code, descending });
11636
+ }
11637
+ }
11638
+ function parseParameter(searchRequest, searchParam, modifier, value) {
11639
+ if (modifier === 'missing') {
11640
+ addFilter(searchRequest, {
11641
+ code: searchParam.code,
11642
+ operator: exports.Operator.MISSING,
11643
+ value,
11644
+ });
11645
+ return;
11646
+ }
11647
+ switch (searchParam.type) {
11648
+ case 'number':
11649
+ case 'date':
11650
+ parsePrefixType(searchRequest, searchParam, value);
11651
+ break;
11652
+ case 'reference':
11653
+ case 'string':
11654
+ case 'token':
11655
+ case 'uri':
11656
+ parseModifierType(searchRequest, searchParam, modifier, value);
11657
+ break;
11658
+ case 'quantity':
11659
+ parseQuantity(searchRequest, searchParam, value);
11660
+ break;
11661
+ }
11662
+ }
11663
+ function parsePrefixType(searchRequest, param, input) {
11664
+ const { operator, value } = parsePrefix(input);
11665
+ addFilter(searchRequest, {
11666
+ code: param.code,
11667
+ operator,
11668
+ value,
11669
+ });
11670
+ }
11671
+ function parseModifierType(searchRequest, param, modifier, value) {
11672
+ addFilter(searchRequest, {
11673
+ code: param.code,
11674
+ operator: parseModifier(modifier),
11675
+ value,
11676
+ });
11677
+ }
11678
+ function parseQuantity(searchRequest, param, input) {
11679
+ const [prefixNumber, unitSystem, unitCode] = input.split('|');
11680
+ const { operator, value } = parsePrefix(prefixNumber);
11681
+ addFilter(searchRequest, {
11682
+ code: param.code,
11683
+ operator,
11684
+ value,
11685
+ unitSystem,
11686
+ unitCode,
11687
+ });
11688
+ }
11689
+ function parseUnknownParameter(searchRequest, code, modifier, value) {
11690
+ let operator = exports.Operator.EQUALS;
11691
+ if (modifier) {
11692
+ operator = modifier;
11693
+ }
11694
+ else if (value.length >= 2) {
11695
+ const prefix = value.substring(0, 2);
11696
+ if (prefix in PREFIX_OPERATORS) {
11697
+ if (value.length === 2 || value.at(2)?.match(/\d/)) {
11698
+ operator = prefix;
11699
+ value = value.substring(prefix.length);
11700
+ }
11701
+ }
11702
+ }
11703
+ addFilter(searchRequest, {
11704
+ code,
11705
+ operator,
11706
+ value,
11707
+ });
11708
+ }
11709
+ function parsePrefix(input) {
11710
+ const prefix = input.substring(0, 2);
11711
+ const prefixOperator = PREFIX_OPERATORS[prefix];
11712
+ if (prefixOperator) {
11713
+ return { operator: prefixOperator, value: input.substring(2) };
11714
+ }
11715
+ return { operator: exports.Operator.EQUALS, value: input };
11716
+ }
11717
+ function parseModifier(modifier) {
11718
+ return MODIFIER_OPERATORS[modifier] || exports.Operator.EQUALS;
11719
+ }
11720
+ function parseIncludeTarget(input) {
11721
+ const parts = input.split(':');
11722
+ parts.forEach((p) => {
11723
+ if (p === '*') {
11724
+ throw new OperationOutcomeError(badRequest(`'*' is not supported as a value for search inclusion parameters`));
11725
+ }
11726
+ });
11727
+ if (parts.length === 1) {
11728
+ // Full wildcard, not currently supported
11729
+ throw new OperationOutcomeError(badRequest(`Invalid include value '${input}': must be of the form ResourceType:search-parameter`));
11730
+ }
11731
+ else if (parts.length === 2) {
11732
+ return {
11733
+ resourceType: parts[0],
11734
+ searchParam: parts[1],
11735
+ };
11736
+ }
11737
+ else if (parts.length === 3) {
11738
+ return {
11739
+ resourceType: parts[0],
11740
+ searchParam: parts[1],
11741
+ targetType: parts[2],
11742
+ };
11743
+ }
11744
+ else {
11745
+ throw new OperationOutcomeError(badRequest(`Invalid include value '${input}'`));
11746
+ }
11747
+ }
11748
+ function addFilter(searchRequest, filter) {
11749
+ if (searchRequest.filters) {
11750
+ searchRequest.filters.push(filter);
11751
+ }
11752
+ else {
11753
+ searchRequest.filters = [filter];
11754
+ }
11755
+ }
11756
+ /**
11757
+ * Formats a search definition object into a query string.
11758
+ * Note: The return value does not include the resource type.
11759
+ * @param {!SearchRequest} definition The search definition.
11760
+ * @returns Formatted URL.
11761
+ */
11762
+ function formatSearchQuery(definition) {
11763
+ const params = [];
11764
+ if (definition.fields) {
11765
+ params.push('_fields=' + definition.fields.join(','));
11766
+ }
11767
+ if (definition.filters) {
11768
+ definition.filters.forEach((filter) => params.push(formatFilter(filter)));
11769
+ }
11770
+ if (definition.sortRules && definition.sortRules.length > 0) {
11771
+ params.push(formatSortRules(definition.sortRules));
11772
+ }
11773
+ if (definition.offset !== undefined) {
11774
+ params.push('_offset=' + definition.offset);
11775
+ }
11776
+ if (definition.count !== undefined) {
11777
+ params.push('_count=' + definition.count);
11778
+ }
11779
+ if (definition.total !== undefined) {
11780
+ params.push('_total=' + definition.total);
11781
+ }
11782
+ if (params.length === 0) {
11783
+ return '';
11784
+ }
11785
+ params.sort();
11786
+ return '?' + params.join('&');
11787
+ }
11788
+ function formatFilter(filter) {
11789
+ const modifier = filter.operator in MODIFIER_OPERATORS ? ':' + filter.operator : '';
11790
+ const prefix = filter.operator !== exports.Operator.EQUALS && filter.operator in PREFIX_OPERATORS ? filter.operator : '';
11791
+ return `${filter.code}${modifier}=${prefix}${encodeURIComponent(filter.value)}`;
11792
+ }
11793
+ function formatSortRules(sortRules) {
11794
+ return '_sort=' + sortRules.map((sr) => (sr.descending ? '-' + sr.code : sr.code)).join(',');
11795
+ }
11796
+
11797
+ const MAPPING_LANGUAGE_OPERATORS = [...FHIRPATH_OPERATORS, 'eq', 'ne', 'co'];
11798
+ function tokenize(str) {
11799
+ return new Tokenizer(str, FHIRPATH_KEYWORDS, MAPPING_LANGUAGE_OPERATORS, {
11800
+ dateTimeLiterals: true,
11801
+ symbolRegex: /[^\s\])]/,
11802
+ }).tokenize();
11803
+ }
11804
+
11805
+ // See: https://hl7.org/fhir/search_filter.html
11806
+ /**
11807
+ * The FhirFilterComparison class represents a comparison expression.
11808
+ */
11809
+ class FhirFilterComparison {
11810
+ constructor(path, operator, value) {
11811
+ this.path = path;
11812
+ this.operator = operator;
11813
+ this.value = value;
11814
+ }
11815
+ }
11816
+ /**
11817
+ * The FhirFilterNegation class represents a negation expression.
11818
+ * It contains a single child expression.
11819
+ */
11820
+ class FhirFilterNegation {
11821
+ constructor(child) {
11822
+ this.child = child;
11823
+ }
11824
+ }
11825
+ /**
11826
+ * The FhirFilterConnective class represents a connective expression.
11827
+ * It contains a list of child expressions.
11828
+ */
11829
+ class FhirFilterConnective {
11830
+ constructor(keyword, left, right) {
11831
+ this.keyword = keyword;
11832
+ this.left = left;
11833
+ this.right = right;
11834
+ }
11835
+ }
11836
+
11837
+ /**
11838
+ * The operatorMap maps FHIR _filter operators to Medplum search operators.
11839
+ * See _filter operators: https://www.hl7.org/fhir/search_filter.html#ops
11840
+ */
11841
+ const operatorMap = {
11842
+ // eq - an item in the set has an equal value
11843
+ eq: exports.Operator.EQUALS,
11844
+ // ne - An item in the set has an unequal value
11845
+ ne: exports.Operator.NOT_EQUALS,
11846
+ // co - An item in the set contains this value
11847
+ co: exports.Operator.CONTAINS,
11848
+ // sw - An item in the set starts with this value
11849
+ sw: undefined,
11850
+ // ew - An item in the set ends with this value
11851
+ ew: undefined,
11852
+ // gt / lt / ge / le - A value in the set is (greater than, less than, greater or equal, less or equal) the given value
11853
+ gt: exports.Operator.GREATER_THAN,
11854
+ lt: exports.Operator.LESS_THAN,
11855
+ ge: exports.Operator.GREATER_THAN_OR_EQUALS,
11856
+ le: exports.Operator.LESS_THAN_OR_EQUALS,
11857
+ // ap - A value in the set is approximately the same as this value.
11858
+ // Note that the recommended value for the approximation is 10% of the stated value (or for a date, 10% of the gap between now and the date), but systems may choose other values where appropriate
11859
+ ap: exports.Operator.APPROXIMATELY,
11860
+ // sa - The value starts after the specified value
11861
+ sa: exports.Operator.STARTS_AFTER,
11862
+ // eb - The value ends before the specified value
11863
+ eb: exports.Operator.ENDS_BEFORE,
11864
+ // pr - The set is empty or not (value is false or true)
11865
+ pr: exports.Operator.MISSING,
11866
+ // po - True if a (implied) date period in the set overlaps with the implied period in the value
11867
+ po: undefined,
11868
+ // ss - True if the value subsumes a concept in the set
11869
+ ss: undefined,
11870
+ // sb - True if the value is subsumed by a concept in the set
11871
+ sb: undefined,
11872
+ // in - True if one of the concepts is in the nominated value set by URI, either a relative, literal or logical vs
11873
+ in: exports.Operator.IN,
11874
+ // ni - True if none of the concepts are in the nominated value set by URI, either a relative, literal or logical vs
11875
+ ni: exports.Operator.NOT_IN,
11876
+ // re - True if one of the references in set points to the given URL
11877
+ re: undefined,
11878
+ // identifier - True if the identifier is in the identifier set (Medplum extension)
11879
+ identifier: exports.Operator.IDENTIFIER,
11880
+ };
11881
+ function getOperator(value) {
11882
+ const operator = operatorMap[value];
11883
+ if (!operator) {
11884
+ throw new OperationOutcomeError(badRequest('Invalid operator: ' + value));
11885
+ }
11886
+ return operator;
11887
+ }
11888
+ class FilterParameterParser {
11889
+ constructor(parser) {
11890
+ this.parser = parser;
11891
+ }
11892
+ parse() {
11893
+ let result;
11894
+ if (this.parser.peek()?.value === '(') {
11895
+ this.parser.consume('(');
11896
+ result = this.parse();
11897
+ this.parser.consume(')');
11898
+ }
11899
+ else if (this.parser.peek()?.value === 'not') {
11900
+ this.parser.consume('Symbol', 'not');
11901
+ this.parser.consume('(');
11902
+ result = new FhirFilterNegation(this.parse());
11903
+ this.parser.consume(')');
11904
+ }
11905
+ else {
11906
+ result = new FhirFilterComparison(this.parser.consume('Symbol').value, getOperator(this.parser.consume('Symbol').value), this.parser.consume().value);
11907
+ }
11908
+ const next = this.parser.peek()?.value;
11909
+ if (next === 'and' || next === 'or') {
11910
+ this.parser.consume('Symbol', next);
11911
+ return new FhirFilterConnective(next, result, this.parse());
11912
+ }
11913
+ return result;
11914
+ }
11915
+ }
11916
+ const fhirPathParserBuilder = initFhirPathParserBuilder();
11917
+ /**
11918
+ * Parses a FHIR _filter parameter expression into an AST.
11919
+ * @param input The FHIR _filter parameter expression.
11920
+ * @returns The AST representing the filters.
11921
+ */
11922
+ function parseFilterParameter(input) {
11923
+ const parser = fhirPathParserBuilder.construct(tokenize(input));
11924
+ parser.removeComments();
11925
+ return new FilterParameterParser(parser).parse();
11926
+ }
11927
+
11928
+ /**
11929
+ * The Hl7Context class represents the parsing context for an HL7 message.
11930
+ *
11931
+ * MSH-1:
11932
+ * https://hl7-definition.caristix.com/v2/HL7v2.6/Fields/MSH.1
11933
+ *
11934
+ * MSH-2:
11935
+ * https://hl7-definition.caristix.com/v2/HL7v2.6/Fields/MSH.2
11936
+ *
11937
+ * See this tutorial on MSH, and why it's a bad idea to use anything other than the default values:
11938
+ * https://www.hl7soup.com/HL7TutorialMSH.html
11939
+ */
11940
+ class Hl7Context {
11941
+ constructor(segmentSeparator = '\r', fieldSeparator = '|', componentSeparator = '^', repetitionSeparator = '~', escapeCharacter = '\\', subcomponentSeparator = '&') {
11942
+ this.segmentSeparator = segmentSeparator;
11943
+ this.fieldSeparator = fieldSeparator;
11944
+ this.componentSeparator = componentSeparator;
11945
+ this.repetitionSeparator = repetitionSeparator;
11946
+ this.escapeCharacter = escapeCharacter;
11947
+ this.subcomponentSeparator = subcomponentSeparator;
11948
+ }
11949
+ /**
11950
+ * Returns the MSH-2 field value based on the configured separators.
11951
+ * @returns The HL7 MSH-2 field value.
11952
+ */
11953
+ getMsh2() {
11954
+ return (this.fieldSeparator +
11955
+ this.componentSeparator +
11956
+ this.repetitionSeparator +
11957
+ this.escapeCharacter +
11958
+ this.subcomponentSeparator);
11959
+ }
11960
+ }
11961
+ /**
11962
+ * The Hl7Message class represents one HL7 message.
11963
+ * A message is a collection of segments.
11964
+ */
11965
+ class Hl7Message {
11966
+ /**
11967
+ * Creates a new HL7 message.
11968
+ * @param segments The HL7 segments.
11969
+ * @param context Optional HL7 parsing context.
11970
+ */
11971
+ constructor(segments, context = new Hl7Context()) {
11972
+ this.context = context;
11973
+ this.segments = segments;
11974
+ }
11975
+ /**
11976
+ * Returns an HL7 segment by index or by name.
11977
+ * @param index The HL7 segment index or name.
11978
+ * @returns The HL7 segment if found; otherwise, undefined.
11979
+ */
11980
+ get(index) {
11981
+ if (typeof index === 'number') {
11982
+ return this.segments[index];
11983
+ }
11984
+ return this.segments.find((s) => s.name === index);
11985
+ }
11986
+ /**
11987
+ * Returns all HL7 segments of a given name.
11988
+ * @param name The HL7 segment name.
11989
+ * @returns An array of HL7 segments with the specified name.
11990
+ */
11991
+ getAll(name) {
11992
+ return this.segments.filter((s) => s.name === name);
11993
+ }
11994
+ /**
11995
+ * Returns the HL7 message as a string.
11996
+ * @returns The HL7 message as a string.
11997
+ */
11998
+ toString() {
11999
+ return this.segments.map((s) => s.toString()).join(this.context.segmentSeparator);
12000
+ }
12001
+ /**
12002
+ * Returns an HL7 "ACK" (acknowledgement) message for this message.
12003
+ * @returns The HL7 "ACK" message.
11553
12004
  */
11554
12005
  buildAck() {
11555
12006
  const now = new Date();
@@ -11689,6 +12140,28 @@
11689
12140
  return new Hl7Field(text.split(context.repetitionSeparator).map((r) => r.split(context.componentSeparator)), context);
11690
12141
  }
11691
12142
  }
12143
+ /**
12144
+ * Returns a formatted string representing the date in ISO-8601 format.
12145
+ * @param hl7Date Date string.
12146
+ * @param options Optional configuration Object
12147
+ * @returns
12148
+ */
12149
+ function parseHl7Date(hl7Date, options) {
12150
+ if (!hl7Date) {
12151
+ return undefined;
12152
+ }
12153
+ options = { ...{ seconds: true, tzOffset: 'Z' }, ...options };
12154
+ const year = Number.parseInt(hl7Date.substring(0, 4));
12155
+ const month = Number.parseInt(hl7Date.substring(4, 6));
12156
+ const date = Number.parseInt(hl7Date.substring(6, 8));
12157
+ const hours = Number.parseInt(hl7Date.substring(8, 10));
12158
+ const minutes = Number.parseInt(hl7Date.substring(10, 12));
12159
+ const seconds = options.seconds ? Number.parseInt(hl7Date.substring(12, 14)) : 0;
12160
+ return `${pad2(year)}-${pad2(month)}-${pad2(date)}T${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}.000${options.tzOffset}`;
12161
+ }
12162
+ function pad2(n) {
12163
+ return n.toString().padStart(2, '0');
12164
+ }
11692
12165
 
11693
12166
  /*
11694
12167
  * This file provides schema validation utilities for FHIR JSON objects.
@@ -12051,577 +12524,219 @@
12051
12524
  if (Array.isArray(typedPropertyValue)) {
12052
12525
  // At present, there are no choice types that are arrays in the FHIR spec
12053
12526
  // Leaving this here to make TypeScript happy, and in case that changes
12054
- typedPropertyValue = typedPropertyValue[0];
12055
- }
12056
- if (typedPropertyValue && key === basePropertyName + capitalize(typedPropertyValue.type)) {
12057
- return true;
12058
- }
12059
- }
12060
- return false;
12061
- }
12062
- /**
12063
- * Recursively checks for null values in an object.
12064
- *
12065
- * Note that "null" is a special value in JSON that is not allowed in FHIR.
12066
- *
12067
- * @param value Input value of any type.
12068
- * @param path Path string to the value for OperationOutcome.
12069
- * @param issues Output list of issues.
12070
- */
12071
- function checkForNull(value, path, issues) {
12072
- if (value === null) {
12073
- issues.push(createStructureIssue(path, 'Invalid null value'));
12074
- }
12075
- else if (Array.isArray(value)) {
12076
- checkArrayForNull(value, path, issues);
12077
- }
12078
- else if (typeof value === 'object') {
12079
- checkObjectForNull(value, path, issues);
12080
- }
12081
- }
12082
- function checkArrayForNull(array, path, issues) {
12083
- for (let i = 0; i < array.length; i++) {
12084
- if (array[i] === undefined) {
12085
- issues.push(createStructureIssue(`${path}[${i}]`, 'Invalid undefined value'));
12086
- }
12087
- else {
12088
- checkForNull(array[i], `${path}[${i}]`, issues);
12089
- }
12090
- }
12091
- }
12092
- function checkObjectForNull(obj, path, issues) {
12093
- for (const [key, value] of Object.entries(obj)) {
12094
- checkForNull(value, `${path}${path ? '.' : ''}${key}`, issues);
12095
- }
12096
- }
12097
- function createStructureIssue(expression, details) {
12098
- return {
12099
- severity: 'error',
12100
- code: 'structure',
12101
- details: {
12102
- text: details,
12103
- },
12104
- expression: [expression],
12105
- };
12106
- }
12107
-
12108
- exports.SearchParameterType = void 0;
12109
- (function (SearchParameterType) {
12110
- SearchParameterType["BOOLEAN"] = "BOOLEAN";
12111
- SearchParameterType["NUMBER"] = "NUMBER";
12112
- SearchParameterType["QUANTITY"] = "QUANTITY";
12113
- SearchParameterType["TEXT"] = "TEXT";
12114
- SearchParameterType["REFERENCE"] = "REFERENCE";
12115
- SearchParameterType["DATE"] = "DATE";
12116
- SearchParameterType["DATETIME"] = "DATETIME";
12117
- SearchParameterType["PERIOD"] = "PERIOD";
12118
- SearchParameterType["UUID"] = "UUID";
12119
- })(exports.SearchParameterType || (exports.SearchParameterType = {}));
12120
- /**
12121
- * Returns the type details of a SearchParameter.
12122
- *
12123
- * The SearchParameter resource has a "type" parameter, but that is missing some critical information.
12124
- *
12125
- * For example:
12126
- * 1) The "date" type includes "date", "datetime", and "period".
12127
- * 2) The "token" type includes enums and booleans.
12128
- * 3) Arrays/multiple values are not reflected at all.
12129
- *
12130
- * @param resourceType The root resource type.
12131
- * @param searchParam The search parameter.
12132
- * @returns The search parameter type details.
12133
- */
12134
- function getSearchParameterDetails(resourceType, searchParam) {
12135
- let result = globalSchema.types[resourceType]?.searchParamsDetails?.[searchParam.code];
12136
- if (!result) {
12137
- result = buildSearchParamterDetails(resourceType, searchParam);
12138
- }
12139
- return result;
12140
- }
12141
- function setSearchParamterDetails(resourceType, code, details) {
12142
- const typeSchema = globalSchema.types[resourceType];
12143
- if (!typeSchema.searchParamsDetails) {
12144
- typeSchema.searchParamsDetails = {};
12145
- }
12146
- typeSchema.searchParamsDetails[code] = details;
12147
- }
12148
- function buildSearchParamterDetails(resourceType, searchParam) {
12149
- const code = searchParam.code;
12150
- const columnName = convertCodeToColumnName(code);
12151
- const expression = getExpressionForResourceType(resourceType, searchParam.expression)?.split('.');
12152
- if (!expression) {
12153
- // This happens on compound types
12154
- // In the future, explore returning multiple column definitions
12155
- return { columnName, type: exports.SearchParameterType.TEXT };
12156
- }
12157
- const defaultType = getSearchParameterType(searchParam);
12158
- let baseType = resourceType;
12159
- let elementDefinition = undefined;
12160
- let propertyType = undefined;
12161
- let array = false;
12162
- for (let i = 1; i < expression.length; i++) {
12163
- const propertyName = expression[i];
12164
- elementDefinition = getElementDefinition(baseType, propertyName);
12165
- if (!elementDefinition) {
12166
- throw new Error(`Element definition not found for ${resourceType} ${searchParam.code}`);
12167
- }
12168
- if (elementDefinition.max === '*') {
12169
- array = true;
12170
- }
12171
- propertyType = elementDefinition.type?.[0].code;
12172
- if (!propertyType) {
12173
- // This happens when one of parent properties uses contentReference
12174
- // In the future, explore following the reference
12175
- return { columnName, type: defaultType, array };
12176
- }
12177
- if (i < expression.length - 1) {
12178
- if (isBackboneElement(propertyType)) {
12179
- baseType = buildTypeName(elementDefinition.path?.split('.'));
12180
- }
12181
- else {
12182
- baseType = propertyType;
12183
- }
12184
- }
12185
- }
12186
- const type = getSearchParameterType(searchParam, propertyType);
12187
- const result = { columnName, type, elementDefinition, array };
12188
- setSearchParamterDetails(resourceType, code, result);
12189
- return result;
12190
- }
12191
- function isBackboneElement(propertyType) {
12192
- return propertyType === 'Element' || propertyType === 'BackboneElement';
12193
- }
12194
- /**
12195
- * Converts a hyphen-delimited code to camelCase string.
12196
- * @param code The search parameter code.
12197
- * @returns The SQL column name.
12198
- */
12199
- function convertCodeToColumnName(code) {
12200
- return code.split('-').reduce((result, word, index) => result + (index ? capitalize(word) : word), '');
12201
- }
12202
- function getSearchParameterType(searchParam, propertyType) {
12203
- let type = exports.SearchParameterType.TEXT;
12204
- switch (searchParam.type) {
12205
- case 'date':
12206
- if (propertyType === exports.PropertyType.dateTime || propertyType === exports.PropertyType.instant) {
12207
- type = exports.SearchParameterType.DATETIME;
12208
- }
12209
- else {
12210
- type = exports.SearchParameterType.DATE;
12211
- }
12212
- break;
12213
- case 'number':
12214
- type = exports.SearchParameterType.NUMBER;
12215
- break;
12216
- case 'quantity':
12217
- type = exports.SearchParameterType.QUANTITY;
12218
- break;
12219
- case 'reference':
12220
- type = exports.SearchParameterType.REFERENCE;
12221
- break;
12222
- case 'token':
12223
- if (propertyType === 'boolean') {
12224
- type = exports.SearchParameterType.BOOLEAN;
12225
- }
12226
- break;
12227
- }
12228
- return type;
12229
- }
12230
- function getExpressionForResourceType(resourceType, expression) {
12231
- const expressions = expression.split(' | ');
12232
- for (const e of expressions) {
12233
- if (isIgnoredExpression(e)) {
12234
- continue;
12235
- }
12236
- const simplified = simplifyExpression(e);
12237
- if (simplified.startsWith(resourceType + '.')) {
12238
- return simplified;
12527
+ typedPropertyValue = typedPropertyValue[0];
12528
+ }
12529
+ if (typedPropertyValue && key === basePropertyName + capitalize(typedPropertyValue.type)) {
12530
+ return true;
12239
12531
  }
12240
12532
  }
12241
- return undefined;
12242
- }
12243
- function isIgnoredExpression(input) {
12244
- return input.includes(' as Period') || input.includes(' as SampledDate');
12533
+ return false;
12245
12534
  }
12246
- function simplifyExpression(input) {
12247
- let result = input.trim();
12248
- if (result.startsWith('(') && result.endsWith(')')) {
12249
- result = result.substring(1, result.length - 1);
12535
+ /**
12536
+ * Recursively checks for null values in an object.
12537
+ *
12538
+ * Note that "null" is a special value in JSON that is not allowed in FHIR.
12539
+ *
12540
+ * @param value Input value of any type.
12541
+ * @param path Path string to the value for OperationOutcome.
12542
+ * @param issues Output list of issues.
12543
+ */
12544
+ function checkForNull(value, path, issues) {
12545
+ if (value === null) {
12546
+ issues.push(createStructureIssue(path, 'Invalid null value'));
12250
12547
  }
12251
- if (result.includes('[0]')) {
12252
- result = result.replaceAll('[0]', '');
12548
+ else if (Array.isArray(value)) {
12549
+ checkArrayForNull(value, path, issues);
12253
12550
  }
12254
- const stopStrings = [' != ', ' as ', '.as(', '.exists(', '.resolve(', '.where('];
12255
- for (const stopString of stopStrings) {
12256
- if (result.includes(stopString)) {
12257
- result = result.substring(0, result.indexOf(stopString));
12258
- }
12551
+ else if (typeof value === 'object') {
12552
+ checkObjectForNull(value, path, issues);
12259
12553
  }
12260
- return result;
12261
12554
  }
12262
-
12263
- const DEFAULT_SEARCH_COUNT = 20;
12264
- /**
12265
- * Search operators.
12266
- * These operators represent "modifiers" and "prefixes" in FHIR search.
12267
- * See: https://www.hl7.org/fhir/search.html
12268
- */
12269
- exports.Operator = void 0;
12270
- (function (Operator) {
12271
- Operator["EQUALS"] = "eq";
12272
- Operator["NOT_EQUALS"] = "ne";
12273
- // Numbers
12274
- Operator["GREATER_THAN"] = "gt";
12275
- Operator["LESS_THAN"] = "lt";
12276
- Operator["GREATER_THAN_OR_EQUALS"] = "ge";
12277
- Operator["LESS_THAN_OR_EQUALS"] = "le";
12278
- // Dates
12279
- Operator["STARTS_AFTER"] = "sa";
12280
- Operator["ENDS_BEFORE"] = "eb";
12281
- Operator["APPROXIMATELY"] = "ap";
12282
- // String
12283
- Operator["CONTAINS"] = "contains";
12284
- Operator["EXACT"] = "exact";
12285
- // Token
12286
- Operator["TEXT"] = "text";
12287
- Operator["NOT"] = "not";
12288
- Operator["ABOVE"] = "above";
12289
- Operator["BELOW"] = "below";
12290
- Operator["IN"] = "in";
12291
- Operator["NOT_IN"] = "not-in";
12292
- Operator["OF_TYPE"] = "of-type";
12293
- // All
12294
- Operator["MISSING"] = "missing";
12295
- // Reference
12296
- Operator["IDENTIFIER"] = "identifier";
12297
- // _include and _revinclude
12298
- Operator["ITERATE"] = "iterate";
12299
- })(exports.Operator || (exports.Operator = {}));
12300
- /**
12301
- * Parameter names may specify a modifier as a suffix.
12302
- * The modifiers are separated from the parameter name by a colon.
12303
- * See: https://www.hl7.org/fhir/search.html#modifiers
12304
- */
12305
- const MODIFIER_OPERATORS = {
12306
- contains: exports.Operator.CONTAINS,
12307
- exact: exports.Operator.EXACT,
12308
- above: exports.Operator.ABOVE,
12309
- below: exports.Operator.BELOW,
12310
- text: exports.Operator.TEXT,
12311
- not: exports.Operator.NOT,
12312
- in: exports.Operator.IN,
12313
- 'not-in': exports.Operator.NOT_IN,
12314
- 'of-type': exports.Operator.OF_TYPE,
12315
- missing: exports.Operator.MISSING,
12316
- identifier: exports.Operator.IDENTIFIER,
12317
- iterate: exports.Operator.ITERATE,
12318
- };
12319
- /**
12320
- * For the ordered parameter types of number, date, and quantity,
12321
- * a prefix to the parameter value may be used to control the nature
12322
- * of the matching.
12323
- * See: https://www.hl7.org/fhir/search.html#prefix
12324
- */
12325
- const PREFIX_OPERATORS = {
12326
- eq: exports.Operator.EQUALS,
12327
- ne: exports.Operator.NOT_EQUALS,
12328
- lt: exports.Operator.LESS_THAN,
12329
- le: exports.Operator.LESS_THAN_OR_EQUALS,
12330
- gt: exports.Operator.GREATER_THAN,
12331
- ge: exports.Operator.GREATER_THAN_OR_EQUALS,
12332
- sa: exports.Operator.STARTS_AFTER,
12333
- eb: exports.Operator.ENDS_BEFORE,
12334
- ap: exports.Operator.APPROXIMATELY,
12335
- };
12336
- /**
12337
- * Parses a search URL into a search request.
12338
- * @param resourceType The FHIR resource type.
12339
- * @param query The collection of query string parameters.
12340
- * @returns A parsed SearchRequest.
12341
- */
12342
- function parseSearchRequest(resourceType, query) {
12343
- const queryArray = [];
12344
- for (const [key, value] of Object.entries(query)) {
12345
- if (Array.isArray(value)) {
12346
- for (let i = 0; i < value.length; i++) {
12347
- queryArray.push([key, value[i]]);
12348
- }
12555
+ function checkArrayForNull(array, path, issues) {
12556
+ for (let i = 0; i < array.length; i++) {
12557
+ if (array[i] === undefined) {
12558
+ issues.push(createStructureIssue(`${path}[${i}]`, 'Invalid undefined value'));
12349
12559
  }
12350
12560
  else {
12351
- queryArray.push([key, value || '']);
12561
+ checkForNull(array[i], `${path}[${i}]`, issues);
12352
12562
  }
12353
12563
  }
12354
- return parseSearchImpl(resourceType, queryArray);
12355
12564
  }
12356
- /**
12357
- * Parses a search URL into a search request.
12358
- * @param url The search URL.
12359
- * @returns A parsed SearchRequest.
12360
- */
12361
- function parseSearchUrl(url) {
12362
- const resourceType = url.pathname.split('/').filter(Boolean).pop();
12363
- return parseSearchImpl(resourceType, url.searchParams.entries());
12565
+ function checkObjectForNull(obj, path, issues) {
12566
+ for (const [key, value] of Object.entries(obj)) {
12567
+ checkForNull(value, `${path}${path ? '.' : ''}${key}`, issues);
12568
+ }
12569
+ }
12570
+ function createStructureIssue(expression, details) {
12571
+ return {
12572
+ severity: 'error',
12573
+ code: 'structure',
12574
+ details: {
12575
+ text: details,
12576
+ },
12577
+ expression: [expression],
12578
+ };
12364
12579
  }
12580
+
12581
+ exports.SearchParameterType = void 0;
12582
+ (function (SearchParameterType) {
12583
+ SearchParameterType["BOOLEAN"] = "BOOLEAN";
12584
+ SearchParameterType["NUMBER"] = "NUMBER";
12585
+ SearchParameterType["QUANTITY"] = "QUANTITY";
12586
+ SearchParameterType["TEXT"] = "TEXT";
12587
+ SearchParameterType["REFERENCE"] = "REFERENCE";
12588
+ SearchParameterType["CANONICAL"] = "CANONICAL";
12589
+ SearchParameterType["DATE"] = "DATE";
12590
+ SearchParameterType["DATETIME"] = "DATETIME";
12591
+ SearchParameterType["PERIOD"] = "PERIOD";
12592
+ SearchParameterType["UUID"] = "UUID";
12593
+ })(exports.SearchParameterType || (exports.SearchParameterType = {}));
12365
12594
  /**
12366
- * Parses a URL string into a SearchRequest.
12367
- * @param url The URL to parse.
12368
- * @returns Parsed search definition.
12595
+ * Returns the type details of a SearchParameter.
12596
+ *
12597
+ * The SearchParameter resource has a "type" parameter, but that is missing some critical information.
12598
+ *
12599
+ * For example:
12600
+ * 1) The "date" type includes "date", "datetime", and "period".
12601
+ * 2) The "token" type includes enums and booleans.
12602
+ * 3) Arrays/multiple values are not reflected at all.
12603
+ *
12604
+ * @param resourceType The root resource type.
12605
+ * @param searchParam The search parameter.
12606
+ * @returns The search parameter type details.
12369
12607
  */
12370
- function parseSearchDefinition(url) {
12371
- return parseSearchUrl(new URL(url, 'https://example.com/'));
12372
- }
12373
- function parseSearchImpl(resourceType, query) {
12374
- const searchRequest = {
12375
- resourceType,
12376
- };
12377
- for (const [key, value] of query) {
12378
- parseKeyValue(searchRequest, key, value);
12608
+ function getSearchParameterDetails(resourceType, searchParam) {
12609
+ let result = globalSchema.types[resourceType]?.searchParamsDetails?.[searchParam.code];
12610
+ if (!result) {
12611
+ result = buildSearchParamterDetails(resourceType, searchParam);
12379
12612
  }
12380
- return searchRequest;
12613
+ return result;
12381
12614
  }
12382
- function parseKeyValue(searchRequest, key, value) {
12383
- let code;
12384
- let modifier;
12385
- const colonIndex = key.indexOf(':');
12386
- if (colonIndex >= 0) {
12387
- code = key.substring(0, colonIndex);
12388
- modifier = key.substring(colonIndex + 1);
12615
+ function setSearchParamterDetails(resourceType, code, details) {
12616
+ const typeSchema = globalSchema.types[resourceType];
12617
+ if (!typeSchema.searchParamsDetails) {
12618
+ typeSchema.searchParamsDetails = {};
12389
12619
  }
12390
- else {
12391
- code = key;
12392
- modifier = '';
12620
+ typeSchema.searchParamsDetails[code] = details;
12621
+ }
12622
+ function buildSearchParamterDetails(resourceType, searchParam) {
12623
+ const code = searchParam.code;
12624
+ const columnName = convertCodeToColumnName(code);
12625
+ const expression = getExpressionForResourceType(resourceType, searchParam.expression)?.split('.');
12626
+ if (!expression) {
12627
+ // This happens on compound types
12628
+ // In the future, explore returning multiple column definitions
12629
+ return { columnName, type: exports.SearchParameterType.TEXT };
12393
12630
  }
12394
- switch (code) {
12395
- case '_sort':
12396
- parseSortRule(searchRequest, value);
12397
- break;
12398
- case '_count':
12399
- searchRequest.count = parseInt(value);
12400
- break;
12401
- case '_offset':
12402
- searchRequest.offset = parseInt(value);
12403
- break;
12404
- case '_total':
12405
- searchRequest.total = value;
12406
- break;
12407
- case '_summary':
12408
- searchRequest.total = 'estimate';
12409
- searchRequest.count = 0;
12410
- break;
12411
- case '_include': {
12412
- const target = parseIncludeTarget(value);
12413
- if (modifier === 'iterate') {
12414
- target.modifier = exports.Operator.ITERATE;
12415
- }
12416
- if (searchRequest.include) {
12417
- searchRequest.include.push(target);
12418
- }
12419
- else {
12420
- searchRequest.include = [target];
12421
- }
12422
- break;
12631
+ const defaultType = getSearchParameterType(searchParam);
12632
+ let baseType = resourceType;
12633
+ let elementDefinition = undefined;
12634
+ let propertyType = undefined;
12635
+ let array = false;
12636
+ for (let i = 1; i < expression.length; i++) {
12637
+ const propertyName = expression[i];
12638
+ elementDefinition = getElementDefinition(baseType, propertyName);
12639
+ if (!elementDefinition) {
12640
+ throw new Error(`Element definition not found for ${resourceType} ${searchParam.code}`);
12423
12641
  }
12424
- case '_revinclude': {
12425
- const target = parseIncludeTarget(value);
12426
- if (modifier === 'iterate') {
12427
- target.modifier = exports.Operator.ITERATE;
12428
- }
12429
- if (searchRequest.revInclude) {
12430
- searchRequest.revInclude.push(target);
12431
- }
12432
- else {
12433
- searchRequest.revInclude = [target];
12434
- }
12435
- break;
12642
+ if (elementDefinition.max !== '0' && elementDefinition.max !== '1') {
12643
+ array = true;
12436
12644
  }
12437
- case '_fields':
12438
- searchRequest.fields = value.split(',');
12439
- break;
12440
- default: {
12441
- const param = globalSchema.types[searchRequest.resourceType]?.searchParams?.[code];
12442
- if (param) {
12443
- parseParameter(searchRequest, param, modifier, value);
12645
+ propertyType = elementDefinition.type?.[0].code;
12646
+ if (!propertyType) {
12647
+ // This happens when one of parent properties uses contentReference
12648
+ // In the future, explore following the reference
12649
+ return { columnName, type: defaultType, array };
12650
+ }
12651
+ if (i < expression.length - 1) {
12652
+ if (isBackboneElement(propertyType)) {
12653
+ baseType = buildTypeName(elementDefinition.path?.split('.'));
12444
12654
  }
12445
12655
  else {
12446
- parseUnknownParameter(searchRequest, code, modifier, value);
12656
+ baseType = propertyType;
12447
12657
  }
12448
12658
  }
12449
12659
  }
12660
+ const type = getSearchParameterType(searchParam, propertyType);
12661
+ const result = { columnName, type, elementDefinition, array };
12662
+ setSearchParamterDetails(resourceType, code, result);
12663
+ return result;
12450
12664
  }
12451
- function parseSortRule(searchRequest, value) {
12452
- for (const field of value.split(',')) {
12453
- let code;
12454
- let descending = false;
12455
- if (field.startsWith('-')) {
12456
- code = field.substring(1);
12457
- descending = true;
12458
- }
12459
- else {
12460
- code = field;
12461
- }
12462
- if (!searchRequest.sortRules) {
12463
- searchRequest.sortRules = [];
12464
- }
12465
- searchRequest.sortRules.push({ code, descending });
12466
- }
12665
+ function isBackboneElement(propertyType) {
12666
+ return propertyType === 'Element' || propertyType === 'BackboneElement';
12467
12667
  }
12468
- function parseParameter(searchRequest, searchParam, modifier, value) {
12469
- if (modifier === 'missing') {
12470
- addFilter(searchRequest, {
12471
- code: searchParam.code,
12472
- operator: exports.Operator.MISSING,
12473
- value,
12474
- });
12475
- return;
12476
- }
12668
+ /**
12669
+ * Converts a hyphen-delimited code to camelCase string.
12670
+ * @param code The search parameter code.
12671
+ * @returns The SQL column name.
12672
+ */
12673
+ function convertCodeToColumnName(code) {
12674
+ return code.split('-').reduce((result, word, index) => result + (index ? capitalize(word) : word), '');
12675
+ }
12676
+ function getSearchParameterType(searchParam, propertyType) {
12677
+ let type = exports.SearchParameterType.TEXT;
12477
12678
  switch (searchParam.type) {
12478
- case 'number':
12479
12679
  case 'date':
12480
- parsePrefixType(searchRequest, searchParam, value);
12680
+ if (propertyType === exports.PropertyType.date) {
12681
+ type = exports.SearchParameterType.DATE;
12682
+ }
12683
+ else {
12684
+ type = exports.SearchParameterType.DATETIME;
12685
+ }
12481
12686
  break;
12482
- case 'reference':
12483
- case 'string':
12484
- case 'token':
12485
- case 'uri':
12486
- parseModifierType(searchRequest, searchParam, modifier, value);
12687
+ case 'number':
12688
+ type = exports.SearchParameterType.NUMBER;
12487
12689
  break;
12488
12690
  case 'quantity':
12489
- parseQuantity(searchRequest, searchParam, value);
12691
+ type = exports.SearchParameterType.QUANTITY;
12490
12692
  break;
12491
- }
12492
- }
12493
- function parsePrefixType(searchRequest, param, input) {
12494
- const { operator, value } = parsePrefix(input);
12495
- addFilter(searchRequest, {
12496
- code: param.code,
12497
- operator,
12498
- value,
12499
- });
12500
- }
12501
- function parseModifierType(searchRequest, param, modifier, value) {
12502
- addFilter(searchRequest, {
12503
- code: param.code,
12504
- operator: parseModifier(modifier),
12505
- value,
12506
- });
12507
- }
12508
- function parseQuantity(searchRequest, param, input) {
12509
- const [prefixNumber, unitSystem, unitCode] = input.split('|');
12510
- const { operator, value } = parsePrefix(prefixNumber);
12511
- addFilter(searchRequest, {
12512
- code: param.code,
12513
- operator,
12514
- value,
12515
- unitSystem,
12516
- unitCode,
12517
- });
12518
- }
12519
- function parseUnknownParameter(searchRequest, code, modifier, value) {
12520
- let operator = exports.Operator.EQUALS;
12521
- if (modifier) {
12522
- operator = modifier;
12523
- }
12524
- else if (value.length >= 2) {
12525
- const prefix = value.substring(0, 2);
12526
- if (prefix in PREFIX_OPERATORS) {
12527
- if (value.length === 2 || value.at(2)?.match(/\d/)) {
12528
- operator = prefix;
12529
- value = value.substring(prefix.length);
12693
+ case 'reference':
12694
+ if (propertyType === exports.PropertyType.canonical) {
12695
+ type = exports.SearchParameterType.CANONICAL;
12530
12696
  }
12531
- }
12532
- }
12533
- addFilter(searchRequest, {
12534
- code,
12535
- operator,
12536
- value,
12537
- });
12538
- }
12539
- function parsePrefix(input) {
12540
- const prefix = input.substring(0, 2);
12541
- const prefixOperator = PREFIX_OPERATORS[prefix];
12542
- if (prefixOperator) {
12543
- return { operator: prefixOperator, value: input.substring(2) };
12697
+ else {
12698
+ type = exports.SearchParameterType.REFERENCE;
12699
+ }
12700
+ break;
12701
+ case 'token':
12702
+ if (propertyType === 'boolean') {
12703
+ type = exports.SearchParameterType.BOOLEAN;
12704
+ }
12705
+ break;
12544
12706
  }
12545
- return { operator: exports.Operator.EQUALS, value: input };
12546
- }
12547
- function parseModifier(modifier) {
12548
- return MODIFIER_OPERATORS[modifier] || exports.Operator.EQUALS;
12707
+ return type;
12549
12708
  }
12550
- function parseIncludeTarget(input) {
12551
- const parts = input.split(':');
12552
- parts.forEach((p) => {
12553
- if (p === '*') {
12554
- throw new OperationOutcomeError(badRequest(`'*' is not supported as a value for search inclusion parameters`));
12709
+ function getExpressionForResourceType(resourceType, expression) {
12710
+ const expressions = expression.split(' | ');
12711
+ for (const e of expressions) {
12712
+ if (isIgnoredExpression(e)) {
12713
+ continue;
12714
+ }
12715
+ const simplified = simplifyExpression(e);
12716
+ if (simplified.startsWith(resourceType + '.')) {
12717
+ return simplified;
12555
12718
  }
12556
- });
12557
- if (parts.length === 1) {
12558
- // Full wildcard, not currently supported
12559
- throw new OperationOutcomeError(badRequest(`Invalid include value '${input}': must be of the form ResourceType:search-parameter`));
12560
- }
12561
- else if (parts.length === 2) {
12562
- return {
12563
- resourceType: parts[0],
12564
- searchParam: parts[1],
12565
- };
12566
- }
12567
- else if (parts.length === 3) {
12568
- return {
12569
- resourceType: parts[0],
12570
- searchParam: parts[1],
12571
- targetType: parts[2],
12572
- };
12573
- }
12574
- else {
12575
- throw new OperationOutcomeError(badRequest(`Invalid include value '${input}'`));
12576
12719
  }
12720
+ return undefined;
12577
12721
  }
12578
- function addFilter(searchRequest, filter) {
12579
- if (searchRequest.filters) {
12580
- searchRequest.filters.push(filter);
12581
- }
12582
- else {
12583
- searchRequest.filters = [filter];
12584
- }
12722
+ function isIgnoredExpression(input) {
12723
+ return input.includes(' as Period') || input.includes(' as SampledDate');
12585
12724
  }
12586
- /**
12587
- * Formats a search definition object into a query string.
12588
- * Note: The return value does not include the resource type.
12589
- * @param {!SearchRequest} definition The search definition.
12590
- * @returns Formatted URL.
12591
- */
12592
- function formatSearchQuery(definition) {
12593
- const params = [];
12594
- if (definition.fields) {
12595
- params.push('_fields=' + definition.fields.join(','));
12596
- }
12597
- if (definition.filters) {
12598
- definition.filters.forEach((filter) => params.push(formatFilter(filter)));
12599
- }
12600
- if (definition.sortRules && definition.sortRules.length > 0) {
12601
- params.push(formatSortRules(definition.sortRules));
12602
- }
12603
- if (definition.offset !== undefined) {
12604
- params.push('_offset=' + definition.offset);
12605
- }
12606
- if (definition.count !== undefined) {
12607
- params.push('_count=' + definition.count);
12725
+ function simplifyExpression(input) {
12726
+ let result = input.trim();
12727
+ if (result.startsWith('(') && result.endsWith(')')) {
12728
+ result = result.substring(1, result.length - 1);
12608
12729
  }
12609
- if (definition.total !== undefined) {
12610
- params.push('_total=' + definition.total);
12730
+ if (result.includes('[0]')) {
12731
+ result = result.replaceAll('[0]', '');
12611
12732
  }
12612
- if (params.length === 0) {
12613
- return '';
12733
+ const stopStrings = [' != ', ' as ', '.as(', '.exists(', '.resolve(', '.where('];
12734
+ for (const stopString of stopStrings) {
12735
+ if (result.includes(stopString)) {
12736
+ result = result.substring(0, result.indexOf(stopString));
12737
+ }
12614
12738
  }
12615
- params.sort();
12616
- return '?' + params.join('&');
12617
- }
12618
- function formatFilter(filter) {
12619
- const modifier = filter.operator in MODIFIER_OPERATORS ? ':' + filter.operator : '';
12620
- const prefix = filter.operator !== exports.Operator.EQUALS && filter.operator in PREFIX_OPERATORS ? filter.operator : '';
12621
- return `${filter.code}${modifier}=${prefix}${encodeURIComponent(filter.value)}`;
12622
- }
12623
- function formatSortRules(sortRules) {
12624
- return '_sort=' + sortRules.map((sr) => (sr.descending ? '-' + sr.code : sr.code)).join(',');
12739
+ return result;
12625
12740
  }
12626
12741
 
12627
12742
  /**
@@ -12779,6 +12894,29 @@
12779
12894
  return operator === exports.Operator.NOT_EQUALS || operator === exports.Operator.NOT;
12780
12895
  }
12781
12896
 
12897
+ /**
12898
+ * Reads data from a Readable stream and returns a Promise that resolves with a Buffer containing all the data.
12899
+ * @param stream - The Readable stream to read from.
12900
+ * @returns A Promise that resolves with a Buffer containing all the data from the Readable stream.
12901
+ */
12902
+ function streamToBuffer(stream) {
12903
+ const chunks = [];
12904
+ return new Promise((resolve, reject) => {
12905
+ stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
12906
+ stream.on('error', (err) => {
12907
+ console.error(err.message);
12908
+ stream.destroy();
12909
+ reject(err);
12910
+ });
12911
+ stream.on('end', () => {
12912
+ resolve(Buffer.concat(chunks));
12913
+ });
12914
+ stream.on('close', () => {
12915
+ stream.destroy();
12916
+ });
12917
+ });
12918
+ }
12919
+
12782
12920
  exports.AndAtom = AndAtom;
12783
12921
  exports.ArithemticOperatorAtom = ArithemticOperatorAtom;
12784
12922
  exports.AsAtom = AsAtom;
@@ -12852,6 +12990,7 @@
12852
12990
  exports.fhirPathNot = fhirPathNot;
12853
12991
  exports.findObservationInterval = findObservationInterval;
12854
12992
  exports.findObservationReferenceRange = findObservationReferenceRange;
12993
+ exports.findResourceByCode = findResourceByCode;
12855
12994
  exports.forbidden = forbidden;
12856
12995
  exports.formatAddress = formatAddress;
12857
12996
  exports.formatCodeableConcept = formatCodeableConcept;
@@ -12922,6 +13061,7 @@
12922
13061
  exports.operationOutcomeToString = operationOutcomeToString;
12923
13062
  exports.parseFhirPath = parseFhirPath;
12924
13063
  exports.parseFilterParameter = parseFilterParameter;
13064
+ exports.parseHl7Date = parseHl7Date;
12925
13065
  exports.parseJWTPayload = parseJWTPayload;
12926
13066
  exports.parseMappingLanguage = parseMappingLanguage;
12927
13067
  exports.parseSearchDefinition = parseSearchDefinition;
@@ -12936,6 +13076,7 @@
12936
13076
  exports.removeDuplicates = removeDuplicates;
12937
13077
  exports.resolveId = resolveId;
12938
13078
  exports.setCodeBySystem = setCodeBySystem;
13079
+ exports.streamToBuffer = streamToBuffer;
12939
13080
  exports.stringify = stringify;
12940
13081
  exports.toJsBoolean = toJsBoolean;
12941
13082
  exports.toTypedValue = toTypedValue;