@openhi/constructs 0.0.139 → 0.0.141

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.
@@ -5405,6 +5405,89 @@ async function listAppointmentsOperation(params) {
5405
5405
  );
5406
5406
  }
5407
5407
 
5408
+ // src/data/search/engine/reference-predicate.ts
5409
+ function buildOpenHiResourceUrn(opts) {
5410
+ return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
5411
+ }
5412
+ var REFERENCE_CONTAINMENT_SQL_FRAGMENT = "(resource @> :containmentRelative::jsonb OR resource @> :containmentUrn::jsonb)";
5413
+ function parseTypedReference(s) {
5414
+ const match = /^([A-Za-z][A-Za-z0-9_]*)\/([^\s/]+)$/.exec(s);
5415
+ if (!match) {
5416
+ return void 0;
5417
+ }
5418
+ return { resourceType: match[1], resourceId: match[2] };
5419
+ }
5420
+ function wrapReferenceInShape(shape, reference) {
5421
+ switch (shape.kind) {
5422
+ case "scalar":
5423
+ return { [shape.field]: { reference } };
5424
+ case "array-of-references":
5425
+ return { [shape.field]: [{ reference }] };
5426
+ case "array-of-objects":
5427
+ return { [shape.field]: [{ [shape.subfield]: { reference } }] };
5428
+ }
5429
+ }
5430
+ function buildReferenceContainmentPayload(params) {
5431
+ const parsed = parseTypedReference(params.reference);
5432
+ if (!parsed) {
5433
+ throw new Error(
5434
+ `Reference "${params.reference}" is not a valid typed reference (<ResourceType>/<id>).`
5435
+ );
5436
+ }
5437
+ const urn = buildOpenHiResourceUrn({
5438
+ tenantId: params.tenantId,
5439
+ workspaceId: params.workspaceId,
5440
+ resourceType: parsed.resourceType,
5441
+ resourceId: parsed.resourceId
5442
+ });
5443
+ return {
5444
+ containmentRelative: JSON.stringify(
5445
+ wrapReferenceInShape(params.shape, params.reference)
5446
+ ),
5447
+ containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
5448
+ };
5449
+ }
5450
+ function jsonbPathToReferenceShape(jsonbPath) {
5451
+ const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5452
+ if (scalar) {
5453
+ return { kind: "scalar", field: scalar[1] };
5454
+ }
5455
+ const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5456
+ if (arrayOfRefs) {
5457
+ return { kind: "array-of-references", field: arrayOfRefs[1] };
5458
+ }
5459
+ const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
5460
+ jsonbPath
5461
+ );
5462
+ if (arrayOfObjs) {
5463
+ return {
5464
+ kind: "array-of-objects",
5465
+ field: arrayOfObjs[1],
5466
+ subfield: arrayOfObjs[2]
5467
+ };
5468
+ }
5469
+ throw new Error(
5470
+ `Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5471
+ );
5472
+ }
5473
+ function emitReferencePredicate(opts) {
5474
+ const shape = jsonbPathToReferenceShape(opts.jsonbPath);
5475
+ const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
5476
+ shape,
5477
+ reference: opts.rawValue,
5478
+ tenantId: opts.context.tenantId,
5479
+ workspaceId: opts.context.workspaceId
5480
+ });
5481
+ const relName = `${opts.paramName}R`;
5482
+ const urnName = `${opts.paramName}U`;
5483
+ const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
5484
+ const params = [
5485
+ { name: relName, value: containmentRelative },
5486
+ { name: urnName, value: containmentUrn }
5487
+ ];
5488
+ return { sql, params };
5489
+ }
5490
+
5408
5491
  // src/data/postgres/data-api-postgres-query-runner.ts
5409
5492
  import {
5410
5493
  ExecuteStatementCommand,
@@ -5554,13 +5637,17 @@ function parseDateSearchValue(raw) {
5554
5637
  return { prefix: "eq", value: raw };
5555
5638
  }
5556
5639
  function flatJsonbExtract(jsonbPath) {
5557
- const match = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5558
- if (!match) {
5559
- throw new Error(
5560
- `Generic date predicate requires a flat top-level JSONPath like "$.fieldName"; received "${jsonbPath}".`
5561
- );
5640
+ const topLevel = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5641
+ if (topLevel) {
5642
+ return `resource->>'${topLevel[1]}'`;
5562
5643
  }
5563
- return `resource->>'${match[1]}'`;
5644
+ const nested = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5645
+ if (nested) {
5646
+ return `resource->'${nested[1]}'->>'${nested[2]}'`;
5647
+ }
5648
+ throw new Error(
5649
+ `Generic date predicate requires a scalar JSONPath like "$.fieldName" or "$.field.subfield"; received "${jsonbPath}".`
5650
+ );
5564
5651
  }
5565
5652
  function emitDatePredicate(opts) {
5566
5653
  const { jsonbPath, rawValue, paramName } = opts;
@@ -5596,129 +5683,95 @@ function emitFieldMissingPredicate(opts) {
5596
5683
  const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
5597
5684
  return { sql, params: [] };
5598
5685
  }
5599
- var DEFAULT_INTERVAL_PARAM_PREFIX = "intervalDateConstraint";
5600
- function intervalConstraintParamName(prefix, index) {
5601
- return `${prefix}${index}`;
5686
+
5687
+ // src/data/search/engine/string-predicate.ts
5688
+ var STRING_MODIFIERS = ["exact", "contains"];
5689
+ function isStringModifier(s) {
5690
+ return STRING_MODIFIERS.includes(s);
5602
5691
  }
5603
- function buildIntervalSingleSql(prefix, startExtract, endExtract, paramName) {
5604
- switch (prefix) {
5605
- case "eq":
5606
- return `(${startExtract} IS NOT NULL AND ${startExtract} = :${paramName})`;
5607
- case "gt":
5608
- return `(${endExtract} IS NULL OR ${endExtract} > :${paramName})`;
5609
- case "lt":
5610
- return `(${startExtract} IS NULL OR ${startExtract} < :${paramName})`;
5611
- case "ge":
5612
- return `(${endExtract} IS NULL OR ${endExtract} >= :${paramName})`;
5613
- case "le":
5614
- return `(${startExtract} IS NULL OR ${startExtract} <= :${paramName})`;
5615
- case "sa":
5616
- return `(${startExtract} IS NOT NULL AND ${startExtract} > :${paramName})`;
5617
- case "eb":
5618
- return `(${endExtract} IS NOT NULL AND ${endExtract} < :${paramName})`;
5619
- }
5620
- }
5621
- function buildIntervalDateSearchPredicateSql(constraints, opts) {
5622
- if (constraints.length === 0) {
5623
- return [];
5624
- }
5625
- const paramPrefix = opts.paramNamePrefix ?? DEFAULT_INTERVAL_PARAM_PREFIX;
5626
- const startExtract = flatJsonbExtract(opts.startPath);
5627
- const endExtract = flatJsonbExtract(opts.endPath);
5628
- const fragments = constraints.map(
5629
- (c, i) => buildIntervalSingleSql(
5630
- c.prefix,
5631
- startExtract,
5632
- endExtract,
5633
- intervalConstraintParamName(paramPrefix, i)
5634
- )
5692
+ function jsonbPathToStringShape(jsonbPath) {
5693
+ const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5694
+ if (scalar) {
5695
+ return { kind: "scalar", field: scalar[1] };
5696
+ }
5697
+ const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5698
+ if (arrayOfScalars) {
5699
+ return { kind: "array-of-scalars", field: arrayOfScalars[1] };
5700
+ }
5701
+ const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
5702
+ jsonbPath
5635
5703
  );
5636
- fragments.push(`(${startExtract} IS NOT NULL OR ${endExtract} IS NOT NULL)`);
5637
- return fragments;
5638
- }
5639
- function buildIntervalDateSearchPredicateParams(constraints, opts) {
5640
- const paramPrefix = opts?.paramNamePrefix ?? DEFAULT_INTERVAL_PARAM_PREFIX;
5641
- return constraints.map((c, i) => ({
5642
- name: intervalConstraintParamName(paramPrefix, i),
5643
- value: c.value
5644
- }));
5645
- }
5646
- var APPOINTMENT_DATE_SEARCH_PREFIXES = [
5647
- "gt",
5648
- "lt",
5649
- "ge",
5650
- "le",
5651
- "sa",
5652
- "eb"
5653
- ];
5654
- function isAppointmentDateSearchPrefix(s) {
5655
- return APPOINTMENT_DATE_SEARCH_PREFIXES.includes(
5656
- s
5704
+ if (arrayOfObjs) {
5705
+ return {
5706
+ kind: "array-of-objects",
5707
+ field: arrayOfObjs[1],
5708
+ subfield: arrayOfObjs[2]
5709
+ };
5710
+ }
5711
+ throw new Error(
5712
+ `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5657
5713
  );
5658
5714
  }
5659
- function buildAppointmentDateSearchPredicateSql(constraints) {
5660
- return buildIntervalDateSearchPredicateSql(constraints, {
5661
- startPath: "$.start",
5662
- endPath: "$.end",
5663
- paramNamePrefix: "apptDateConstraint"
5664
- });
5665
- }
5666
- function buildAppointmentDateSearchPredicateParams(constraints) {
5667
- return buildIntervalDateSearchPredicateParams(constraints, {
5668
- paramNamePrefix: "apptDateConstraint"
5669
- });
5670
- }
5671
-
5672
- // src/data/search/engine/reference-predicate.ts
5673
- function buildOpenHiResourceUrn(opts) {
5674
- return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
5715
+ function escapeLikePattern(value) {
5716
+ return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
5675
5717
  }
5676
- var REFERENCE_CONTAINMENT_SQL_FRAGMENT = "(resource @> :containmentRelative::jsonb OR resource @> :containmentUrn::jsonb)";
5677
- function parseTypedReference(s) {
5678
- const match = /^([A-Za-z][A-Za-z0-9_]*)\/([^\s/]+)$/.exec(s);
5679
- if (!match) {
5680
- return void 0;
5718
+ function buildLikePattern(value, modifier) {
5719
+ const escaped = escapeLikePattern(value);
5720
+ switch (modifier) {
5721
+ case "exact":
5722
+ return escaped;
5723
+ case "contains":
5724
+ return `%${escaped}%`;
5725
+ case void 0:
5726
+ return `${escaped}%`;
5681
5727
  }
5682
- return { resourceType: match[1], resourceId: match[2] };
5683
5728
  }
5684
- function wrapReferenceInShape(shape, reference) {
5729
+ function buildIlikeExtractSql(shape, paramName) {
5685
5730
  switch (shape.kind) {
5686
5731
  case "scalar":
5687
- return { [shape.field]: { reference } };
5688
- case "array-of-references":
5689
- return { [shape.field]: [{ reference }] };
5732
+ return `resource->>'${shape.field}' ILIKE :${paramName}`;
5733
+ case "array-of-scalars":
5734
+ return [
5735
+ "EXISTS (SELECT 1 FROM",
5736
+ `jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
5737
+ `WHERE s_elem.text_val ILIKE :${paramName})`
5738
+ ].join(" ");
5690
5739
  case "array-of-objects":
5691
- return { [shape.field]: [{ [shape.subfield]: { reference } }] };
5740
+ return [
5741
+ "EXISTS (SELECT 1 FROM",
5742
+ `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
5743
+ `WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
5744
+ ].join(" ");
5692
5745
  }
5693
5746
  }
5694
- function buildReferenceContainmentPayload(params) {
5695
- const parsed = parseTypedReference(params.reference);
5696
- if (!parsed) {
5747
+ function emitStringPredicate(opts) {
5748
+ const shape = jsonbPathToStringShape(opts.jsonbPath);
5749
+ const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
5750
+ const sql = buildIlikeExtractSql(shape, opts.paramName);
5751
+ const pattern = buildLikePattern(opts.rawValue, modifier);
5752
+ const params = [
5753
+ { name: opts.paramName, value: pattern }
5754
+ ];
5755
+ return { sql, params };
5756
+ }
5757
+ function checkModifier(modifier) {
5758
+ if (!isStringModifier(modifier)) {
5697
5759
  throw new Error(
5698
- `Reference "${params.reference}" is not a valid typed reference (<ResourceType>/<id>).`
5760
+ `String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
5699
5761
  );
5700
5762
  }
5701
- const urn = buildOpenHiResourceUrn({
5702
- tenantId: params.tenantId,
5703
- workspaceId: params.workspaceId,
5704
- resourceType: parsed.resourceType,
5705
- resourceId: parsed.resourceId
5706
- });
5707
- return {
5708
- containmentRelative: JSON.stringify(
5709
- wrapReferenceInShape(params.shape, params.reference)
5710
- ),
5711
- containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
5712
- };
5763
+ return modifier;
5713
5764
  }
5714
- function jsonbPathToReferenceShape(jsonbPath) {
5765
+
5766
+ // src/data/search/engine/token-predicate.ts
5767
+ function jsonbPathToTokenShape(jsonbPath) {
5715
5768
  const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5716
5769
  if (scalar) {
5717
5770
  return { kind: "scalar", field: scalar[1] };
5718
5771
  }
5719
- const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5720
- if (arrayOfRefs) {
5721
- return { kind: "array-of-references", field: arrayOfRefs[1] };
5772
+ const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5773
+ if (arrayOfScalars) {
5774
+ return { kind: "array-of-scalars", field: arrayOfScalars[1] };
5722
5775
  }
5723
5776
  const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
5724
5777
  jsonbPath
@@ -5731,240 +5784,408 @@ function jsonbPathToReferenceShape(jsonbPath) {
5731
5784
  };
5732
5785
  }
5733
5786
  throw new Error(
5734
- `Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5787
+ `Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5735
5788
  );
5736
5789
  }
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)`;
5790
+ function parseTokenValue(raw) {
5791
+ const idx = raw.indexOf("|");
5792
+ if (idx === -1) {
5793
+ return { code: raw };
5794
+ }
5795
+ const system = raw.slice(0, idx);
5796
+ const code = raw.slice(idx + 1);
5797
+ return system.length > 0 ? { system, code } : { code };
5798
+ }
5799
+ function wrapTokenInShape(shape, value) {
5800
+ switch (shape.kind) {
5801
+ case "scalar":
5802
+ return { [shape.field]: value };
5803
+ case "array-of-scalars":
5804
+ return { [shape.field]: [value] };
5805
+ case "array-of-objects":
5806
+ return { [shape.field]: [{ [shape.subfield]: value }] };
5807
+ }
5808
+ }
5809
+ function emitTokenPredicate(opts) {
5810
+ const shape = jsonbPathToTokenShape(opts.jsonbPath);
5811
+ const { code } = parseTokenValue(opts.rawValue);
5812
+ const payload = wrapTokenInShape(shape, code);
5813
+ const sql = `resource @> :${opts.paramName}::jsonb`;
5748
5814
  const params = [
5749
- { name: relName, value: containmentRelative },
5750
- { name: urnName, value: containmentUrn }
5815
+ { name: opts.paramName, value: JSON.stringify(payload) }
5751
5816
  ];
5752
5817
  return { sql, params };
5753
5818
  }
5754
5819
 
5755
- // src/data/operations/data/appointment/appointment-search-by-actor-operation.ts
5756
- var DEFAULT_LIMIT = 100;
5757
- function buildSearchAppointmentsByActorSql(opts) {
5758
- const datePredicates = buildAppointmentDateSearchPredicateSql(
5759
- opts?.dateConstraints ?? []
5760
- );
5761
- const lines = [
5762
- "SELECT resource_id AS id, resource",
5763
- "FROM resources",
5764
- "WHERE tenant_id = :tenantId",
5765
- " AND workspace_id = :workspaceId",
5766
- " AND resource_type = 'Appointment'",
5767
- " AND deleted_at IS NULL",
5768
- ` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`
5769
- ];
5770
- for (const fragment of datePredicates) {
5771
- lines.push(` AND ${fragment}`);
5820
+ // src/data/search/engine/combinator.ts
5821
+ function parseQueryKey(key) {
5822
+ const idx = key.indexOf(":");
5823
+ if (idx === -1) {
5824
+ return { code: key, modifier: void 0 };
5772
5825
  }
5773
- lines.push("ORDER BY last_updated DESC");
5774
- lines.push("LIMIT :limit;");
5775
- return lines.join("\n");
5826
+ return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
5776
5827
  }
5777
- async function searchAppointmentsByActorOperation(params) {
5778
- const { context, actorReference } = params;
5779
- const dateConstraints = params.dateConstraints ?? [];
5780
- const { tenantId, workspaceId } = context;
5781
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
5782
- const limit = params.limit ?? DEFAULT_LIMIT;
5783
- const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
5784
- shape: {
5785
- kind: "array-of-objects",
5786
- field: "participant",
5787
- subfield: "actor"
5788
- },
5789
- reference: actorReference,
5790
- tenantId,
5791
- workspaceId
5792
- });
5793
- const sql = buildSearchAppointmentsByActorSql({ dateConstraints });
5794
- const queryParams = [
5795
- { name: "tenantId", value: tenantId },
5796
- { name: "workspaceId", value: workspaceId },
5797
- { name: "containmentRelative", value: containmentRelative },
5798
- { name: "containmentUrn", value: containmentUrn },
5799
- { name: "limit", value: limit },
5800
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
5801
- ];
5802
- const rows = await runner.query(sql, queryParams);
5803
- const entries = rows.map((row) => ({
5804
- id: row.id,
5805
- resource: {
5806
- ...row.resource,
5807
- id: row.id
5828
+ function flattenQueryValues(raw) {
5829
+ const list = Array.isArray(raw) ? raw : [raw];
5830
+ const out = [];
5831
+ for (const v of list) {
5832
+ for (const piece of v.split(",")) {
5833
+ out.push(piece);
5808
5834
  }
5809
- }));
5810
- return { entries, total: entries.length };
5835
+ }
5836
+ return out;
5811
5837
  }
5812
-
5813
- // src/data/operations/data/appointment/appointment-search-by-date-operation.ts
5814
- var DEFAULT_LIMIT2 = 100;
5815
- function buildSearchAppointmentsByDateSql(opts) {
5816
- const datePredicates = buildAppointmentDateSearchPredicateSql(
5817
- opts.dateConstraints
5838
+ function parseQueryEntries(query) {
5839
+ const entries = [];
5840
+ for (const [rawKey, rawValue] of Object.entries(query)) {
5841
+ if (rawValue === void 0) continue;
5842
+ const { code, modifier } = parseQueryKey(rawKey);
5843
+ entries.push({
5844
+ code,
5845
+ modifier,
5846
+ values: flattenQueryValues(rawValue)
5847
+ });
5848
+ }
5849
+ return entries;
5850
+ }
5851
+ function findParam(params, code) {
5852
+ return params.find((p) => p.code === code);
5853
+ }
5854
+ function checkModifierAllowed(modifier, param) {
5855
+ const universal = modifier === "missing" || modifier === "not";
5856
+ const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
5857
+ if (universal || stringNative) {
5858
+ if (param.modifiers && !param.modifiers.includes(modifier)) {
5859
+ throw new Error(
5860
+ `Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
5861
+ );
5862
+ }
5863
+ return;
5864
+ }
5865
+ throw new Error(
5866
+ `Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
5818
5867
  );
5819
- const lines = [
5820
- "SELECT resource_id AS id, resource",
5821
- "FROM resources",
5822
- "WHERE tenant_id = :tenantId",
5823
- " AND workspace_id = :workspaceId",
5824
- " AND resource_type = 'Appointment'",
5825
- " AND deleted_at IS NULL"
5826
- ];
5827
- for (const fragment of datePredicates) {
5828
- lines.push(` AND ${fragment}`);
5868
+ }
5869
+ function emitOne(opts) {
5870
+ const { param, modifier, rawValue, paramName, context } = opts;
5871
+ if (modifier === "missing") {
5872
+ const missing = parseMissingValue(rawValue);
5873
+ return emitFieldMissingPredicate({
5874
+ jsonbPath: param.jsonbPath,
5875
+ missing
5876
+ });
5829
5877
  }
5830
- lines.push("ORDER BY last_updated DESC");
5831
- lines.push("LIMIT :limit;");
5832
- return lines.join("\n");
5878
+ const negate = modifier === "not";
5879
+ const effectiveModifier = negate ? void 0 : modifier;
5880
+ const inner = emitForType({
5881
+ paramType: param.type,
5882
+ jsonbPath: param.jsonbPath,
5883
+ rawValue,
5884
+ paramName,
5885
+ modifier: effectiveModifier,
5886
+ context
5887
+ });
5888
+ if (!negate) {
5889
+ return inner;
5890
+ }
5891
+ return { sql: `NOT (${inner.sql})`, params: inner.params };
5833
5892
  }
5834
- async function searchAppointmentsByDateOperation(params) {
5835
- const { context, dateConstraints } = params;
5836
- if (dateConstraints.length === 0) {
5837
- throw new Error(
5838
- "searchAppointmentsByDateOperation requires at least one dateConstraint"
5839
- );
5893
+ function parseMissingValue(raw) {
5894
+ if (raw === "true") return true;
5895
+ if (raw === "false") return false;
5896
+ throw new Error(
5897
+ `:missing requires a value of "true" or "false"; received "${raw}".`
5898
+ );
5899
+ }
5900
+ function emitForType(opts) {
5901
+ switch (opts.paramType) {
5902
+ case "token":
5903
+ return emitTokenPredicate({
5904
+ jsonbPath: opts.jsonbPath,
5905
+ rawValue: opts.rawValue,
5906
+ paramName: opts.paramName,
5907
+ context: opts.context
5908
+ });
5909
+ case "date":
5910
+ return emitDatePredicate({
5911
+ jsonbPath: opts.jsonbPath,
5912
+ rawValue: opts.rawValue,
5913
+ paramName: opts.paramName,
5914
+ context: opts.context
5915
+ });
5916
+ case "reference":
5917
+ return emitReferencePredicate({
5918
+ jsonbPath: opts.jsonbPath,
5919
+ rawValue: opts.rawValue,
5920
+ paramName: opts.paramName,
5921
+ context: opts.context
5922
+ });
5923
+ case "string":
5924
+ return emitStringPredicate({
5925
+ jsonbPath: opts.jsonbPath,
5926
+ rawValue: opts.rawValue,
5927
+ paramName: opts.paramName,
5928
+ modifier: opts.modifier,
5929
+ context: opts.context
5930
+ });
5840
5931
  }
5841
- const { tenantId, workspaceId } = context;
5842
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
5843
- const limit = params.limit ?? DEFAULT_LIMIT2;
5844
- const sql = buildSearchAppointmentsByDateSql({ dateConstraints });
5845
- const queryParams = [
5846
- { name: "tenantId", value: tenantId },
5847
- { name: "workspaceId", value: workspaceId },
5848
- { name: "limit", value: limit },
5849
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
5850
- ];
5851
- const rows = await runner.query(sql, queryParams);
5852
- const entries = rows.map((row) => ({
5853
- id: row.id,
5854
- resource: {
5855
- ...row.resource,
5856
- id: row.id
5932
+ }
5933
+ function combineSearchPredicates(opts) {
5934
+ const entries = parseQueryEntries(opts.query);
5935
+ const groupSqls = [];
5936
+ const allParams = [];
5937
+ let paramIdx = 0;
5938
+ for (const entry of entries) {
5939
+ const registered = findParam(opts.registeredParams, entry.code);
5940
+ if (!registered) {
5941
+ continue;
5857
5942
  }
5858
- }));
5859
- return { entries, total: entries.length };
5943
+ if (entry.modifier !== void 0) {
5944
+ checkModifierAllowed(entry.modifier, registered);
5945
+ }
5946
+ if (entry.values.length === 0) {
5947
+ continue;
5948
+ }
5949
+ const fragmentSqls = [];
5950
+ let valueIdx = 0;
5951
+ for (const rawValue of entry.values) {
5952
+ const paramName = `p${paramIdx}v${valueIdx}`;
5953
+ const frag = emitOne({
5954
+ param: registered,
5955
+ modifier: entry.modifier,
5956
+ rawValue,
5957
+ paramName,
5958
+ context: opts.context
5959
+ });
5960
+ fragmentSqls.push(frag.sql);
5961
+ for (const p of frag.params) {
5962
+ allParams.push(p);
5963
+ }
5964
+ valueIdx++;
5965
+ }
5966
+ paramIdx++;
5967
+ if (fragmentSqls.length === 1) {
5968
+ groupSqls.push(fragmentSqls[0]);
5969
+ } else {
5970
+ groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
5971
+ }
5972
+ }
5973
+ if (groupSqls.length === 0) {
5974
+ return { sql: "", params: [] };
5975
+ }
5976
+ return { sql: groupSqls.join(" AND "), params: allParams };
5860
5977
  }
5861
5978
 
5862
- // src/data/operations/data/appointment/appointment-search-by-patient-operation.ts
5863
- var DEFAULT_LIMIT3 = 100;
5864
- function buildSearchAppointmentsByPatientSql(opts) {
5865
- const datePredicates = buildAppointmentDateSearchPredicateSql(
5866
- opts?.dateConstraints ?? []
5867
- );
5979
+ // src/data/search/operations/generic-search-operation.ts
5980
+ var DEFAULT_LIMIT = 100;
5981
+ function buildGenericSearchSql(opts) {
5868
5982
  const lines = [
5869
5983
  "SELECT resource_id AS id, resource",
5870
5984
  "FROM resources",
5871
5985
  "WHERE tenant_id = :tenantId",
5872
5986
  " AND workspace_id = :workspaceId",
5873
- " AND resource_type = 'Appointment'",
5874
- " AND deleted_at IS NULL",
5875
- " AND (resource @> :containmentRelative::jsonb",
5876
- " OR resource @> :containmentUrn::jsonb)"
5987
+ " AND resource_type = :resourceType",
5988
+ " AND deleted_at IS NULL"
5877
5989
  ];
5878
- for (const fragment of datePredicates) {
5879
- lines.push(` AND ${fragment}`);
5990
+ if (opts.combinedSql.length > 0) {
5991
+ lines.push(` AND (${opts.combinedSql})`);
5880
5992
  }
5881
5993
  lines.push("ORDER BY last_updated DESC");
5882
5994
  lines.push("LIMIT :limit;");
5883
5995
  return lines.join("\n");
5884
5996
  }
5885
- async function searchAppointmentsByPatientOperation(params) {
5886
- const { context, patientId } = params;
5887
- const dateConstraints = params.dateConstraints ?? [];
5888
- const { tenantId, workspaceId } = context;
5997
+ async function genericSearchOperation(params) {
5998
+ const { resourceType, tenantId, workspaceId, query, resolver } = params;
5889
5999
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
5890
- const limit = params.limit ?? DEFAULT_LIMIT3;
5891
- const containmentRelative = JSON.stringify({
5892
- participant: [{ actor: { reference: `Patient/${patientId}` } }]
5893
- });
5894
- const containmentUrn = JSON.stringify({
5895
- participant: [
5896
- {
5897
- actor: {
5898
- reference: buildOpenHiResourceUrn({
5899
- tenantId,
5900
- workspaceId,
5901
- resourceType: "Patient",
5902
- resourceId: patientId
5903
- })
5904
- }
5905
- }
5906
- ]
6000
+ const limit = params.limit ?? DEFAULT_LIMIT;
6001
+ const registeredParams = resolver(resourceType, tenantId);
6002
+ const context = { tenantId, workspaceId, resourceType };
6003
+ const combined = combineSearchPredicates({
6004
+ query,
6005
+ registeredParams,
6006
+ context
5907
6007
  });
5908
- const sql = buildSearchAppointmentsByPatientSql({ dateConstraints });
6008
+ const sql = buildGenericSearchSql({ combinedSql: combined.sql });
5909
6009
  const queryParams = [
5910
6010
  { name: "tenantId", value: tenantId },
5911
6011
  { name: "workspaceId", value: workspaceId },
5912
- { name: "containmentRelative", value: containmentRelative },
5913
- { name: "containmentUrn", value: containmentUrn },
6012
+ { name: "resourceType", value: resourceType },
5914
6013
  { name: "limit", value: limit },
5915
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
6014
+ ...combined.params
5916
6015
  ];
5917
6016
  const rows = await runner.query(sql, queryParams);
5918
6017
  const entries = rows.map((row) => ({
5919
6018
  id: row.id,
5920
- resource: {
5921
- ...row.resource,
5922
- id: row.id
5923
- }
6019
+ resource: { ...row.resource, id: row.id }
5924
6020
  }));
5925
6021
  return { entries, total: entries.length };
5926
6022
  }
5927
6023
 
5928
- // src/data/rest-api/routes/data/appointment/appointment-list-route.ts
5929
- function singleStringQueryParam(req, name) {
5930
- const v = req.query[name];
5931
- if (typeof v !== "string") {
5932
- return void 0;
5933
- }
5934
- const trimmed = v.trim();
5935
- return trimmed === "" ? void 0 : trimmed;
5936
- }
5937
- function isError(v) {
5938
- return v.error !== void 0;
5939
- }
5940
- function parseAppointmentDateConstraints(req) {
5941
- const raw = req.query.date;
5942
- if (raw === void 0) {
5943
- return [];
5944
- }
5945
- const values = Array.isArray(raw) ? raw : [raw];
5946
- const out = [];
5947
- for (const v of values) {
5948
- if (typeof v !== "string") {
5949
- return { error: "Each ?date= value must be a string." };
5950
- }
5951
- const trimmed = v.trim();
5952
- if (trimmed === "") {
5953
- return { error: "?date= value must not be empty." };
5954
- }
5955
- const prefix = trimmed.slice(0, 2);
5956
- const datetime = trimmed.slice(2);
5957
- if (!isAppointmentDateSearchPrefix(prefix)) {
5958
- return {
5959
- error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${APPOINTMENT_DATE_SEARCH_PREFIXES.join(", ")}.`
5960
- };
5961
- }
5962
- if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
5963
- return { error: `Invalid datetime in ?date=${trimmed}.` };
5964
- }
5965
- out.push({ prefix, value: datetime });
6024
+ // src/data/search/registry/appointment-search-parameters.ts
6025
+ var APPOINTMENT_SEARCH_PARAMETERS = [
6026
+ { code: "status", type: "token", jsonbPath: "$.status" },
6027
+ { code: "date", type: "date", jsonbPath: "$.start" },
6028
+ {
6029
+ code: "patient",
6030
+ type: "reference",
6031
+ jsonbPath: "$.participant[*].actor"
6032
+ },
6033
+ {
6034
+ code: "actor",
6035
+ type: "reference",
6036
+ jsonbPath: "$.participant[*].actor"
6037
+ },
6038
+ {
6039
+ code: "practitioner",
6040
+ type: "reference",
6041
+ jsonbPath: "$.participant[*].actor"
6042
+ },
6043
+ { code: "service-type", type: "token", jsonbPath: "$.serviceType[*]" },
6044
+ {
6045
+ code: "service-category",
6046
+ type: "token",
6047
+ jsonbPath: "$.serviceCategory[*]"
6048
+ },
6049
+ { code: "specialty", type: "token", jsonbPath: "$.specialty[*]" },
6050
+ {
6051
+ code: "appointment-type",
6052
+ type: "token",
6053
+ jsonbPath: "$.appointmentType"
6054
+ },
6055
+ { code: "slot", type: "reference", jsonbPath: "$.slot[*]" }
6056
+ ];
6057
+
6058
+ // src/data/search/registry/encounter-search-parameters.ts
6059
+ var ENCOUNTER_SEARCH_PARAMETERS = [
6060
+ { code: "status", type: "token", jsonbPath: "$.status" },
6061
+ { code: "class", type: "token", jsonbPath: "$.class" },
6062
+ { code: "type", type: "token", jsonbPath: "$.type[*]" },
6063
+ { code: "subject", type: "reference", jsonbPath: "$.subject" },
6064
+ { code: "patient", type: "reference", jsonbPath: "$.subject" },
6065
+ {
6066
+ code: "participant",
6067
+ type: "reference",
6068
+ jsonbPath: "$.participant[*].individual"
6069
+ },
6070
+ { code: "date", type: "date", jsonbPath: "$.period.start" },
6071
+ {
6072
+ code: "service-provider",
6073
+ type: "reference",
6074
+ jsonbPath: "$.serviceProvider"
6075
+ },
6076
+ { code: "appointment", type: "reference", jsonbPath: "$.appointment[*]" },
6077
+ {
6078
+ code: "episode-of-care",
6079
+ type: "reference",
6080
+ jsonbPath: "$.episodeOfCare[*]"
5966
6081
  }
5967
- return out;
6082
+ ];
6083
+
6084
+ // src/data/search/registry/observation-search-parameters.ts
6085
+ var OBSERVATION_SEARCH_PARAMETERS = [
6086
+ { code: "status", type: "token", jsonbPath: "$.status" },
6087
+ { code: "category", type: "token", jsonbPath: "$.category[*]" },
6088
+ { code: "code", type: "token", jsonbPath: "$.code" },
6089
+ { code: "subject", type: "reference", jsonbPath: "$.subject" },
6090
+ { code: "patient", type: "reference", jsonbPath: "$.subject" },
6091
+ { code: "encounter", type: "reference", jsonbPath: "$.encounter" },
6092
+ { code: "performer", type: "reference", jsonbPath: "$.performer[*]" },
6093
+ { code: "date", type: "date", jsonbPath: "$.effectiveDateTime" },
6094
+ {
6095
+ code: "value-string",
6096
+ type: "string",
6097
+ jsonbPath: "$.valueString",
6098
+ modifiers: ["exact", "contains", "missing", "not"]
6099
+ },
6100
+ { code: "identifier", type: "token", jsonbPath: "$.identifier[*]" },
6101
+ { code: "based-on", type: "reference", jsonbPath: "$.basedOn[*]" },
6102
+ { code: "part-of", type: "reference", jsonbPath: "$.partOf[*]" }
6103
+ ];
6104
+
6105
+ // src/data/search/registry/patient-search-parameters.ts
6106
+ var PATIENT_SEARCH_PARAMETERS = [
6107
+ { code: "gender", type: "token", jsonbPath: "$.gender" },
6108
+ { code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
6109
+ { code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
6110
+ {
6111
+ code: "name",
6112
+ type: "string",
6113
+ jsonbPath: "$.name[*].text",
6114
+ modifiers: ["exact", "contains", "missing", "not"]
6115
+ },
6116
+ {
6117
+ code: "family",
6118
+ type: "string",
6119
+ jsonbPath: "$.name[*].family",
6120
+ modifiers: ["exact", "contains", "missing", "not"]
6121
+ },
6122
+ {
6123
+ code: "given",
6124
+ type: "string",
6125
+ jsonbPath: "$.name[*].given",
6126
+ modifiers: ["exact", "contains", "missing", "not"]
6127
+ },
6128
+ { code: "active", type: "token", jsonbPath: "$.active" },
6129
+ { code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
6130
+ {
6131
+ code: "general-practitioner",
6132
+ type: "reference",
6133
+ jsonbPath: "$.generalPractitioner[*]"
6134
+ },
6135
+ {
6136
+ code: "organization",
6137
+ type: "reference",
6138
+ jsonbPath: "$.managingOrganization"
6139
+ }
6140
+ ];
6141
+
6142
+ // src/data/search/registry/procedure-search-parameters.ts
6143
+ var PROCEDURE_SEARCH_PARAMETERS = [
6144
+ { code: "status", type: "token", jsonbPath: "$.status" },
6145
+ { code: "category", type: "token", jsonbPath: "$.category" },
6146
+ { code: "code", type: "token", jsonbPath: "$.code" },
6147
+ { code: "subject", type: "reference", jsonbPath: "$.subject" },
6148
+ { code: "patient", type: "reference", jsonbPath: "$.subject" },
6149
+ { code: "encounter", type: "reference", jsonbPath: "$.encounter" },
6150
+ {
6151
+ code: "performer",
6152
+ type: "reference",
6153
+ jsonbPath: "$.performer[*].actor"
6154
+ },
6155
+ { code: "date", type: "date", jsonbPath: "$.performedDateTime" },
6156
+ { code: "location", type: "reference", jsonbPath: "$.location" },
6157
+ { code: "identifier", type: "token", jsonbPath: "$.identifier[*]" },
6158
+ { code: "based-on", type: "reference", jsonbPath: "$.basedOn[*]" },
6159
+ { code: "part-of", type: "reference", jsonbPath: "$.partOf[*]" },
6160
+ { code: "reason-code", type: "token", jsonbPath: "$.reasonCode[*]" },
6161
+ {
6162
+ code: "reason-reference",
6163
+ type: "reference",
6164
+ jsonbPath: "$.reasonReference[*]"
6165
+ }
6166
+ ];
6167
+
6168
+ // src/data/search/registry/resolver.ts
6169
+ var STATIC_SEARCH_PARAMETER_MAP = {
6170
+ Appointment: APPOINTMENT_SEARCH_PARAMETERS,
6171
+ Encounter: ENCOUNTER_SEARCH_PARAMETERS,
6172
+ Observation: OBSERVATION_SEARCH_PARAMETERS,
6173
+ Patient: PATIENT_SEARCH_PARAMETERS,
6174
+ Procedure: PROCEDURE_SEARCH_PARAMETERS
6175
+ };
6176
+ var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
6177
+ function getRegisteredSearchParameters(resourceType) {
6178
+ return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
6179
+ }
6180
+
6181
+ // src/data/rest-api/routes/data/appointment/appointment-list-route.ts
6182
+ var APPOINTMENT_RESOURCE_TYPE = "Appointment";
6183
+ function stripModifier(key) {
6184
+ const idx = key.indexOf(":");
6185
+ return idx === -1 ? key : key.slice(0, idx);
6186
+ }
6187
+ function isResultParameter(key) {
6188
+ return key.startsWith("_");
5968
6189
  }
5969
6190
  function sendInvalidSearch400(res, diagnostics) {
5970
6191
  return res.status(400).json({
@@ -5972,95 +6193,93 @@ function sendInvalidSearch400(res, diagnostics) {
5972
6193
  issue: [{ severity: "error", code: "invalid", diagnostics }]
5973
6194
  });
5974
6195
  }
6196
+ function extractSearchParamKeys(query) {
6197
+ const out = [];
6198
+ for (const rawKey of Object.keys(query)) {
6199
+ if (isResultParameter(rawKey)) {
6200
+ continue;
6201
+ }
6202
+ out.push({ rawKey, code: stripModifier(rawKey) });
6203
+ }
6204
+ return out;
6205
+ }
6206
+ function buildUnknownParamDiagnostics(unknownCodes) {
6207
+ const validCodes = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
6208
+ const codes = unknownCodes.join(", ");
6209
+ const isPlural = unknownCodes.length !== 1;
6210
+ return [
6211
+ `Unknown search ${isPlural ? "parameters" : "parameter"} for Appointment: ${codes}.`,
6212
+ `Valid codes: ${validCodes}.`
6213
+ ].join(" ");
6214
+ }
6215
+ function findMalformedReference(query, searchParamKeys) {
6216
+ const referenceCodes = new Set(
6217
+ getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
6218
+ );
6219
+ for (const { rawKey, code } of searchParamKeys) {
6220
+ if (!referenceCodes.has(code)) {
6221
+ continue;
6222
+ }
6223
+ const raw = query[rawKey];
6224
+ const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
6225
+ for (const v of values) {
6226
+ const trimmed = v.trim();
6227
+ if (trimmed.length === 0) {
6228
+ continue;
6229
+ }
6230
+ if (parseTypedReference(trimmed) === void 0) {
6231
+ return { rawKey, value: trimmed };
6232
+ }
6233
+ }
6234
+ }
6235
+ return void 0;
6236
+ }
5975
6237
  async function listAppointmentsRoute(req, res) {
5976
- const patientId = singleStringQueryParam(req, "patient");
5977
- const actorRef = singleStringQueryParam(req, "actor");
5978
- const parsed = parseAppointmentDateConstraints(req);
5979
- if (isError(parsed)) {
5980
- return sendInvalidSearch400(res, parsed.error);
6238
+ const searchParamKeys = extractSearchParamKeys(
6239
+ req.query
6240
+ );
6241
+ if (searchParamKeys.length === 0) {
6242
+ return handleListRoute({
6243
+ req,
6244
+ res,
6245
+ basePath: BASE_PATH.APPOINTMENT,
6246
+ listOperation: listAppointmentsOperation,
6247
+ errorLogContext: "GET /Appointment list error:"
6248
+ });
5981
6249
  }
5982
- const dateConstraints = parsed;
5983
- if (patientId !== void 0 && actorRef !== void 0) {
6250
+ const registered = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE);
6251
+ const validCodes = new Set(registered.map((p) => p.code));
6252
+ const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
6253
+ if (unknownCodes.length > 0) {
5984
6254
  return sendInvalidSearch400(
5985
6255
  res,
5986
- "?patient= and ?actor= cannot be combined on the same request."
6256
+ buildUnknownParamDiagnostics([...new Set(unknownCodes)])
5987
6257
  );
5988
6258
  }
5989
- if (actorRef !== void 0) {
5990
- if (parseTypedReference(actorRef) === void 0) {
5991
- return sendInvalidSearch400(
5992
- res,
5993
- `?actor must be a typed reference like "Practitioner/<id>"; got "${actorRef}".`
5994
- );
5995
- }
5996
- const ctx = req.openhiContext;
5997
- try {
5998
- const result = await searchAppointmentsByActorOperation({
5999
- context: ctx,
6000
- actorReference: actorRef,
6001
- dateConstraints
6002
- });
6003
- const bundle = buildSearchsetBundle(
6004
- BASE_PATH.APPOINTMENT,
6005
- result.entries
6006
- );
6007
- return res.json(bundle);
6008
- } catch (err) {
6009
- return sendOperationOutcome500(
6010
- res,
6011
- err,
6012
- "GET /Appointment?actor= search error:"
6013
- );
6014
- }
6015
- }
6016
- if (patientId !== void 0) {
6017
- const ctx = req.openhiContext;
6018
- try {
6019
- const result = await searchAppointmentsByPatientOperation({
6020
- context: ctx,
6021
- patientId,
6022
- dateConstraints
6023
- });
6024
- const bundle = buildSearchsetBundle(
6025
- BASE_PATH.APPOINTMENT,
6026
- result.entries
6027
- );
6028
- return res.json(bundle);
6029
- } catch (err) {
6030
- return sendOperationOutcome500(
6031
- res,
6032
- err,
6033
- "GET /Appointment?patient= search error:"
6034
- );
6035
- }
6259
+ const malformedRef = findMalformedReference(
6260
+ req.query,
6261
+ searchParamKeys
6262
+ );
6263
+ if (malformedRef !== void 0) {
6264
+ return sendInvalidSearch400(
6265
+ res,
6266
+ `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
6267
+ );
6036
6268
  }
6037
- if (dateConstraints.length > 0) {
6038
- const ctx = req.openhiContext;
6039
- try {
6040
- const result = await searchAppointmentsByDateOperation({
6041
- context: ctx,
6042
- dateConstraints
6043
- });
6044
- const bundle = buildSearchsetBundle(
6045
- BASE_PATH.APPOINTMENT,
6046
- result.entries
6047
- );
6048
- return res.json(bundle);
6049
- } catch (err) {
6050
- return sendOperationOutcome500(
6051
- res,
6052
- err,
6053
- "GET /Appointment?date= search error:"
6054
- );
6055
- }
6269
+ const ctx = req.openhiContext;
6270
+ try {
6271
+ const result = await genericSearchOperation({
6272
+ resourceType: APPOINTMENT_RESOURCE_TYPE,
6273
+ tenantId: ctx.tenantId,
6274
+ workspaceId: ctx.workspaceId,
6275
+ query: req.query,
6276
+ resolver: defaultSearchParameterResolver
6277
+ });
6278
+ const bundle = buildSearchsetBundle(BASE_PATH.APPOINTMENT, result.entries);
6279
+ return res.json(bundle);
6280
+ } catch (err) {
6281
+ return sendOperationOutcome500(res, err, "GET /Appointment search error:");
6056
6282
  }
6057
- return handleListRoute({
6058
- req,
6059
- res,
6060
- basePath: BASE_PATH.APPOINTMENT,
6061
- listOperation: listAppointmentsOperation,
6062
- errorLogContext: "GET /Appointment list error:"
6063
- });
6064
6283
  }
6065
6284
 
6066
6285
  // src/data/operations/data/appointment/appointment-update-operation.ts
@@ -13568,353 +13787,108 @@ async function listEncountersOperation(params) {
13568
13787
  );
13569
13788
  }
13570
13789
 
13571
- // src/data/operations/data/encounter/encounter-period-search-predicate.ts
13572
- var PERIOD_SEARCH_PREFIXES = [
13573
- "gt",
13574
- "lt",
13575
- "ge",
13576
- "le",
13577
- "sa",
13578
- "eb"
13579
- ];
13580
- function isPeriodSearchPrefix(s) {
13581
- return PERIOD_SEARCH_PREFIXES.includes(s);
13790
+ // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
13791
+ var ENCOUNTER_RESOURCE_TYPE = "Encounter";
13792
+ function stripModifier2(key) {
13793
+ const idx = key.indexOf(":");
13794
+ return idx === -1 ? key : key.slice(0, idx);
13582
13795
  }
13583
- var PERIOD_START = "resource->'period'->>'start'";
13584
- var PERIOD_END = "resource->'period'->>'end'";
13585
- var HAS_ANY_BOUND_GUARD = `(${PERIOD_START} IS NOT NULL OR ${PERIOD_END} IS NOT NULL)`;
13586
- function buildSinglePredicateSql(prefix, paramName) {
13587
- switch (prefix) {
13588
- case "gt":
13589
- return `(${PERIOD_END} IS NULL OR ${PERIOD_END} > :${paramName})`;
13590
- case "lt":
13591
- return `(${PERIOD_START} IS NULL OR ${PERIOD_START} < :${paramName})`;
13592
- case "ge":
13593
- return `(${PERIOD_END} IS NULL OR ${PERIOD_END} >= :${paramName})`;
13594
- case "le":
13595
- return `(${PERIOD_START} IS NULL OR ${PERIOD_START} <= :${paramName})`;
13596
- case "sa":
13597
- return `(${PERIOD_START} IS NOT NULL AND ${PERIOD_START} > :${paramName})`;
13598
- case "eb":
13599
- return `(${PERIOD_END} IS NOT NULL AND ${PERIOD_END} < :${paramName})`;
13600
- }
13796
+ function isResultParameter2(key) {
13797
+ return key.startsWith("_");
13601
13798
  }
13602
- function periodConstraintParamName(index) {
13603
- return `periodConstraint${index}`;
13799
+ function sendInvalidSearch4002(res, diagnostics) {
13800
+ return res.status(400).json({
13801
+ resourceType: "OperationOutcome",
13802
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
13803
+ });
13604
13804
  }
13605
- function buildPeriodSearchPredicateSql(constraints) {
13606
- if (constraints.length === 0) {
13607
- return [];
13805
+ function extractSearchParamKeys2(query) {
13806
+ const out = [];
13807
+ for (const rawKey of Object.keys(query)) {
13808
+ if (isResultParameter2(rawKey)) {
13809
+ continue;
13810
+ }
13811
+ out.push({ rawKey, code: stripModifier2(rawKey) });
13608
13812
  }
13609
- const fragments = constraints.map(
13610
- (c, i) => buildSinglePredicateSql(c.prefix, periodConstraintParamName(i))
13611
- );
13612
- fragments.push(HAS_ANY_BOUND_GUARD);
13613
- return fragments;
13813
+ return out;
13614
13814
  }
13615
- function buildPeriodSearchPredicateParams(constraints) {
13616
- return constraints.map((c, i) => ({
13617
- name: periodConstraintParamName(i),
13618
- value: c.value
13619
- }));
13815
+ function buildUnknownParamDiagnostics2(unknownCodes) {
13816
+ const validCodes = getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
13817
+ const codes = unknownCodes.join(", ");
13818
+ const isPlural = unknownCodes.length !== 1;
13819
+ return [
13820
+ `Unknown search ${isPlural ? "parameters" : "parameter"} for Encounter: ${codes}.`,
13821
+ `Valid codes: ${validCodes}.`
13822
+ ].join(" ");
13620
13823
  }
13621
-
13622
- // src/data/operations/data/encounter/encounter-search-by-date-operation.ts
13623
- var DEFAULT_LIMIT4 = 100;
13624
- function buildSearchEncountersByDateSql(opts) {
13625
- const periodPredicates = buildPeriodSearchPredicateSql(
13626
- opts.periodConstraints
13824
+ function findMalformedReference2(query, searchParamKeys) {
13825
+ const referenceCodes = new Set(
13826
+ getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
13627
13827
  );
13628
- const lines = [
13629
- "SELECT resource_id AS id, resource",
13630
- "FROM resources",
13631
- "WHERE tenant_id = :tenantId",
13632
- " AND workspace_id = :workspaceId",
13633
- " AND resource_type = 'Encounter'",
13634
- " AND deleted_at IS NULL"
13635
- ];
13636
- for (const fragment of periodPredicates) {
13637
- lines.push(` AND ${fragment}`);
13638
- }
13639
- lines.push("ORDER BY last_updated DESC");
13640
- lines.push("LIMIT :limit;");
13641
- return lines.join("\n");
13642
- }
13643
- async function searchEncountersByDateOperation(params) {
13644
- const { context, periodConstraints } = params;
13645
- if (periodConstraints.length === 0) {
13646
- throw new Error(
13647
- "searchEncountersByDateOperation requires at least one periodConstraint"
13648
- );
13649
- }
13650
- const { tenantId, workspaceId } = context;
13651
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
13652
- const limit = params.limit ?? DEFAULT_LIMIT4;
13653
- const sql = buildSearchEncountersByDateSql({ periodConstraints });
13654
- const queryParams = [
13655
- { name: "tenantId", value: tenantId },
13656
- { name: "workspaceId", value: workspaceId },
13657
- { name: "limit", value: limit },
13658
- ...buildPeriodSearchPredicateParams(periodConstraints)
13659
- ];
13660
- const rows = await runner.query(sql, queryParams);
13661
- const entries = rows.map((row) => ({
13662
- id: row.id,
13663
- resource: {
13664
- ...row.resource,
13665
- id: row.id
13666
- }
13667
- }));
13668
- return { entries, total: entries.length };
13669
- }
13670
-
13671
- // src/data/operations/data/encounter/encounter-search-by-participant-operation.ts
13672
- var DEFAULT_LIMIT5 = 100;
13673
- function buildSearchEncountersByParticipantSql(opts) {
13674
- const periodPredicates = buildPeriodSearchPredicateSql(
13675
- opts?.periodConstraints ?? []
13676
- );
13677
- const lines = [
13678
- "SELECT resource_id AS id, resource",
13679
- "FROM resources",
13680
- "WHERE tenant_id = :tenantId",
13681
- " AND workspace_id = :workspaceId",
13682
- " AND resource_type = 'Encounter'",
13683
- " AND deleted_at IS NULL",
13684
- ` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`
13685
- ];
13686
- for (const fragment of periodPredicates) {
13687
- lines.push(` AND ${fragment}`);
13688
- }
13689
- lines.push("ORDER BY last_updated DESC");
13690
- lines.push("LIMIT :limit;");
13691
- return lines.join("\n");
13692
- }
13693
- async function searchEncountersByParticipantOperation(params) {
13694
- const { context, participantReference } = params;
13695
- const periodConstraints = params.periodConstraints ?? [];
13696
- const { tenantId, workspaceId } = context;
13697
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
13698
- const limit = params.limit ?? DEFAULT_LIMIT5;
13699
- const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
13700
- shape: {
13701
- kind: "array-of-objects",
13702
- field: "participant",
13703
- subfield: "individual"
13704
- },
13705
- reference: participantReference,
13706
- tenantId,
13707
- workspaceId
13708
- });
13709
- const sql = buildSearchEncountersByParticipantSql({ periodConstraints });
13710
- const queryParams = [
13711
- { name: "tenantId", value: tenantId },
13712
- { name: "workspaceId", value: workspaceId },
13713
- { name: "containmentRelative", value: containmentRelative },
13714
- { name: "containmentUrn", value: containmentUrn },
13715
- { name: "limit", value: limit },
13716
- ...buildPeriodSearchPredicateParams(periodConstraints)
13717
- ];
13718
- const rows = await runner.query(sql, queryParams);
13719
- const entries = rows.map((row) => ({
13720
- id: row.id,
13721
- resource: {
13722
- ...row.resource,
13723
- id: row.id
13724
- }
13725
- }));
13726
- return { entries, total: entries.length };
13727
- }
13728
-
13729
- // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
13730
- var DEFAULT_LIMIT6 = 100;
13731
- function buildSearchEncountersByPatientSql(opts) {
13732
- const periodPredicates = buildPeriodSearchPredicateSql(
13733
- opts?.periodConstraints ?? []
13734
- );
13735
- const lines = [
13736
- "SELECT resource_id AS id, resource",
13737
- "FROM resources",
13738
- "WHERE tenant_id = :tenantId",
13739
- " AND workspace_id = :workspaceId",
13740
- " AND resource_type = 'Encounter'",
13741
- " AND deleted_at IS NULL",
13742
- " AND (resource @> :containmentRelative::jsonb",
13743
- " OR resource @> :containmentUrn::jsonb)"
13744
- ];
13745
- for (const fragment of periodPredicates) {
13746
- lines.push(` AND ${fragment}`);
13747
- }
13748
- lines.push("ORDER BY last_updated DESC");
13749
- lines.push("LIMIT :limit;");
13750
- return lines.join("\n");
13751
- }
13752
- async function searchEncountersByPatientOperation(params) {
13753
- const { context, patientId } = params;
13754
- const periodConstraints = params.periodConstraints ?? [];
13755
- const { tenantId, workspaceId } = context;
13756
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
13757
- const limit = params.limit ?? DEFAULT_LIMIT6;
13758
- const containmentRelative = JSON.stringify({
13759
- subject: { reference: `Patient/${patientId}` }
13760
- });
13761
- const containmentUrn = JSON.stringify({
13762
- subject: {
13763
- reference: buildOpenHiResourceUrn({
13764
- tenantId,
13765
- workspaceId,
13766
- resourceType: "Patient",
13767
- resourceId: patientId
13768
- })
13769
- }
13770
- });
13771
- const sql = buildSearchEncountersByPatientSql({ periodConstraints });
13772
- const queryParams = [
13773
- { name: "tenantId", value: tenantId },
13774
- { name: "workspaceId", value: workspaceId },
13775
- { name: "containmentRelative", value: containmentRelative },
13776
- { name: "containmentUrn", value: containmentUrn },
13777
- { name: "limit", value: limit },
13778
- ...buildPeriodSearchPredicateParams(periodConstraints)
13779
- ];
13780
- const rows = await runner.query(sql, queryParams);
13781
- const entries = rows.map((row) => ({
13782
- id: row.id,
13783
- resource: {
13784
- ...row.resource,
13785
- id: row.id
13786
- }
13787
- }));
13788
- return { entries, total: entries.length };
13789
- }
13790
-
13791
- // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
13792
- function singleStringQueryParam2(req, name) {
13793
- const v = req.query[name];
13794
- if (typeof v !== "string") {
13795
- return void 0;
13796
- }
13797
- const trimmed = v.trim();
13798
- return trimmed === "" ? void 0 : trimmed;
13799
- }
13800
- function isError2(v) {
13801
- return v.error !== void 0;
13802
- }
13803
- function parseEncounterDateConstraints(req) {
13804
- const raw = req.query.date;
13805
- if (raw === void 0) {
13806
- return [];
13807
- }
13808
- const values = Array.isArray(raw) ? raw : [raw];
13809
- const out = [];
13810
- for (const v of values) {
13811
- if (typeof v !== "string") {
13812
- return { error: "Each ?date= value must be a string." };
13813
- }
13814
- const trimmed = v.trim();
13815
- if (trimmed === "") {
13816
- return { error: "?date= value must not be empty." };
13817
- }
13818
- const prefix = trimmed.slice(0, 2);
13819
- const datetime = trimmed.slice(2);
13820
- if (!isPeriodSearchPrefix(prefix)) {
13821
- return {
13822
- error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${PERIOD_SEARCH_PREFIXES.join(", ")}.`
13823
- };
13828
+ for (const { rawKey, code } of searchParamKeys) {
13829
+ if (!referenceCodes.has(code)) {
13830
+ continue;
13824
13831
  }
13825
- if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
13826
- return { error: `Invalid datetime in ?date=${trimmed}.` };
13832
+ const raw = query[rawKey];
13833
+ const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
13834
+ for (const v of values) {
13835
+ const trimmed = v.trim();
13836
+ if (trimmed.length === 0) {
13837
+ continue;
13838
+ }
13839
+ if (parseTypedReference(trimmed) === void 0) {
13840
+ return { rawKey, value: trimmed };
13841
+ }
13827
13842
  }
13828
- out.push({ prefix, value: datetime });
13829
13843
  }
13830
- return out;
13831
- }
13832
- function sendInvalidSearch4002(res, diagnostics) {
13833
- return res.status(400).json({
13834
- resourceType: "OperationOutcome",
13835
- issue: [{ severity: "error", code: "invalid", diagnostics }]
13836
- });
13844
+ return void 0;
13837
13845
  }
13838
13846
  async function listEncountersRoute(req, res) {
13839
- const patientId = singleStringQueryParam2(req, "patient");
13840
- const participantRef = singleStringQueryParam2(req, "participant");
13841
- const parsed = parseEncounterDateConstraints(req);
13842
- if (isError2(parsed)) {
13843
- return sendInvalidSearch4002(res, parsed.error);
13844
- }
13845
- const periodConstraints = parsed;
13846
- if (patientId !== void 0 && participantRef !== void 0) {
13847
+ const searchParamKeys = extractSearchParamKeys2(
13848
+ req.query
13849
+ );
13850
+ if (searchParamKeys.length === 0) {
13851
+ return handleListRoute({
13852
+ req,
13853
+ res,
13854
+ basePath: BASE_PATH.ENCOUNTER,
13855
+ listOperation: listEncountersOperation,
13856
+ errorLogContext: "GET /Encounter list error:"
13857
+ });
13858
+ }
13859
+ const registered = getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE);
13860
+ const validCodes = new Set(registered.map((p) => p.code));
13861
+ const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
13862
+ if (unknownCodes.length > 0) {
13847
13863
  return sendInvalidSearch4002(
13848
13864
  res,
13849
- "?patient= and ?participant= cannot be combined on the same request."
13865
+ buildUnknownParamDiagnostics2([...new Set(unknownCodes)])
13850
13866
  );
13851
13867
  }
13852
- if (participantRef !== void 0) {
13853
- if (parseTypedReference(participantRef) === void 0) {
13854
- return sendInvalidSearch4002(
13855
- res,
13856
- `?participant must be a typed reference like "Practitioner/<id>"; got "${participantRef}".`
13857
- );
13858
- }
13859
- const ctx = req.openhiContext;
13860
- try {
13861
- const result = await searchEncountersByParticipantOperation({
13862
- context: ctx,
13863
- participantReference: participantRef,
13864
- periodConstraints
13865
- });
13866
- const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13867
- return res.json(bundle);
13868
- } catch (err) {
13869
- return sendOperationOutcome500(
13870
- res,
13871
- err,
13872
- "GET /Encounter?participant= search error:"
13873
- );
13874
- }
13875
- }
13876
- if (patientId !== void 0) {
13877
- const ctx = req.openhiContext;
13878
- try {
13879
- const result = await searchEncountersByPatientOperation({
13880
- context: ctx,
13881
- patientId,
13882
- periodConstraints
13883
- });
13884
- const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13885
- return res.json(bundle);
13886
- } catch (err) {
13887
- return sendOperationOutcome500(
13888
- res,
13889
- err,
13890
- "GET /Encounter?patient= search error:"
13891
- );
13892
- }
13868
+ const malformedRef = findMalformedReference2(
13869
+ req.query,
13870
+ searchParamKeys
13871
+ );
13872
+ if (malformedRef !== void 0) {
13873
+ return sendInvalidSearch4002(
13874
+ res,
13875
+ `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
13876
+ );
13893
13877
  }
13894
- if (periodConstraints.length > 0) {
13895
- const ctx = req.openhiContext;
13896
- try {
13897
- const result = await searchEncountersByDateOperation({
13898
- context: ctx,
13899
- periodConstraints
13900
- });
13901
- const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13902
- return res.json(bundle);
13903
- } catch (err) {
13904
- return sendOperationOutcome500(
13905
- res,
13906
- err,
13907
- "GET /Encounter?date= search error:"
13908
- );
13909
- }
13878
+ const ctx = req.openhiContext;
13879
+ try {
13880
+ const result = await genericSearchOperation({
13881
+ resourceType: ENCOUNTER_RESOURCE_TYPE,
13882
+ tenantId: ctx.tenantId,
13883
+ workspaceId: ctx.workspaceId,
13884
+ query: req.query,
13885
+ resolver: defaultSearchParameterResolver
13886
+ });
13887
+ const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13888
+ return res.json(bundle);
13889
+ } catch (err) {
13890
+ return sendOperationOutcome500(res, err, "GET /Encounter search error:");
13910
13891
  }
13911
- return handleListRoute({
13912
- req,
13913
- res,
13914
- basePath: BASE_PATH.ENCOUNTER,
13915
- listOperation: listEncountersOperation,
13916
- errorLogContext: "GET /Encounter list error:"
13917
- });
13918
13892
  }
13919
13893
 
13920
13894
  // src/data/operations/data/encounter/encounter-update-operation.ts
@@ -24594,44 +24568,137 @@ async function listObservationsOperation(params) {
24594
24568
  }
24595
24569
 
24596
24570
  // src/data/rest-api/routes/data/observation/observation-list-route.ts
24597
- async function listObservationsRoute(req, res) {
24598
- return handleListRoute({
24599
- req,
24600
- res,
24601
- basePath: BASE_PATH.OBSERVATION,
24602
- listOperation: listObservationsOperation,
24603
- errorLogContext: "GET /Observation list error:"
24571
+ var OBSERVATION_RESOURCE_TYPE = "Observation";
24572
+ function stripModifier3(key) {
24573
+ const idx = key.indexOf(":");
24574
+ return idx === -1 ? key : key.slice(0, idx);
24575
+ }
24576
+ function isResultParameter3(key) {
24577
+ return key.startsWith("_");
24578
+ }
24579
+ function sendInvalidSearch4003(res, diagnostics) {
24580
+ return res.status(400).json({
24581
+ resourceType: "OperationOutcome",
24582
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
24604
24583
  });
24605
24584
  }
24606
-
24607
- // src/data/operations/data/observation/observation-update-operation.ts
24608
- async function updateObservationOperation(params) {
24609
- const { context, id, body, tableName } = params;
24610
- const { tenantId, workspaceId, date, actorId, actorName } = context;
24611
- const service = getDynamoDataService(tableName);
24612
- return updateDataEntityById(
24613
- service.entities.observation,
24614
- tenantId,
24615
- workspaceId,
24616
- id,
24617
- "Observation",
24618
- context,
24619
- (existingResourceStr) => buildUpdatedResourceWithAudit(
24620
- body,
24621
- id,
24622
- date,
24623
- actorId,
24624
- actorName,
24625
- existingResourceStr,
24626
- "Observation"
24627
- )
24628
- );
24585
+ function extractSearchParamKeys3(query) {
24586
+ const out = [];
24587
+ for (const rawKey of Object.keys(query)) {
24588
+ if (isResultParameter3(rawKey)) {
24589
+ continue;
24590
+ }
24591
+ out.push({ rawKey, code: stripModifier3(rawKey) });
24592
+ }
24593
+ return out;
24629
24594
  }
24630
-
24631
- // src/data/rest-api/routes/data/observation/observation-update-route.ts
24632
- async function updateObservationRoute(req, res) {
24633
- const bodyResult = requireJsonBodyAs(req, res);
24634
- if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
24595
+ function buildUnknownParamDiagnostics3(unknownCodes) {
24596
+ const validCodes = getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
24597
+ const codes = unknownCodes.join(", ");
24598
+ const isPlural = unknownCodes.length !== 1;
24599
+ return [
24600
+ `Unknown search ${isPlural ? "parameters" : "parameter"} for Observation: ${codes}.`,
24601
+ `Valid codes: ${validCodes}.`
24602
+ ].join(" ");
24603
+ }
24604
+ function findMalformedReference3(query, searchParamKeys) {
24605
+ const referenceCodes = new Set(
24606
+ getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
24607
+ );
24608
+ for (const { rawKey, code } of searchParamKeys) {
24609
+ if (!referenceCodes.has(code)) {
24610
+ continue;
24611
+ }
24612
+ const raw = query[rawKey];
24613
+ const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
24614
+ for (const v of values) {
24615
+ const trimmed = v.trim();
24616
+ if (trimmed.length === 0) {
24617
+ continue;
24618
+ }
24619
+ if (parseTypedReference(trimmed) === void 0) {
24620
+ return { rawKey, value: trimmed };
24621
+ }
24622
+ }
24623
+ }
24624
+ return void 0;
24625
+ }
24626
+ async function listObservationsRoute(req, res) {
24627
+ const searchParamKeys = extractSearchParamKeys3(
24628
+ req.query
24629
+ );
24630
+ if (searchParamKeys.length === 0) {
24631
+ return handleListRoute({
24632
+ req,
24633
+ res,
24634
+ basePath: BASE_PATH.OBSERVATION,
24635
+ listOperation: listObservationsOperation,
24636
+ errorLogContext: "GET /Observation list error:"
24637
+ });
24638
+ }
24639
+ const registered = getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE);
24640
+ const validCodes = new Set(registered.map((p) => p.code));
24641
+ const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
24642
+ if (unknownCodes.length > 0) {
24643
+ return sendInvalidSearch4003(
24644
+ res,
24645
+ buildUnknownParamDiagnostics3([...new Set(unknownCodes)])
24646
+ );
24647
+ }
24648
+ const malformedRef = findMalformedReference3(
24649
+ req.query,
24650
+ searchParamKeys
24651
+ );
24652
+ if (malformedRef !== void 0) {
24653
+ return sendInvalidSearch4003(
24654
+ res,
24655
+ `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
24656
+ );
24657
+ }
24658
+ const ctx = req.openhiContext;
24659
+ try {
24660
+ const result = await genericSearchOperation({
24661
+ resourceType: OBSERVATION_RESOURCE_TYPE,
24662
+ tenantId: ctx.tenantId,
24663
+ workspaceId: ctx.workspaceId,
24664
+ query: req.query,
24665
+ resolver: defaultSearchParameterResolver
24666
+ });
24667
+ const bundle = buildSearchsetBundle(BASE_PATH.OBSERVATION, result.entries);
24668
+ return res.json(bundle);
24669
+ } catch (err) {
24670
+ return sendOperationOutcome500(res, err, "GET /Observation search error:");
24671
+ }
24672
+ }
24673
+
24674
+ // src/data/operations/data/observation/observation-update-operation.ts
24675
+ async function updateObservationOperation(params) {
24676
+ const { context, id, body, tableName } = params;
24677
+ const { tenantId, workspaceId, date, actorId, actorName } = context;
24678
+ const service = getDynamoDataService(tableName);
24679
+ return updateDataEntityById(
24680
+ service.entities.observation,
24681
+ tenantId,
24682
+ workspaceId,
24683
+ id,
24684
+ "Observation",
24685
+ context,
24686
+ (existingResourceStr) => buildUpdatedResourceWithAudit(
24687
+ body,
24688
+ id,
24689
+ date,
24690
+ actorId,
24691
+ actorName,
24692
+ existingResourceStr,
24693
+ "Observation"
24694
+ )
24695
+ );
24696
+ }
24697
+
24698
+ // src/data/rest-api/routes/data/observation/observation-update-route.ts
24699
+ async function updateObservationRoute(req, res) {
24700
+ const bodyResult = requireJsonBodyAs(req, res);
24701
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
24635
24702
  const id = String(req.params.id);
24636
24703
  const ctx = req.openhiContext;
24637
24704
  const body = bodyResult.body;
@@ -25499,492 +25566,109 @@ async function createPatientRoute(req, res) {
25499
25566
  try {
25500
25567
  const result = await createPatientOperation({
25501
25568
  context: ctx,
25502
- body: patient
25503
- });
25504
- return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
25505
- } catch (err) {
25506
- return sendOperationOutcome500(res, err, "POST Patient error:");
25507
- }
25508
- }
25509
-
25510
- // src/data/operations/data/patient/patient-delete-operation.ts
25511
- async function deletePatientOperation(params) {
25512
- const { context, id, tableName } = params;
25513
- const { tenantId, workspaceId } = context;
25514
- const service = getDynamoDataService(tableName);
25515
- await deleteDataEntityById(
25516
- service.entities.patient,
25517
- tenantId,
25518
- workspaceId,
25519
- id
25520
- );
25521
- }
25522
-
25523
- // src/data/rest-api/routes/data/patient/patient-delete-route.ts
25524
- async function deletePatientRoute(req, res) {
25525
- const id = String(req.params.id);
25526
- const ctx = req.openhiContext;
25527
- try {
25528
- await deletePatientOperation({ context: ctx, id });
25529
- return res.status(204).send();
25530
- } catch (err) {
25531
- return sendOperationOutcome500(res, err, "DELETE Patient error:");
25532
- }
25533
- }
25534
-
25535
- // src/data/operations/data/patient/patient-get-by-id-operation.ts
25536
- async function getPatientByIdOperation(params) {
25537
- const { context, id, tableName } = params;
25538
- const { tenantId, workspaceId } = context;
25539
- const service = getDynamoDataService(tableName);
25540
- return getDataEntityById(
25541
- service.entities.patient,
25542
- tenantId,
25543
- workspaceId,
25544
- id,
25545
- "Patient"
25546
- );
25547
- }
25548
-
25549
- // src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
25550
- async function getPatientByIdRoute(req, res) {
25551
- const id = String(req.params.id);
25552
- const ctx = req.openhiContext;
25553
- try {
25554
- const result = await getPatientByIdOperation({ context: ctx, id });
25555
- return res.json(result.resource);
25556
- } catch (err) {
25557
- const status = domainErrorToHttpStatus(err);
25558
- if (status === 404) {
25559
- const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
25560
- return sendOperationOutcome404(res, diagnostics);
25561
- }
25562
- return sendOperationOutcome500(res, err, "GET Patient error:");
25563
- }
25564
- }
25565
-
25566
- // src/data/operations/data/patient/patient-list-operation.ts
25567
- async function listPatientsOperation(params) {
25568
- const { context, tableName, mode } = params;
25569
- const { tenantId, workspaceId } = context;
25570
- const service = getDynamoDataService(tableName);
25571
- return listDataEntitiesByWorkspace(
25572
- service.entities.patient,
25573
- tenantId,
25574
- workspaceId,
25575
- mode
25576
- );
25577
- }
25578
-
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: [] };
25569
+ body: patient
25570
+ });
25571
+ return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
25572
+ } catch (err) {
25573
+ return sendOperationOutcome500(res, err, "POST Patient error:");
25867
25574
  }
25868
- return { sql: groupSqls.join(" AND "), params: allParams };
25869
25575
  }
25870
25576
 
25871
- // src/data/search/operations/generic-search-operation.ts
25872
- var DEFAULT_LIMIT7 = 100;
25873
- function buildGenericSearchSql(opts) {
25874
- const lines = [
25875
- "SELECT resource_id AS id, resource",
25876
- "FROM resources",
25877
- "WHERE tenant_id = :tenantId",
25878
- " AND workspace_id = :workspaceId",
25879
- " AND resource_type = :resourceType",
25880
- " AND deleted_at IS NULL"
25881
- ];
25882
- if (opts.combinedSql.length > 0) {
25883
- lines.push(` AND (${opts.combinedSql})`);
25577
+ // src/data/operations/data/patient/patient-delete-operation.ts
25578
+ async function deletePatientOperation(params) {
25579
+ const { context, id, tableName } = params;
25580
+ const { tenantId, workspaceId } = context;
25581
+ const service = getDynamoDataService(tableName);
25582
+ await deleteDataEntityById(
25583
+ service.entities.patient,
25584
+ tenantId,
25585
+ workspaceId,
25586
+ id
25587
+ );
25588
+ }
25589
+
25590
+ // src/data/rest-api/routes/data/patient/patient-delete-route.ts
25591
+ async function deletePatientRoute(req, res) {
25592
+ const id = String(req.params.id);
25593
+ const ctx = req.openhiContext;
25594
+ try {
25595
+ await deletePatientOperation({ context: ctx, id });
25596
+ return res.status(204).send();
25597
+ } catch (err) {
25598
+ return sendOperationOutcome500(res, err, "DELETE Patient error:");
25884
25599
  }
25885
- lines.push("ORDER BY last_updated DESC");
25886
- lines.push("LIMIT :limit;");
25887
- return lines.join("\n");
25888
25600
  }
25889
- async function genericSearchOperation(params) {
25890
- const { resourceType, tenantId, workspaceId, query, resolver } = params;
25891
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
25892
- const limit = params.limit ?? DEFAULT_LIMIT7;
25893
- const registeredParams = resolver(resourceType, tenantId);
25894
- const context = { tenantId, workspaceId, resourceType };
25895
- const combined = combineSearchPredicates({
25896
- query,
25897
- registeredParams,
25898
- context
25899
- });
25900
- const sql = buildGenericSearchSql({ combinedSql: combined.sql });
25901
- const queryParams = [
25902
- { name: "tenantId", value: tenantId },
25903
- { name: "workspaceId", value: workspaceId },
25904
- { name: "resourceType", value: resourceType },
25905
- { name: "limit", value: limit },
25906
- ...combined.params
25907
- ];
25908
- const rows = await runner.query(sql, queryParams);
25909
- const entries = rows.map((row) => ({
25910
- id: row.id,
25911
- resource: { ...row.resource, id: row.id }
25912
- }));
25913
- return { entries, total: entries.length };
25601
+
25602
+ // src/data/operations/data/patient/patient-get-by-id-operation.ts
25603
+ async function getPatientByIdOperation(params) {
25604
+ const { context, id, tableName } = params;
25605
+ const { tenantId, workspaceId } = context;
25606
+ const service = getDynamoDataService(tableName);
25607
+ return getDataEntityById(
25608
+ service.entities.patient,
25609
+ tenantId,
25610
+ workspaceId,
25611
+ id,
25612
+ "Patient"
25613
+ );
25914
25614
  }
25915
25615
 
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"
25616
+ // src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
25617
+ async function getPatientByIdRoute(req, res) {
25618
+ const id = String(req.params.id);
25619
+ const ctx = req.openhiContext;
25620
+ try {
25621
+ const result = await getPatientByIdOperation({ context: ctx, id });
25622
+ return res.json(result.resource);
25623
+ } catch (err) {
25624
+ const status = domainErrorToHttpStatus(err);
25625
+ if (status === 404) {
25626
+ const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
25627
+ return sendOperationOutcome404(res, diagnostics);
25628
+ }
25629
+ return sendOperationOutcome500(res, err, "GET Patient error:");
25950
25630
  }
25951
- ];
25631
+ }
25952
25632
 
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] ?? [];
25633
+ // src/data/operations/data/patient/patient-list-operation.ts
25634
+ async function listPatientsOperation(params) {
25635
+ const { context, tableName, mode } = params;
25636
+ const { tenantId, workspaceId } = context;
25637
+ const service = getDynamoDataService(tableName);
25638
+ return listDataEntitiesByWorkspace(
25639
+ service.entities.patient,
25640
+ tenantId,
25641
+ workspaceId,
25642
+ mode
25643
+ );
25960
25644
  }
25961
25645
 
25962
25646
  // src/data/rest-api/routes/data/patient/patient-list-route.ts
25963
25647
  var PATIENT_RESOURCE_TYPE = "Patient";
25964
- function stripModifier(key) {
25648
+ function stripModifier4(key) {
25965
25649
  const idx = key.indexOf(":");
25966
25650
  return idx === -1 ? key : key.slice(0, idx);
25967
25651
  }
25968
- function isResultParameter(key) {
25652
+ function isResultParameter4(key) {
25969
25653
  return key.startsWith("_");
25970
25654
  }
25971
- function sendInvalidSearch4003(res, diagnostics) {
25655
+ function sendInvalidSearch4004(res, diagnostics) {
25972
25656
  return res.status(400).json({
25973
25657
  resourceType: "OperationOutcome",
25974
25658
  issue: [{ severity: "error", code: "invalid", diagnostics }]
25975
25659
  });
25976
25660
  }
25977
- function extractSearchParamKeys(query) {
25661
+ function extractSearchParamKeys4(query) {
25978
25662
  const out = [];
25979
25663
  for (const rawKey of Object.keys(query)) {
25980
- if (isResultParameter(rawKey)) {
25664
+ if (isResultParameter4(rawKey)) {
25981
25665
  continue;
25982
25666
  }
25983
- out.push({ rawKey, code: stripModifier(rawKey) });
25667
+ out.push({ rawKey, code: stripModifier4(rawKey) });
25984
25668
  }
25985
25669
  return out;
25986
25670
  }
25987
- function buildUnknownParamDiagnostics(unknownCodes) {
25671
+ function buildUnknownParamDiagnostics4(unknownCodes) {
25988
25672
  const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
25989
25673
  const codes = unknownCodes.join(", ");
25990
25674
  const isPlural = unknownCodes.length !== 1;
@@ -25993,7 +25677,7 @@ function buildUnknownParamDiagnostics(unknownCodes) {
25993
25677
  `Valid codes: ${validCodes}.`
25994
25678
  ].join(" ");
25995
25679
  }
25996
- function findMalformedReference(query, searchParamKeys) {
25680
+ function findMalformedReference4(query, searchParamKeys) {
25997
25681
  const referenceCodes = new Set(
25998
25682
  getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
25999
25683
  );
@@ -26016,7 +25700,7 @@ function findMalformedReference(query, searchParamKeys) {
26016
25700
  return void 0;
26017
25701
  }
26018
25702
  async function listPatientsRoute(req, res) {
26019
- const searchParamKeys = extractSearchParamKeys(
25703
+ const searchParamKeys = extractSearchParamKeys4(
26020
25704
  req.query
26021
25705
  );
26022
25706
  if (searchParamKeys.length === 0) {
@@ -26032,17 +25716,17 @@ async function listPatientsRoute(req, res) {
26032
25716
  const validCodes = new Set(registered.map((p) => p.code));
26033
25717
  const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
26034
25718
  if (unknownCodes.length > 0) {
26035
- return sendInvalidSearch4003(
25719
+ return sendInvalidSearch4004(
26036
25720
  res,
26037
- buildUnknownParamDiagnostics([...new Set(unknownCodes)])
25721
+ buildUnknownParamDiagnostics4([...new Set(unknownCodes)])
26038
25722
  );
26039
25723
  }
26040
- const malformedRef = findMalformedReference(
25724
+ const malformedRef = findMalformedReference4(
26041
25725
  req.query,
26042
25726
  searchParamKeys
26043
25727
  );
26044
25728
  if (malformedRef !== void 0) {
26045
- return sendInvalidSearch4003(
25729
+ return sendInvalidSearch4004(
26046
25730
  res,
26047
25731
  `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
26048
25732
  );
@@ -27381,15 +27065,108 @@ async function listProceduresOperation(params) {
27381
27065
  }
27382
27066
 
27383
27067
  // src/data/rest-api/routes/data/procedure/procedure-list-route.ts
27384
- async function listProceduresRoute(req, res) {
27385
- return handleListRoute({
27386
- req,
27387
- res,
27388
- basePath: BASE_PATH.PROCEDURE,
27389
- listOperation: listProceduresOperation,
27390
- errorLogContext: "GET /Procedure list error:"
27068
+ var PROCEDURE_RESOURCE_TYPE = "Procedure";
27069
+ function stripModifier5(key) {
27070
+ const idx = key.indexOf(":");
27071
+ return idx === -1 ? key : key.slice(0, idx);
27072
+ }
27073
+ function isResultParameter5(key) {
27074
+ return key.startsWith("_");
27075
+ }
27076
+ function sendInvalidSearch4005(res, diagnostics) {
27077
+ return res.status(400).json({
27078
+ resourceType: "OperationOutcome",
27079
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
27391
27080
  });
27392
27081
  }
27082
+ function extractSearchParamKeys5(query) {
27083
+ const out = [];
27084
+ for (const rawKey of Object.keys(query)) {
27085
+ if (isResultParameter5(rawKey)) {
27086
+ continue;
27087
+ }
27088
+ out.push({ rawKey, code: stripModifier5(rawKey) });
27089
+ }
27090
+ return out;
27091
+ }
27092
+ function buildUnknownParamDiagnostics5(unknownCodes) {
27093
+ const validCodes = getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
27094
+ const codes = unknownCodes.join(", ");
27095
+ const isPlural = unknownCodes.length !== 1;
27096
+ return [
27097
+ `Unknown search ${isPlural ? "parameters" : "parameter"} for Procedure: ${codes}.`,
27098
+ `Valid codes: ${validCodes}.`
27099
+ ].join(" ");
27100
+ }
27101
+ function findMalformedReference5(query, searchParamKeys) {
27102
+ const referenceCodes = new Set(
27103
+ getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
27104
+ );
27105
+ for (const { rawKey, code } of searchParamKeys) {
27106
+ if (!referenceCodes.has(code)) {
27107
+ continue;
27108
+ }
27109
+ const raw = query[rawKey];
27110
+ const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
27111
+ for (const v of values) {
27112
+ const trimmed = v.trim();
27113
+ if (trimmed.length === 0) {
27114
+ continue;
27115
+ }
27116
+ if (parseTypedReference(trimmed) === void 0) {
27117
+ return { rawKey, value: trimmed };
27118
+ }
27119
+ }
27120
+ }
27121
+ return void 0;
27122
+ }
27123
+ async function listProceduresRoute(req, res) {
27124
+ const searchParamKeys = extractSearchParamKeys5(
27125
+ req.query
27126
+ );
27127
+ if (searchParamKeys.length === 0) {
27128
+ return handleListRoute({
27129
+ req,
27130
+ res,
27131
+ basePath: BASE_PATH.PROCEDURE,
27132
+ listOperation: listProceduresOperation,
27133
+ errorLogContext: "GET /Procedure list error:"
27134
+ });
27135
+ }
27136
+ const registered = getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE);
27137
+ const validCodes = new Set(registered.map((p) => p.code));
27138
+ const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
27139
+ if (unknownCodes.length > 0) {
27140
+ return sendInvalidSearch4005(
27141
+ res,
27142
+ buildUnknownParamDiagnostics5([...new Set(unknownCodes)])
27143
+ );
27144
+ }
27145
+ const malformedRef = findMalformedReference5(
27146
+ req.query,
27147
+ searchParamKeys
27148
+ );
27149
+ if (malformedRef !== void 0) {
27150
+ return sendInvalidSearch4005(
27151
+ res,
27152
+ `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
27153
+ );
27154
+ }
27155
+ const ctx = req.openhiContext;
27156
+ try {
27157
+ const result = await genericSearchOperation({
27158
+ resourceType: PROCEDURE_RESOURCE_TYPE,
27159
+ tenantId: ctx.tenantId,
27160
+ workspaceId: ctx.workspaceId,
27161
+ query: req.query,
27162
+ resolver: defaultSearchParameterResolver
27163
+ });
27164
+ const bundle = buildSearchsetBundle(BASE_PATH.PROCEDURE, result.entries);
27165
+ return res.json(bundle);
27166
+ } catch (err) {
27167
+ return sendOperationOutcome500(res, err, "GET /Procedure search error:");
27168
+ }
27169
+ }
27393
27170
 
27394
27171
  // src/data/operations/data/procedure/procedure-update-operation.ts
27395
27172
  async function updateProcedureOperation(params) {
@@ -29845,7 +29622,7 @@ async function listSchedulesOperation(params) {
29845
29622
  }
29846
29623
 
29847
29624
  // src/data/operations/data/schedule/schedule-search-by-actor-operation.ts
29848
- var DEFAULT_LIMIT8 = 100;
29625
+ var DEFAULT_LIMIT2 = 100;
29849
29626
  function buildSearchSchedulesByActorSql() {
29850
29627
  return [
29851
29628
  "SELECT resource_id AS id, resource",
@@ -29863,7 +29640,7 @@ async function searchSchedulesByActorOperation(params) {
29863
29640
  const { context, actorReference } = params;
29864
29641
  const { tenantId, workspaceId } = context;
29865
29642
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
29866
- const limit = params.limit ?? DEFAULT_LIMIT8;
29643
+ const limit = params.limit ?? DEFAULT_LIMIT2;
29867
29644
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
29868
29645
  shape: { kind: "array-of-references", field: "actor" },
29869
29646
  reference: actorReference,
@@ -29889,7 +29666,7 @@ async function searchSchedulesByActorOperation(params) {
29889
29666
  }
29890
29667
 
29891
29668
  // src/data/rest-api/routes/data/schedule/schedule-list-route.ts
29892
- function singleStringQueryParam3(req, name) {
29669
+ function singleStringQueryParam(req, name) {
29893
29670
  const v = req.query[name];
29894
29671
  if (typeof v !== "string") {
29895
29672
  return void 0;
@@ -29897,17 +29674,17 @@ function singleStringQueryParam3(req, name) {
29897
29674
  const trimmed = v.trim();
29898
29675
  return trimmed === "" ? void 0 : trimmed;
29899
29676
  }
29900
- function sendInvalidSearch4004(res, diagnostics) {
29677
+ function sendInvalidSearch4006(res, diagnostics) {
29901
29678
  return res.status(400).json({
29902
29679
  resourceType: "OperationOutcome",
29903
29680
  issue: [{ severity: "error", code: "invalid", diagnostics }]
29904
29681
  });
29905
29682
  }
29906
29683
  async function listSchedulesRoute(req, res) {
29907
- const actorRef = singleStringQueryParam3(req, "actor");
29684
+ const actorRef = singleStringQueryParam(req, "actor");
29908
29685
  if (actorRef !== void 0) {
29909
29686
  if (parseTypedReference(actorRef) === void 0) {
29910
- return sendInvalidSearch4004(
29687
+ return sendInvalidSearch4006(
29911
29688
  res,
29912
29689
  `?actor must be a typed reference like "Practitioner/<id>"; got "${actorRef}".`
29913
29690
  );
@@ -33609,7 +33386,7 @@ async function listTasksOperation(params) {
33609
33386
  }
33610
33387
 
33611
33388
  // src/data/operations/data/task/task-search-by-owner-operation.ts
33612
- var DEFAULT_LIMIT9 = 100;
33389
+ var DEFAULT_LIMIT3 = 100;
33613
33390
  function buildSearchTasksByOwnerSql() {
33614
33391
  return [
33615
33392
  "SELECT resource_id AS id, resource",
@@ -33627,7 +33404,7 @@ async function searchTasksByOwnerOperation(params) {
33627
33404
  const { context, ownerReference } = params;
33628
33405
  const { tenantId, workspaceId } = context;
33629
33406
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
33630
- const limit = params.limit ?? DEFAULT_LIMIT9;
33407
+ const limit = params.limit ?? DEFAULT_LIMIT3;
33631
33408
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
33632
33409
  shape: { kind: "scalar", field: "owner" },
33633
33410
  reference: ownerReference,
@@ -33653,7 +33430,7 @@ async function searchTasksByOwnerOperation(params) {
33653
33430
  }
33654
33431
 
33655
33432
  // src/data/operations/data/task/task-search-by-requester-operation.ts
33656
- var DEFAULT_LIMIT10 = 100;
33433
+ var DEFAULT_LIMIT4 = 100;
33657
33434
  function buildSearchTasksByRequesterSql() {
33658
33435
  return [
33659
33436
  "SELECT resource_id AS id, resource",
@@ -33671,7 +33448,7 @@ async function searchTasksByRequesterOperation(params) {
33671
33448
  const { context, requesterReference } = params;
33672
33449
  const { tenantId, workspaceId } = context;
33673
33450
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
33674
- const limit = params.limit ?? DEFAULT_LIMIT10;
33451
+ const limit = params.limit ?? DEFAULT_LIMIT4;
33675
33452
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
33676
33453
  shape: { kind: "scalar", field: "requester" },
33677
33454
  reference: requesterReference,
@@ -33697,7 +33474,7 @@ async function searchTasksByRequesterOperation(params) {
33697
33474
  }
33698
33475
 
33699
33476
  // src/data/rest-api/routes/data/task/task-list-route.ts
33700
- function singleStringQueryParam4(req, name) {
33477
+ function singleStringQueryParam2(req, name) {
33701
33478
  const v = req.query[name];
33702
33479
  if (typeof v !== "string") {
33703
33480
  return void 0;
@@ -33705,24 +33482,24 @@ function singleStringQueryParam4(req, name) {
33705
33482
  const trimmed = v.trim();
33706
33483
  return trimmed === "" ? void 0 : trimmed;
33707
33484
  }
33708
- function sendInvalidSearch4005(res, diagnostics) {
33485
+ function sendInvalidSearch4007(res, diagnostics) {
33709
33486
  return res.status(400).json({
33710
33487
  resourceType: "OperationOutcome",
33711
33488
  issue: [{ severity: "error", code: "invalid", diagnostics }]
33712
33489
  });
33713
33490
  }
33714
33491
  async function listTasksRoute(req, res) {
33715
- const ownerRef = singleStringQueryParam4(req, "owner");
33716
- const requesterRef = singleStringQueryParam4(req, "requester");
33492
+ const ownerRef = singleStringQueryParam2(req, "owner");
33493
+ const requesterRef = singleStringQueryParam2(req, "requester");
33717
33494
  if (ownerRef !== void 0 && requesterRef !== void 0) {
33718
- return sendInvalidSearch4005(
33495
+ return sendInvalidSearch4007(
33719
33496
  res,
33720
33497
  "?owner= and ?requester= cannot be combined on the same request."
33721
33498
  );
33722
33499
  }
33723
33500
  if (ownerRef !== void 0) {
33724
33501
  if (parseTypedReference(ownerRef) === void 0) {
33725
- return sendInvalidSearch4005(
33502
+ return sendInvalidSearch4007(
33726
33503
  res,
33727
33504
  `?owner must be a typed reference like "Practitioner/<id>"; got "${ownerRef}".`
33728
33505
  );
@@ -33745,7 +33522,7 @@ async function listTasksRoute(req, res) {
33745
33522
  }
33746
33523
  if (requesterRef !== void 0) {
33747
33524
  if (parseTypedReference(requesterRef) === void 0) {
33748
- return sendInvalidSearch4005(
33525
+ return sendInvalidSearch4007(
33749
33526
  res,
33750
33527
  `?requester must be a typed reference like "Practitioner/<id>"; got "${requesterRef}".`
33751
33528
  );