@openhi/constructs 0.0.135 → 0.0.136

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.
@@ -2495,7 +2495,6 @@ var ENV_VAR_NAMES = [
2495
2495
  "OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID",
2496
2496
  "OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID",
2497
2497
  "OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL",
2498
- "OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI",
2499
2498
  "OPENHI_RUNTIME_CONFIG_API_BASE_URL"
2500
2499
  ];
2501
2500
  var CACHE_CONTROL_HEADER = "public, max-age=300, s-maxage=300";
@@ -2512,7 +2511,6 @@ function runtimeConfigGetRoute(_req, res) {
2512
2511
  cognitoUserPoolId: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID,
2513
2512
  cognitoUserPoolClientId: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID,
2514
2513
  cognitoDomainUrl: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL,
2515
- cognitoRedirectUri: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI,
2516
2514
  apiBaseUrl: process.env.OPENHI_RUNTIME_CONFIG_API_BASE_URL
2517
2515
  };
2518
2516
  res.setHeader("Cache-Control", CACHE_CONTROL_HEADER);
@@ -5532,6 +5530,29 @@ function getDefaultPostgresQueryRunner() {
5532
5530
  }
5533
5531
 
5534
5532
  // src/data/search/engine/date-predicate.ts
5533
+ var DATE_SEARCH_PREFIXES = [
5534
+ "eq",
5535
+ "gt",
5536
+ "lt",
5537
+ "ge",
5538
+ "le",
5539
+ "sa",
5540
+ "eb"
5541
+ ];
5542
+ function isDateSearchPrefix(s) {
5543
+ return DATE_SEARCH_PREFIXES.includes(s);
5544
+ }
5545
+ var GENERIC_DATE_PREFIXES = ["eq", "gt", "ge", "lt", "le"];
5546
+ function isGenericDatePrefix(s) {
5547
+ return GENERIC_DATE_PREFIXES.includes(s);
5548
+ }
5549
+ function parseDateSearchValue(raw) {
5550
+ const head = raw.slice(0, 2);
5551
+ if (isDateSearchPrefix(head) && raw.length > 2) {
5552
+ return { prefix: head, value: raw.slice(2) };
5553
+ }
5554
+ return { prefix: "eq", value: raw };
5555
+ }
5535
5556
  function flatJsonbExtract(jsonbPath) {
5536
5557
  const match = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5537
5558
  if (!match) {
@@ -5541,6 +5562,40 @@ function flatJsonbExtract(jsonbPath) {
5541
5562
  }
5542
5563
  return `resource->>'${match[1]}'`;
5543
5564
  }
5565
+ function emitDatePredicate(opts) {
5566
+ const { jsonbPath, rawValue, paramName } = opts;
5567
+ const extract = flatJsonbExtract(jsonbPath);
5568
+ const { prefix, value } = parseDateSearchValue(rawValue);
5569
+ if (!isGenericDatePrefix(prefix)) {
5570
+ throw new Error(
5571
+ `Generic date predicate does not support prefix "${prefix}". Supported: ${GENERIC_DATE_PREFIXES.join(", ")}.`
5572
+ );
5573
+ }
5574
+ const sql = buildSingleBoundSql(extract, prefix, paramName);
5575
+ const params = [
5576
+ { name: paramName, value }
5577
+ ];
5578
+ return { sql, params };
5579
+ }
5580
+ function buildSingleBoundSql(extract, prefix, paramName) {
5581
+ switch (prefix) {
5582
+ case "eq":
5583
+ return `${extract} = :${paramName}`;
5584
+ case "gt":
5585
+ return `${extract} > :${paramName}`;
5586
+ case "ge":
5587
+ return `${extract} >= :${paramName}`;
5588
+ case "lt":
5589
+ return `${extract} < :${paramName}`;
5590
+ case "le":
5591
+ return `${extract} <= :${paramName}`;
5592
+ }
5593
+ }
5594
+ function emitFieldMissingPredicate(opts) {
5595
+ const extract = flatJsonbExtract(opts.jsonbPath);
5596
+ const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
5597
+ return { sql, params: [] };
5598
+ }
5544
5599
  var DEFAULT_INTERVAL_PARAM_PREFIX = "intervalDateConstraint";
5545
5600
  function intervalConstraintParamName(prefix, index) {
5546
5601
  return `${prefix}${index}`;
@@ -5656,6 +5711,46 @@ function buildReferenceContainmentPayload(params) {
5656
5711
  containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
5657
5712
  };
5658
5713
  }
5714
+ function jsonbPathToReferenceShape(jsonbPath) {
5715
+ const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5716
+ if (scalar) {
5717
+ return { kind: "scalar", field: scalar[1] };
5718
+ }
5719
+ const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5720
+ if (arrayOfRefs) {
5721
+ return { kind: "array-of-references", field: arrayOfRefs[1] };
5722
+ }
5723
+ const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
5724
+ jsonbPath
5725
+ );
5726
+ if (arrayOfObjs) {
5727
+ return {
5728
+ kind: "array-of-objects",
5729
+ field: arrayOfObjs[1],
5730
+ subfield: arrayOfObjs[2]
5731
+ };
5732
+ }
5733
+ throw new Error(
5734
+ `Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5735
+ );
5736
+ }
5737
+ function emitReferencePredicate(opts) {
5738
+ const shape = jsonbPathToReferenceShape(opts.jsonbPath);
5739
+ const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
5740
+ shape,
5741
+ reference: opts.rawValue,
5742
+ tenantId: opts.context.tenantId,
5743
+ workspaceId: opts.context.workspaceId
5744
+ });
5745
+ const relName = `${opts.paramName}R`;
5746
+ const urnName = `${opts.paramName}U`;
5747
+ const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
5748
+ const params = [
5749
+ { name: relName, value: containmentRelative },
5750
+ { name: urnName, value: containmentUrn }
5751
+ ];
5752
+ return { sql, params };
5753
+ }
5659
5754
 
5660
5755
  // src/data/operations/data/appointment/appointment-search-by-actor-operation.ts
5661
5756
  var DEFAULT_LIMIT = 100;
@@ -25481,39 +25576,334 @@ async function listPatientsOperation(params) {
25481
25576
  );
25482
25577
  }
25483
25578
 
25484
- // src/data/operations/data/patient/patient-search-by-general-practitioner-operation.ts
25579
+ // src/data/search/engine/string-predicate.ts
25580
+ var STRING_MODIFIERS = ["exact", "contains"];
25581
+ function isStringModifier(s) {
25582
+ return STRING_MODIFIERS.includes(s);
25583
+ }
25584
+ function jsonbPathToStringShape(jsonbPath) {
25585
+ const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
25586
+ if (scalar) {
25587
+ return { kind: "scalar", field: scalar[1] };
25588
+ }
25589
+ const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
25590
+ if (arrayOfScalars) {
25591
+ return { kind: "array-of-scalars", field: arrayOfScalars[1] };
25592
+ }
25593
+ const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
25594
+ jsonbPath
25595
+ );
25596
+ if (arrayOfObjs) {
25597
+ return {
25598
+ kind: "array-of-objects",
25599
+ field: arrayOfObjs[1],
25600
+ subfield: arrayOfObjs[2]
25601
+ };
25602
+ }
25603
+ throw new Error(
25604
+ `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
25605
+ );
25606
+ }
25607
+ function escapeLikePattern(value) {
25608
+ return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
25609
+ }
25610
+ function buildLikePattern(value, modifier) {
25611
+ const escaped = escapeLikePattern(value);
25612
+ switch (modifier) {
25613
+ case "exact":
25614
+ return escaped;
25615
+ case "contains":
25616
+ return `%${escaped}%`;
25617
+ case void 0:
25618
+ return `${escaped}%`;
25619
+ }
25620
+ }
25621
+ function buildIlikeExtractSql(shape, paramName) {
25622
+ switch (shape.kind) {
25623
+ case "scalar":
25624
+ return `resource->>'${shape.field}' ILIKE :${paramName}`;
25625
+ case "array-of-scalars":
25626
+ return [
25627
+ "EXISTS (SELECT 1 FROM",
25628
+ `jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
25629
+ `WHERE s_elem.text_val ILIKE :${paramName})`
25630
+ ].join(" ");
25631
+ case "array-of-objects":
25632
+ return [
25633
+ "EXISTS (SELECT 1 FROM",
25634
+ `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
25635
+ `WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
25636
+ ].join(" ");
25637
+ }
25638
+ }
25639
+ function emitStringPredicate(opts) {
25640
+ const shape = jsonbPathToStringShape(opts.jsonbPath);
25641
+ const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
25642
+ const sql = buildIlikeExtractSql(shape, opts.paramName);
25643
+ const pattern = buildLikePattern(opts.rawValue, modifier);
25644
+ const params = [
25645
+ { name: opts.paramName, value: pattern }
25646
+ ];
25647
+ return { sql, params };
25648
+ }
25649
+ function checkModifier(modifier) {
25650
+ if (!isStringModifier(modifier)) {
25651
+ throw new Error(
25652
+ `String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
25653
+ );
25654
+ }
25655
+ return modifier;
25656
+ }
25657
+
25658
+ // src/data/search/engine/token-predicate.ts
25659
+ function jsonbPathToTokenShape(jsonbPath) {
25660
+ const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
25661
+ if (scalar) {
25662
+ return { kind: "scalar", field: scalar[1] };
25663
+ }
25664
+ const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
25665
+ if (arrayOfScalars) {
25666
+ return { kind: "array-of-scalars", field: arrayOfScalars[1] };
25667
+ }
25668
+ const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
25669
+ jsonbPath
25670
+ );
25671
+ if (arrayOfObjs) {
25672
+ return {
25673
+ kind: "array-of-objects",
25674
+ field: arrayOfObjs[1],
25675
+ subfield: arrayOfObjs[2]
25676
+ };
25677
+ }
25678
+ throw new Error(
25679
+ `Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
25680
+ );
25681
+ }
25682
+ function parseTokenValue(raw) {
25683
+ const idx = raw.indexOf("|");
25684
+ if (idx === -1) {
25685
+ return { code: raw };
25686
+ }
25687
+ const system = raw.slice(0, idx);
25688
+ const code = raw.slice(idx + 1);
25689
+ return system.length > 0 ? { system, code } : { code };
25690
+ }
25691
+ function wrapTokenInShape(shape, value) {
25692
+ switch (shape.kind) {
25693
+ case "scalar":
25694
+ return { [shape.field]: value };
25695
+ case "array-of-scalars":
25696
+ return { [shape.field]: [value] };
25697
+ case "array-of-objects":
25698
+ return { [shape.field]: [{ [shape.subfield]: value }] };
25699
+ }
25700
+ }
25701
+ function emitTokenPredicate(opts) {
25702
+ const shape = jsonbPathToTokenShape(opts.jsonbPath);
25703
+ const { code } = parseTokenValue(opts.rawValue);
25704
+ const payload = wrapTokenInShape(shape, code);
25705
+ const sql = `resource @> :${opts.paramName}::jsonb`;
25706
+ const params = [
25707
+ { name: opts.paramName, value: JSON.stringify(payload) }
25708
+ ];
25709
+ return { sql, params };
25710
+ }
25711
+
25712
+ // src/data/search/engine/combinator.ts
25713
+ function parseQueryKey(key) {
25714
+ const idx = key.indexOf(":");
25715
+ if (idx === -1) {
25716
+ return { code: key, modifier: void 0 };
25717
+ }
25718
+ return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
25719
+ }
25720
+ function flattenQueryValues(raw) {
25721
+ const list = Array.isArray(raw) ? raw : [raw];
25722
+ const out = [];
25723
+ for (const v of list) {
25724
+ for (const piece of v.split(",")) {
25725
+ out.push(piece);
25726
+ }
25727
+ }
25728
+ return out;
25729
+ }
25730
+ function parseQueryEntries(query) {
25731
+ const entries = [];
25732
+ for (const [rawKey, rawValue] of Object.entries(query)) {
25733
+ if (rawValue === void 0) continue;
25734
+ const { code, modifier } = parseQueryKey(rawKey);
25735
+ entries.push({
25736
+ code,
25737
+ modifier,
25738
+ values: flattenQueryValues(rawValue)
25739
+ });
25740
+ }
25741
+ return entries;
25742
+ }
25743
+ function findParam(params, code) {
25744
+ return params.find((p) => p.code === code);
25745
+ }
25746
+ function checkModifierAllowed(modifier, param) {
25747
+ const universal = modifier === "missing" || modifier === "not";
25748
+ const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
25749
+ if (universal || stringNative) {
25750
+ if (param.modifiers && !param.modifiers.includes(modifier)) {
25751
+ throw new Error(
25752
+ `Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
25753
+ );
25754
+ }
25755
+ return;
25756
+ }
25757
+ throw new Error(
25758
+ `Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
25759
+ );
25760
+ }
25761
+ function emitOne(opts) {
25762
+ const { param, modifier, rawValue, paramName, context } = opts;
25763
+ if (modifier === "missing") {
25764
+ const missing = parseMissingValue(rawValue);
25765
+ return emitFieldMissingPredicate({
25766
+ jsonbPath: param.jsonbPath,
25767
+ missing
25768
+ });
25769
+ }
25770
+ const negate = modifier === "not";
25771
+ const effectiveModifier = negate ? void 0 : modifier;
25772
+ const inner = emitForType({
25773
+ paramType: param.type,
25774
+ jsonbPath: param.jsonbPath,
25775
+ rawValue,
25776
+ paramName,
25777
+ modifier: effectiveModifier,
25778
+ context
25779
+ });
25780
+ if (!negate) {
25781
+ return inner;
25782
+ }
25783
+ return { sql: `NOT (${inner.sql})`, params: inner.params };
25784
+ }
25785
+ function parseMissingValue(raw) {
25786
+ if (raw === "true") return true;
25787
+ if (raw === "false") return false;
25788
+ throw new Error(
25789
+ `:missing requires a value of "true" or "false"; received "${raw}".`
25790
+ );
25791
+ }
25792
+ function emitForType(opts) {
25793
+ switch (opts.paramType) {
25794
+ case "token":
25795
+ return emitTokenPredicate({
25796
+ jsonbPath: opts.jsonbPath,
25797
+ rawValue: opts.rawValue,
25798
+ paramName: opts.paramName,
25799
+ context: opts.context
25800
+ });
25801
+ case "date":
25802
+ return emitDatePredicate({
25803
+ jsonbPath: opts.jsonbPath,
25804
+ rawValue: opts.rawValue,
25805
+ paramName: opts.paramName,
25806
+ context: opts.context
25807
+ });
25808
+ case "reference":
25809
+ return emitReferencePredicate({
25810
+ jsonbPath: opts.jsonbPath,
25811
+ rawValue: opts.rawValue,
25812
+ paramName: opts.paramName,
25813
+ context: opts.context
25814
+ });
25815
+ case "string":
25816
+ return emitStringPredicate({
25817
+ jsonbPath: opts.jsonbPath,
25818
+ rawValue: opts.rawValue,
25819
+ paramName: opts.paramName,
25820
+ modifier: opts.modifier,
25821
+ context: opts.context
25822
+ });
25823
+ }
25824
+ }
25825
+ function combineSearchPredicates(opts) {
25826
+ const entries = parseQueryEntries(opts.query);
25827
+ const groupSqls = [];
25828
+ const allParams = [];
25829
+ let paramIdx = 0;
25830
+ for (const entry of entries) {
25831
+ const registered = findParam(opts.registeredParams, entry.code);
25832
+ if (!registered) {
25833
+ continue;
25834
+ }
25835
+ if (entry.modifier !== void 0) {
25836
+ checkModifierAllowed(entry.modifier, registered);
25837
+ }
25838
+ if (entry.values.length === 0) {
25839
+ continue;
25840
+ }
25841
+ const fragmentSqls = [];
25842
+ let valueIdx = 0;
25843
+ for (const rawValue of entry.values) {
25844
+ const paramName = `p${paramIdx}v${valueIdx}`;
25845
+ const frag = emitOne({
25846
+ param: registered,
25847
+ modifier: entry.modifier,
25848
+ rawValue,
25849
+ paramName,
25850
+ context: opts.context
25851
+ });
25852
+ fragmentSqls.push(frag.sql);
25853
+ for (const p of frag.params) {
25854
+ allParams.push(p);
25855
+ }
25856
+ valueIdx++;
25857
+ }
25858
+ paramIdx++;
25859
+ if (fragmentSqls.length === 1) {
25860
+ groupSqls.push(fragmentSqls[0]);
25861
+ } else {
25862
+ groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
25863
+ }
25864
+ }
25865
+ if (groupSqls.length === 0) {
25866
+ return { sql: "", params: [] };
25867
+ }
25868
+ return { sql: groupSqls.join(" AND "), params: allParams };
25869
+ }
25870
+
25871
+ // src/data/search/operations/generic-search-operation.ts
25485
25872
  var DEFAULT_LIMIT7 = 100;
25486
- function buildSearchPatientsByGeneralPractitionerSql() {
25487
- return [
25873
+ function buildGenericSearchSql(opts) {
25874
+ const lines = [
25488
25875
  "SELECT resource_id AS id, resource",
25489
25876
  "FROM resources",
25490
25877
  "WHERE tenant_id = :tenantId",
25491
25878
  " AND workspace_id = :workspaceId",
25492
- " AND resource_type = 'Patient'",
25493
- " AND deleted_at IS NULL",
25494
- ` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`,
25495
- "ORDER BY last_updated DESC",
25496
- "LIMIT :limit;"
25497
- ].join("\n");
25879
+ " AND resource_type = :resourceType",
25880
+ " AND deleted_at IS NULL"
25881
+ ];
25882
+ if (opts.combinedSql.length > 0) {
25883
+ lines.push(` AND (${opts.combinedSql})`);
25884
+ }
25885
+ lines.push("ORDER BY last_updated DESC");
25886
+ lines.push("LIMIT :limit;");
25887
+ return lines.join("\n");
25498
25888
  }
25499
- async function searchPatientsByGeneralPractitionerOperation(params) {
25500
- const { context, generalPractitionerReference } = params;
25501
- const { tenantId, workspaceId } = context;
25889
+ async function genericSearchOperation(params) {
25890
+ const { resourceType, tenantId, workspaceId, query, resolver } = params;
25502
25891
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
25503
25892
  const limit = params.limit ?? DEFAULT_LIMIT7;
25504
- const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
25505
- shape: { kind: "array-of-references", field: "generalPractitioner" },
25506
- reference: generalPractitionerReference,
25507
- tenantId,
25508
- workspaceId
25893
+ const registeredParams = resolver(resourceType, tenantId);
25894
+ const context = { tenantId, workspaceId, resourceType };
25895
+ const combined = combineSearchPredicates({
25896
+ query,
25897
+ registeredParams,
25898
+ context
25509
25899
  });
25510
- const sql = buildSearchPatientsByGeneralPractitionerSql();
25900
+ const sql = buildGenericSearchSql({ combinedSql: combined.sql });
25511
25901
  const queryParams = [
25512
25902
  { name: "tenantId", value: tenantId },
25513
25903
  { name: "workspaceId", value: workspaceId },
25514
- { name: "containmentRelative", value: containmentRelative },
25515
- { name: "containmentUrn", value: containmentUrn },
25516
- { name: "limit", value: limit }
25904
+ { name: "resourceType", value: resourceType },
25905
+ { name: "limit", value: limit },
25906
+ ...combined.params
25517
25907
  ];
25518
25908
  const rows = await runner.query(sql, queryParams);
25519
25909
  const entries = rows.map((row) => ({
@@ -25523,14 +25913,60 @@ async function searchPatientsByGeneralPractitionerOperation(params) {
25523
25913
  return { entries, total: entries.length };
25524
25914
  }
25525
25915
 
25526
- // src/data/rest-api/routes/data/patient/patient-list-route.ts
25527
- function singleStringQueryParam3(req, name) {
25528
- const v = req.query[name];
25529
- if (typeof v !== "string") {
25530
- return void 0;
25916
+ // src/data/search/registry/patient-search-parameters.ts
25917
+ var PATIENT_SEARCH_PARAMETERS = [
25918
+ { code: "gender", type: "token", jsonbPath: "$.gender" },
25919
+ { code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
25920
+ { code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
25921
+ {
25922
+ code: "name",
25923
+ type: "string",
25924
+ jsonbPath: "$.name[*].text",
25925
+ modifiers: ["exact", "contains", "missing", "not"]
25926
+ },
25927
+ {
25928
+ code: "family",
25929
+ type: "string",
25930
+ jsonbPath: "$.name[*].family",
25931
+ modifiers: ["exact", "contains", "missing", "not"]
25932
+ },
25933
+ {
25934
+ code: "given",
25935
+ type: "string",
25936
+ jsonbPath: "$.name[*].given",
25937
+ modifiers: ["exact", "contains", "missing", "not"]
25938
+ },
25939
+ { code: "active", type: "token", jsonbPath: "$.active" },
25940
+ { code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
25941
+ {
25942
+ code: "general-practitioner",
25943
+ type: "reference",
25944
+ jsonbPath: "$.generalPractitioner[*]"
25945
+ },
25946
+ {
25947
+ code: "organization",
25948
+ type: "reference",
25949
+ jsonbPath: "$.managingOrganization"
25531
25950
  }
25532
- const trimmed = v.trim();
25533
- return trimmed === "" ? void 0 : trimmed;
25951
+ ];
25952
+
25953
+ // src/data/search/registry/resolver.ts
25954
+ var STATIC_SEARCH_PARAMETER_MAP = {
25955
+ Patient: PATIENT_SEARCH_PARAMETERS
25956
+ };
25957
+ var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
25958
+ function getRegisteredSearchParameters(resourceType) {
25959
+ return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
25960
+ }
25961
+
25962
+ // src/data/rest-api/routes/data/patient/patient-list-route.ts
25963
+ var PATIENT_RESOURCE_TYPE = "Patient";
25964
+ function stripModifier(key) {
25965
+ const idx = key.indexOf(":");
25966
+ return idx === -1 ? key : key.slice(0, idx);
25967
+ }
25968
+ function isResultParameter(key) {
25969
+ return key.startsWith("_");
25534
25970
  }
25535
25971
  function sendInvalidSearch4003(res, diagnostics) {
25536
25972
  return res.status(400).json({
@@ -25538,41 +25974,93 @@ function sendInvalidSearch4003(res, diagnostics) {
25538
25974
  issue: [{ severity: "error", code: "invalid", diagnostics }]
25539
25975
  });
25540
25976
  }
25541
- async function listPatientsRoute(req, res) {
25542
- const generalPractitionerRef = singleStringQueryParam3(
25543
- req,
25544
- "general-practitioner"
25977
+ function extractSearchParamKeys(query) {
25978
+ const out = [];
25979
+ for (const rawKey of Object.keys(query)) {
25980
+ if (isResultParameter(rawKey)) {
25981
+ continue;
25982
+ }
25983
+ out.push({ rawKey, code: stripModifier(rawKey) });
25984
+ }
25985
+ return out;
25986
+ }
25987
+ function buildUnknownParamDiagnostics(unknownCodes) {
25988
+ const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
25989
+ const codes = unknownCodes.join(", ");
25990
+ const isPlural = unknownCodes.length !== 1;
25991
+ return [
25992
+ `Unknown search ${isPlural ? "parameters" : "parameter"} for Patient: ${codes}.`,
25993
+ `Valid codes: ${validCodes}.`
25994
+ ].join(" ");
25995
+ }
25996
+ function findMalformedReference(query, searchParamKeys) {
25997
+ const referenceCodes = new Set(
25998
+ getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
25545
25999
  );
25546
- if (generalPractitionerRef !== void 0) {
25547
- if (parseTypedReference(generalPractitionerRef) === void 0) {
25548
- return sendInvalidSearch4003(
25549
- res,
25550
- `?general-practitioner must be a typed reference like "Practitioner/<id>"; got "${generalPractitionerRef}".`
25551
- );
26000
+ for (const { rawKey, code } of searchParamKeys) {
26001
+ if (!referenceCodes.has(code)) {
26002
+ continue;
25552
26003
  }
25553
- const ctx = req.openhiContext;
25554
- try {
25555
- const result = await searchPatientsByGeneralPractitionerOperation({
25556
- context: ctx,
25557
- generalPractitionerReference: generalPractitionerRef
25558
- });
25559
- const bundle = buildSearchsetBundle(BASE_PATH.PATIENT, result.entries);
25560
- return res.json(bundle);
25561
- } catch (err) {
25562
- return sendOperationOutcome500(
25563
- res,
25564
- err,
25565
- "GET /Patient?general-practitioner= search error:"
25566
- );
26004
+ const raw = query[rawKey];
26005
+ const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
26006
+ for (const v of values) {
26007
+ const trimmed = v.trim();
26008
+ if (trimmed.length === 0) {
26009
+ continue;
26010
+ }
26011
+ if (parseTypedReference(trimmed) === void 0) {
26012
+ return { rawKey, value: trimmed };
26013
+ }
25567
26014
  }
25568
26015
  }
25569
- return handleListRoute({
25570
- req,
25571
- res,
25572
- basePath: BASE_PATH.PATIENT,
25573
- listOperation: listPatientsOperation,
25574
- errorLogContext: "GET /Patient list error:"
25575
- });
26016
+ return void 0;
26017
+ }
26018
+ async function listPatientsRoute(req, res) {
26019
+ const searchParamKeys = extractSearchParamKeys(
26020
+ req.query
26021
+ );
26022
+ if (searchParamKeys.length === 0) {
26023
+ return handleListRoute({
26024
+ req,
26025
+ res,
26026
+ basePath: BASE_PATH.PATIENT,
26027
+ listOperation: listPatientsOperation,
26028
+ errorLogContext: "GET /Patient list error:"
26029
+ });
26030
+ }
26031
+ const registered = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE);
26032
+ const validCodes = new Set(registered.map((p) => p.code));
26033
+ const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
26034
+ if (unknownCodes.length > 0) {
26035
+ return sendInvalidSearch4003(
26036
+ res,
26037
+ buildUnknownParamDiagnostics([...new Set(unknownCodes)])
26038
+ );
26039
+ }
26040
+ const malformedRef = findMalformedReference(
26041
+ req.query,
26042
+ searchParamKeys
26043
+ );
26044
+ if (malformedRef !== void 0) {
26045
+ return sendInvalidSearch4003(
26046
+ res,
26047
+ `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
26048
+ );
26049
+ }
26050
+ const ctx = req.openhiContext;
26051
+ try {
26052
+ const result = await genericSearchOperation({
26053
+ resourceType: PATIENT_RESOURCE_TYPE,
26054
+ tenantId: ctx.tenantId,
26055
+ workspaceId: ctx.workspaceId,
26056
+ query: req.query,
26057
+ resolver: defaultSearchParameterResolver
26058
+ });
26059
+ const bundle = buildSearchsetBundle(BASE_PATH.PATIENT, result.entries);
26060
+ return res.json(bundle);
26061
+ } catch (err) {
26062
+ return sendOperationOutcome500(res, err, "GET /Patient search error:");
26063
+ }
25576
26064
  }
25577
26065
 
25578
26066
  // src/data/operations/data/patient/patient-update-operation.ts
@@ -29401,7 +29889,7 @@ async function searchSchedulesByActorOperation(params) {
29401
29889
  }
29402
29890
 
29403
29891
  // src/data/rest-api/routes/data/schedule/schedule-list-route.ts
29404
- function singleStringQueryParam4(req, name) {
29892
+ function singleStringQueryParam3(req, name) {
29405
29893
  const v = req.query[name];
29406
29894
  if (typeof v !== "string") {
29407
29895
  return void 0;
@@ -29416,7 +29904,7 @@ function sendInvalidSearch4004(res, diagnostics) {
29416
29904
  });
29417
29905
  }
29418
29906
  async function listSchedulesRoute(req, res) {
29419
- const actorRef = singleStringQueryParam4(req, "actor");
29907
+ const actorRef = singleStringQueryParam3(req, "actor");
29420
29908
  if (actorRef !== void 0) {
29421
29909
  if (parseTypedReference(actorRef) === void 0) {
29422
29910
  return sendInvalidSearch4004(
@@ -33209,7 +33697,7 @@ async function searchTasksByRequesterOperation(params) {
33209
33697
  }
33210
33698
 
33211
33699
  // src/data/rest-api/routes/data/task/task-list-route.ts
33212
- function singleStringQueryParam5(req, name) {
33700
+ function singleStringQueryParam4(req, name) {
33213
33701
  const v = req.query[name];
33214
33702
  if (typeof v !== "string") {
33215
33703
  return void 0;
@@ -33224,8 +33712,8 @@ function sendInvalidSearch4005(res, diagnostics) {
33224
33712
  });
33225
33713
  }
33226
33714
  async function listTasksRoute(req, res) {
33227
- const ownerRef = singleStringQueryParam5(req, "owner");
33228
- const requesterRef = singleStringQueryParam5(req, "requester");
33715
+ const ownerRef = singleStringQueryParam4(req, "owner");
33716
+ const requesterRef = singleStringQueryParam4(req, "requester");
33229
33717
  if (ownerRef !== void 0 && requesterRef !== void 0) {
33230
33718
  return sendInvalidSearch4005(
33231
33719
  res,