@openhi/constructs 0.0.139 → 0.0.140

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,
@@ -5596,129 +5679,95 @@ function emitFieldMissingPredicate(opts) {
5596
5679
  const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
5597
5680
  return { sql, params: [] };
5598
5681
  }
5599
- var DEFAULT_INTERVAL_PARAM_PREFIX = "intervalDateConstraint";
5600
- function intervalConstraintParamName(prefix, index) {
5601
- return `${prefix}${index}`;
5682
+
5683
+ // src/data/search/engine/string-predicate.ts
5684
+ var STRING_MODIFIERS = ["exact", "contains"];
5685
+ function isStringModifier(s) {
5686
+ return STRING_MODIFIERS.includes(s);
5602
5687
  }
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})`;
5688
+ function jsonbPathToStringShape(jsonbPath) {
5689
+ const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5690
+ if (scalar) {
5691
+ return { kind: "scalar", field: scalar[1] };
5619
5692
  }
5620
- }
5621
- function buildIntervalDateSearchPredicateSql(constraints, opts) {
5622
- if (constraints.length === 0) {
5623
- return [];
5693
+ const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5694
+ if (arrayOfScalars) {
5695
+ return { kind: "array-of-scalars", field: arrayOfScalars[1] };
5624
5696
  }
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
- )
5697
+ const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
5698
+ jsonbPath
5635
5699
  );
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
5700
+ if (arrayOfObjs) {
5701
+ return {
5702
+ kind: "array-of-objects",
5703
+ field: arrayOfObjs[1],
5704
+ subfield: arrayOfObjs[2]
5705
+ };
5706
+ }
5707
+ throw new Error(
5708
+ `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5657
5709
  );
5658
5710
  }
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}`;
5711
+ function escapeLikePattern(value) {
5712
+ return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
5675
5713
  }
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;
5714
+ function buildLikePattern(value, modifier) {
5715
+ const escaped = escapeLikePattern(value);
5716
+ switch (modifier) {
5717
+ case "exact":
5718
+ return escaped;
5719
+ case "contains":
5720
+ return `%${escaped}%`;
5721
+ case void 0:
5722
+ return `${escaped}%`;
5681
5723
  }
5682
- return { resourceType: match[1], resourceId: match[2] };
5683
5724
  }
5684
- function wrapReferenceInShape(shape, reference) {
5725
+ function buildIlikeExtractSql(shape, paramName) {
5685
5726
  switch (shape.kind) {
5686
5727
  case "scalar":
5687
- return { [shape.field]: { reference } };
5688
- case "array-of-references":
5689
- return { [shape.field]: [{ reference }] };
5728
+ return `resource->>'${shape.field}' ILIKE :${paramName}`;
5729
+ case "array-of-scalars":
5730
+ return [
5731
+ "EXISTS (SELECT 1 FROM",
5732
+ `jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
5733
+ `WHERE s_elem.text_val ILIKE :${paramName})`
5734
+ ].join(" ");
5690
5735
  case "array-of-objects":
5691
- return { [shape.field]: [{ [shape.subfield]: { reference } }] };
5736
+ return [
5737
+ "EXISTS (SELECT 1 FROM",
5738
+ `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
5739
+ `WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
5740
+ ].join(" ");
5692
5741
  }
5693
5742
  }
5694
- function buildReferenceContainmentPayload(params) {
5695
- const parsed = parseTypedReference(params.reference);
5696
- if (!parsed) {
5743
+ function emitStringPredicate(opts) {
5744
+ const shape = jsonbPathToStringShape(opts.jsonbPath);
5745
+ const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
5746
+ const sql = buildIlikeExtractSql(shape, opts.paramName);
5747
+ const pattern = buildLikePattern(opts.rawValue, modifier);
5748
+ const params = [
5749
+ { name: opts.paramName, value: pattern }
5750
+ ];
5751
+ return { sql, params };
5752
+ }
5753
+ function checkModifier(modifier) {
5754
+ if (!isStringModifier(modifier)) {
5697
5755
  throw new Error(
5698
- `Reference "${params.reference}" is not a valid typed reference (<ResourceType>/<id>).`
5756
+ `String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
5699
5757
  );
5700
5758
  }
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
- };
5759
+ return modifier;
5713
5760
  }
5714
- function jsonbPathToReferenceShape(jsonbPath) {
5761
+
5762
+ // src/data/search/engine/token-predicate.ts
5763
+ function jsonbPathToTokenShape(jsonbPath) {
5715
5764
  const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
5716
5765
  if (scalar) {
5717
5766
  return { kind: "scalar", field: scalar[1] };
5718
5767
  }
5719
- const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5720
- if (arrayOfRefs) {
5721
- return { kind: "array-of-references", field: arrayOfRefs[1] };
5768
+ const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
5769
+ if (arrayOfScalars) {
5770
+ return { kind: "array-of-scalars", field: arrayOfScalars[1] };
5722
5771
  }
5723
5772
  const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
5724
5773
  jsonbPath
@@ -5731,240 +5780,332 @@ function jsonbPathToReferenceShape(jsonbPath) {
5731
5780
  };
5732
5781
  }
5733
5782
  throw new Error(
5734
- `Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5783
+ `Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
5735
5784
  );
5736
5785
  }
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)`;
5786
+ function parseTokenValue(raw) {
5787
+ const idx = raw.indexOf("|");
5788
+ if (idx === -1) {
5789
+ return { code: raw };
5790
+ }
5791
+ const system = raw.slice(0, idx);
5792
+ const code = raw.slice(idx + 1);
5793
+ return system.length > 0 ? { system, code } : { code };
5794
+ }
5795
+ function wrapTokenInShape(shape, value) {
5796
+ switch (shape.kind) {
5797
+ case "scalar":
5798
+ return { [shape.field]: value };
5799
+ case "array-of-scalars":
5800
+ return { [shape.field]: [value] };
5801
+ case "array-of-objects":
5802
+ return { [shape.field]: [{ [shape.subfield]: value }] };
5803
+ }
5804
+ }
5805
+ function emitTokenPredicate(opts) {
5806
+ const shape = jsonbPathToTokenShape(opts.jsonbPath);
5807
+ const { code } = parseTokenValue(opts.rawValue);
5808
+ const payload = wrapTokenInShape(shape, code);
5809
+ const sql = `resource @> :${opts.paramName}::jsonb`;
5748
5810
  const params = [
5749
- { name: relName, value: containmentRelative },
5750
- { name: urnName, value: containmentUrn }
5811
+ { name: opts.paramName, value: JSON.stringify(payload) }
5751
5812
  ];
5752
5813
  return { sql, params };
5753
5814
  }
5754
5815
 
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}`);
5816
+ // src/data/search/engine/combinator.ts
5817
+ function parseQueryKey(key) {
5818
+ const idx = key.indexOf(":");
5819
+ if (idx === -1) {
5820
+ return { code: key, modifier: void 0 };
5772
5821
  }
5773
- lines.push("ORDER BY last_updated DESC");
5774
- lines.push("LIMIT :limit;");
5775
- return lines.join("\n");
5822
+ return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
5776
5823
  }
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
5824
+ function flattenQueryValues(raw) {
5825
+ const list = Array.isArray(raw) ? raw : [raw];
5826
+ const out = [];
5827
+ for (const v of list) {
5828
+ for (const piece of v.split(",")) {
5829
+ out.push(piece);
5830
+ }
5831
+ }
5832
+ return out;
5833
+ }
5834
+ function parseQueryEntries(query) {
5835
+ const entries = [];
5836
+ for (const [rawKey, rawValue] of Object.entries(query)) {
5837
+ if (rawValue === void 0) continue;
5838
+ const { code, modifier } = parseQueryKey(rawKey);
5839
+ entries.push({
5840
+ code,
5841
+ modifier,
5842
+ values: flattenQueryValues(rawValue)
5843
+ });
5844
+ }
5845
+ return entries;
5846
+ }
5847
+ function findParam(params, code) {
5848
+ return params.find((p) => p.code === code);
5849
+ }
5850
+ function checkModifierAllowed(modifier, param) {
5851
+ const universal = modifier === "missing" || modifier === "not";
5852
+ const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
5853
+ if (universal || stringNative) {
5854
+ if (param.modifiers && !param.modifiers.includes(modifier)) {
5855
+ throw new Error(
5856
+ `Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
5857
+ );
5858
+ }
5859
+ return;
5860
+ }
5861
+ throw new Error(
5862
+ `Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
5863
+ );
5864
+ }
5865
+ function emitOne(opts) {
5866
+ const { param, modifier, rawValue, paramName, context } = opts;
5867
+ if (modifier === "missing") {
5868
+ const missing = parseMissingValue(rawValue);
5869
+ return emitFieldMissingPredicate({
5870
+ jsonbPath: param.jsonbPath,
5871
+ missing
5872
+ });
5873
+ }
5874
+ const negate = modifier === "not";
5875
+ const effectiveModifier = negate ? void 0 : modifier;
5876
+ const inner = emitForType({
5877
+ paramType: param.type,
5878
+ jsonbPath: param.jsonbPath,
5879
+ rawValue,
5880
+ paramName,
5881
+ modifier: effectiveModifier,
5882
+ context
5792
5883
  });
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
5884
+ if (!negate) {
5885
+ return inner;
5886
+ }
5887
+ return { sql: `NOT (${inner.sql})`, params: inner.params };
5888
+ }
5889
+ function parseMissingValue(raw) {
5890
+ if (raw === "true") return true;
5891
+ if (raw === "false") return false;
5892
+ throw new Error(
5893
+ `:missing requires a value of "true" or "false"; received "${raw}".`
5894
+ );
5895
+ }
5896
+ function emitForType(opts) {
5897
+ switch (opts.paramType) {
5898
+ case "token":
5899
+ return emitTokenPredicate({
5900
+ jsonbPath: opts.jsonbPath,
5901
+ rawValue: opts.rawValue,
5902
+ paramName: opts.paramName,
5903
+ context: opts.context
5904
+ });
5905
+ case "date":
5906
+ return emitDatePredicate({
5907
+ jsonbPath: opts.jsonbPath,
5908
+ rawValue: opts.rawValue,
5909
+ paramName: opts.paramName,
5910
+ context: opts.context
5911
+ });
5912
+ case "reference":
5913
+ return emitReferencePredicate({
5914
+ jsonbPath: opts.jsonbPath,
5915
+ rawValue: opts.rawValue,
5916
+ paramName: opts.paramName,
5917
+ context: opts.context
5918
+ });
5919
+ case "string":
5920
+ return emitStringPredicate({
5921
+ jsonbPath: opts.jsonbPath,
5922
+ rawValue: opts.rawValue,
5923
+ paramName: opts.paramName,
5924
+ modifier: opts.modifier,
5925
+ context: opts.context
5926
+ });
5927
+ }
5928
+ }
5929
+ function combineSearchPredicates(opts) {
5930
+ const entries = parseQueryEntries(opts.query);
5931
+ const groupSqls = [];
5932
+ const allParams = [];
5933
+ let paramIdx = 0;
5934
+ for (const entry of entries) {
5935
+ const registered = findParam(opts.registeredParams, entry.code);
5936
+ if (!registered) {
5937
+ continue;
5808
5938
  }
5809
- }));
5810
- return { entries, total: entries.length };
5939
+ if (entry.modifier !== void 0) {
5940
+ checkModifierAllowed(entry.modifier, registered);
5941
+ }
5942
+ if (entry.values.length === 0) {
5943
+ continue;
5944
+ }
5945
+ const fragmentSqls = [];
5946
+ let valueIdx = 0;
5947
+ for (const rawValue of entry.values) {
5948
+ const paramName = `p${paramIdx}v${valueIdx}`;
5949
+ const frag = emitOne({
5950
+ param: registered,
5951
+ modifier: entry.modifier,
5952
+ rawValue,
5953
+ paramName,
5954
+ context: opts.context
5955
+ });
5956
+ fragmentSqls.push(frag.sql);
5957
+ for (const p of frag.params) {
5958
+ allParams.push(p);
5959
+ }
5960
+ valueIdx++;
5961
+ }
5962
+ paramIdx++;
5963
+ if (fragmentSqls.length === 1) {
5964
+ groupSqls.push(fragmentSqls[0]);
5965
+ } else {
5966
+ groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
5967
+ }
5968
+ }
5969
+ if (groupSqls.length === 0) {
5970
+ return { sql: "", params: [] };
5971
+ }
5972
+ return { sql: groupSqls.join(" AND "), params: allParams };
5811
5973
  }
5812
5974
 
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
5818
- );
5975
+ // src/data/search/operations/generic-search-operation.ts
5976
+ var DEFAULT_LIMIT = 100;
5977
+ function buildGenericSearchSql(opts) {
5819
5978
  const lines = [
5820
5979
  "SELECT resource_id AS id, resource",
5821
5980
  "FROM resources",
5822
5981
  "WHERE tenant_id = :tenantId",
5823
5982
  " AND workspace_id = :workspaceId",
5824
- " AND resource_type = 'Appointment'",
5983
+ " AND resource_type = :resourceType",
5825
5984
  " AND deleted_at IS NULL"
5826
5985
  ];
5827
- for (const fragment of datePredicates) {
5828
- lines.push(` AND ${fragment}`);
5986
+ if (opts.combinedSql.length > 0) {
5987
+ lines.push(` AND (${opts.combinedSql})`);
5829
5988
  }
5830
5989
  lines.push("ORDER BY last_updated DESC");
5831
5990
  lines.push("LIMIT :limit;");
5832
5991
  return lines.join("\n");
5833
5992
  }
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
- );
5840
- }
5841
- const { tenantId, workspaceId } = context;
5993
+ async function genericSearchOperation(params) {
5994
+ const { resourceType, tenantId, workspaceId, query, resolver } = params;
5842
5995
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
5843
- const limit = params.limit ?? DEFAULT_LIMIT2;
5844
- const sql = buildSearchAppointmentsByDateSql({ dateConstraints });
5996
+ const limit = params.limit ?? DEFAULT_LIMIT;
5997
+ const registeredParams = resolver(resourceType, tenantId);
5998
+ const context = { tenantId, workspaceId, resourceType };
5999
+ const combined = combineSearchPredicates({
6000
+ query,
6001
+ registeredParams,
6002
+ context
6003
+ });
6004
+ const sql = buildGenericSearchSql({ combinedSql: combined.sql });
5845
6005
  const queryParams = [
5846
6006
  { name: "tenantId", value: tenantId },
5847
6007
  { name: "workspaceId", value: workspaceId },
6008
+ { name: "resourceType", value: resourceType },
5848
6009
  { name: "limit", value: limit },
5849
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
6010
+ ...combined.params
5850
6011
  ];
5851
6012
  const rows = await runner.query(sql, queryParams);
5852
6013
  const entries = rows.map((row) => ({
5853
6014
  id: row.id,
5854
- resource: {
5855
- ...row.resource,
5856
- id: row.id
5857
- }
6015
+ resource: { ...row.resource, id: row.id }
5858
6016
  }));
5859
6017
  return { entries, total: entries.length };
5860
6018
  }
5861
6019
 
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
- );
5868
- const lines = [
5869
- "SELECT resource_id AS id, resource",
5870
- "FROM resources",
5871
- "WHERE tenant_id = :tenantId",
5872
- " 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)"
5877
- ];
5878
- for (const fragment of datePredicates) {
5879
- lines.push(` AND ${fragment}`);
5880
- }
5881
- lines.push("ORDER BY last_updated DESC");
5882
- lines.push("LIMIT :limit;");
5883
- return lines.join("\n");
5884
- }
5885
- async function searchAppointmentsByPatientOperation(params) {
5886
- const { context, patientId } = params;
5887
- const dateConstraints = params.dateConstraints ?? [];
5888
- const { tenantId, workspaceId } = context;
5889
- 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
- ]
5907
- });
5908
- const sql = buildSearchAppointmentsByPatientSql({ dateConstraints });
5909
- const queryParams = [
5910
- { name: "tenantId", value: tenantId },
5911
- { name: "workspaceId", value: workspaceId },
5912
- { name: "containmentRelative", value: containmentRelative },
5913
- { name: "containmentUrn", value: containmentUrn },
5914
- { name: "limit", value: limit },
5915
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
5916
- ];
5917
- const rows = await runner.query(sql, queryParams);
5918
- const entries = rows.map((row) => ({
5919
- id: row.id,
5920
- resource: {
5921
- ...row.resource,
5922
- id: row.id
5923
- }
5924
- }));
5925
- return { entries, total: entries.length };
5926
- }
6020
+ // src/data/search/registry/appointment-search-parameters.ts
6021
+ var APPOINTMENT_SEARCH_PARAMETERS = [
6022
+ { code: "status", type: "token", jsonbPath: "$.status" },
6023
+ { code: "date", type: "date", jsonbPath: "$.start" },
6024
+ {
6025
+ code: "patient",
6026
+ type: "reference",
6027
+ jsonbPath: "$.participant[*].actor"
6028
+ },
6029
+ {
6030
+ code: "actor",
6031
+ type: "reference",
6032
+ jsonbPath: "$.participant[*].actor"
6033
+ },
6034
+ {
6035
+ code: "practitioner",
6036
+ type: "reference",
6037
+ jsonbPath: "$.participant[*].actor"
6038
+ },
6039
+ { code: "service-type", type: "token", jsonbPath: "$.serviceType[*]" },
6040
+ {
6041
+ code: "service-category",
6042
+ type: "token",
6043
+ jsonbPath: "$.serviceCategory[*]"
6044
+ },
6045
+ { code: "specialty", type: "token", jsonbPath: "$.specialty[*]" },
6046
+ {
6047
+ code: "appointment-type",
6048
+ type: "token",
6049
+ jsonbPath: "$.appointmentType"
6050
+ },
6051
+ { code: "slot", type: "reference", jsonbPath: "$.slot[*]" }
6052
+ ];
5927
6053
 
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;
6054
+ // src/data/search/registry/patient-search-parameters.ts
6055
+ var PATIENT_SEARCH_PARAMETERS = [
6056
+ { code: "gender", type: "token", jsonbPath: "$.gender" },
6057
+ { code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
6058
+ { code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
6059
+ {
6060
+ code: "name",
6061
+ type: "string",
6062
+ jsonbPath: "$.name[*].text",
6063
+ modifiers: ["exact", "contains", "missing", "not"]
6064
+ },
6065
+ {
6066
+ code: "family",
6067
+ type: "string",
6068
+ jsonbPath: "$.name[*].family",
6069
+ modifiers: ["exact", "contains", "missing", "not"]
6070
+ },
6071
+ {
6072
+ code: "given",
6073
+ type: "string",
6074
+ jsonbPath: "$.name[*].given",
6075
+ modifiers: ["exact", "contains", "missing", "not"]
6076
+ },
6077
+ { code: "active", type: "token", jsonbPath: "$.active" },
6078
+ { code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
6079
+ {
6080
+ code: "general-practitioner",
6081
+ type: "reference",
6082
+ jsonbPath: "$.generalPractitioner[*]"
6083
+ },
6084
+ {
6085
+ code: "organization",
6086
+ type: "reference",
6087
+ jsonbPath: "$.managingOrganization"
5933
6088
  }
5934
- const trimmed = v.trim();
5935
- return trimmed === "" ? void 0 : trimmed;
6089
+ ];
6090
+
6091
+ // src/data/search/registry/resolver.ts
6092
+ var STATIC_SEARCH_PARAMETER_MAP = {
6093
+ Appointment: APPOINTMENT_SEARCH_PARAMETERS,
6094
+ Patient: PATIENT_SEARCH_PARAMETERS
6095
+ };
6096
+ var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
6097
+ function getRegisteredSearchParameters(resourceType) {
6098
+ return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
5936
6099
  }
5937
- function isError(v) {
5938
- return v.error !== void 0;
6100
+
6101
+ // src/data/rest-api/routes/data/appointment/appointment-list-route.ts
6102
+ var APPOINTMENT_RESOURCE_TYPE = "Appointment";
6103
+ function stripModifier(key) {
6104
+ const idx = key.indexOf(":");
6105
+ return idx === -1 ? key : key.slice(0, idx);
5939
6106
  }
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 });
5966
- }
5967
- return out;
6107
+ function isResultParameter(key) {
6108
+ return key.startsWith("_");
5968
6109
  }
5969
6110
  function sendInvalidSearch400(res, diagnostics) {
5970
6111
  return res.status(400).json({
@@ -5972,95 +6113,93 @@ function sendInvalidSearch400(res, diagnostics) {
5972
6113
  issue: [{ severity: "error", code: "invalid", diagnostics }]
5973
6114
  });
5974
6115
  }
6116
+ function extractSearchParamKeys(query) {
6117
+ const out = [];
6118
+ for (const rawKey of Object.keys(query)) {
6119
+ if (isResultParameter(rawKey)) {
6120
+ continue;
6121
+ }
6122
+ out.push({ rawKey, code: stripModifier(rawKey) });
6123
+ }
6124
+ return out;
6125
+ }
6126
+ function buildUnknownParamDiagnostics(unknownCodes) {
6127
+ const validCodes = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
6128
+ const codes = unknownCodes.join(", ");
6129
+ const isPlural = unknownCodes.length !== 1;
6130
+ return [
6131
+ `Unknown search ${isPlural ? "parameters" : "parameter"} for Appointment: ${codes}.`,
6132
+ `Valid codes: ${validCodes}.`
6133
+ ].join(" ");
6134
+ }
6135
+ function findMalformedReference(query, searchParamKeys) {
6136
+ const referenceCodes = new Set(
6137
+ getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
6138
+ );
6139
+ for (const { rawKey, code } of searchParamKeys) {
6140
+ if (!referenceCodes.has(code)) {
6141
+ continue;
6142
+ }
6143
+ const raw = query[rawKey];
6144
+ const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
6145
+ for (const v of values) {
6146
+ const trimmed = v.trim();
6147
+ if (trimmed.length === 0) {
6148
+ continue;
6149
+ }
6150
+ if (parseTypedReference(trimmed) === void 0) {
6151
+ return { rawKey, value: trimmed };
6152
+ }
6153
+ }
6154
+ }
6155
+ return void 0;
6156
+ }
5975
6157
  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);
6158
+ const searchParamKeys = extractSearchParamKeys(
6159
+ req.query
6160
+ );
6161
+ if (searchParamKeys.length === 0) {
6162
+ return handleListRoute({
6163
+ req,
6164
+ res,
6165
+ basePath: BASE_PATH.APPOINTMENT,
6166
+ listOperation: listAppointmentsOperation,
6167
+ errorLogContext: "GET /Appointment list error:"
6168
+ });
5981
6169
  }
5982
- const dateConstraints = parsed;
5983
- if (patientId !== void 0 && actorRef !== void 0) {
6170
+ const registered = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE);
6171
+ const validCodes = new Set(registered.map((p) => p.code));
6172
+ const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
6173
+ if (unknownCodes.length > 0) {
5984
6174
  return sendInvalidSearch400(
5985
6175
  res,
5986
- "?patient= and ?actor= cannot be combined on the same request."
6176
+ buildUnknownParamDiagnostics([...new Set(unknownCodes)])
5987
6177
  );
5988
6178
  }
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
- }
6179
+ const malformedRef = findMalformedReference(
6180
+ req.query,
6181
+ searchParamKeys
6182
+ );
6183
+ if (malformedRef !== void 0) {
6184
+ return sendInvalidSearch400(
6185
+ res,
6186
+ `?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
6187
+ );
6036
6188
  }
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
- }
6189
+ const ctx = req.openhiContext;
6190
+ try {
6191
+ const result = await genericSearchOperation({
6192
+ resourceType: APPOINTMENT_RESOURCE_TYPE,
6193
+ tenantId: ctx.tenantId,
6194
+ workspaceId: ctx.workspaceId,
6195
+ query: req.query,
6196
+ resolver: defaultSearchParameterResolver
6197
+ });
6198
+ const bundle = buildSearchsetBundle(BASE_PATH.APPOINTMENT, result.entries);
6199
+ return res.json(bundle);
6200
+ } catch (err) {
6201
+ return sendOperationOutcome500(res, err, "GET /Appointment search error:");
6056
6202
  }
6057
- return handleListRoute({
6058
- req,
6059
- res,
6060
- basePath: BASE_PATH.APPOINTMENT,
6061
- listOperation: listAppointmentsOperation,
6062
- errorLogContext: "GET /Appointment list error:"
6063
- });
6064
6203
  }
6065
6204
 
6066
6205
  // src/data/operations/data/appointment/appointment-update-operation.ts
@@ -13620,7 +13759,7 @@ function buildPeriodSearchPredicateParams(constraints) {
13620
13759
  }
13621
13760
 
13622
13761
  // src/data/operations/data/encounter/encounter-search-by-date-operation.ts
13623
- var DEFAULT_LIMIT4 = 100;
13762
+ var DEFAULT_LIMIT2 = 100;
13624
13763
  function buildSearchEncountersByDateSql(opts) {
13625
13764
  const periodPredicates = buildPeriodSearchPredicateSql(
13626
13765
  opts.periodConstraints
@@ -13649,7 +13788,7 @@ async function searchEncountersByDateOperation(params) {
13649
13788
  }
13650
13789
  const { tenantId, workspaceId } = context;
13651
13790
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
13652
- const limit = params.limit ?? DEFAULT_LIMIT4;
13791
+ const limit = params.limit ?? DEFAULT_LIMIT2;
13653
13792
  const sql = buildSearchEncountersByDateSql({ periodConstraints });
13654
13793
  const queryParams = [
13655
13794
  { name: "tenantId", value: tenantId },
@@ -13669,7 +13808,7 @@ async function searchEncountersByDateOperation(params) {
13669
13808
  }
13670
13809
 
13671
13810
  // src/data/operations/data/encounter/encounter-search-by-participant-operation.ts
13672
- var DEFAULT_LIMIT5 = 100;
13811
+ var DEFAULT_LIMIT3 = 100;
13673
13812
  function buildSearchEncountersByParticipantSql(opts) {
13674
13813
  const periodPredicates = buildPeriodSearchPredicateSql(
13675
13814
  opts?.periodConstraints ?? []
@@ -13695,7 +13834,7 @@ async function searchEncountersByParticipantOperation(params) {
13695
13834
  const periodConstraints = params.periodConstraints ?? [];
13696
13835
  const { tenantId, workspaceId } = context;
13697
13836
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
13698
- const limit = params.limit ?? DEFAULT_LIMIT5;
13837
+ const limit = params.limit ?? DEFAULT_LIMIT3;
13699
13838
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
13700
13839
  shape: {
13701
13840
  kind: "array-of-objects",
@@ -13727,7 +13866,7 @@ async function searchEncountersByParticipantOperation(params) {
13727
13866
  }
13728
13867
 
13729
13868
  // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
13730
- var DEFAULT_LIMIT6 = 100;
13869
+ var DEFAULT_LIMIT4 = 100;
13731
13870
  function buildSearchEncountersByPatientSql(opts) {
13732
13871
  const periodPredicates = buildPeriodSearchPredicateSql(
13733
13872
  opts?.periodConstraints ?? []
@@ -13754,7 +13893,7 @@ async function searchEncountersByPatientOperation(params) {
13754
13893
  const periodConstraints = params.periodConstraints ?? [];
13755
13894
  const { tenantId, workspaceId } = context;
13756
13895
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
13757
- const limit = params.limit ?? DEFAULT_LIMIT6;
13896
+ const limit = params.limit ?? DEFAULT_LIMIT4;
13758
13897
  const containmentRelative = JSON.stringify({
13759
13898
  subject: { reference: `Patient/${patientId}` }
13760
13899
  });
@@ -13789,7 +13928,7 @@ async function searchEncountersByPatientOperation(params) {
13789
13928
  }
13790
13929
 
13791
13930
  // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
13792
- function singleStringQueryParam2(req, name) {
13931
+ function singleStringQueryParam(req, name) {
13793
13932
  const v = req.query[name];
13794
13933
  if (typeof v !== "string") {
13795
13934
  return void 0;
@@ -13797,7 +13936,7 @@ function singleStringQueryParam2(req, name) {
13797
13936
  const trimmed = v.trim();
13798
13937
  return trimmed === "" ? void 0 : trimmed;
13799
13938
  }
13800
- function isError2(v) {
13939
+ function isError(v) {
13801
13940
  return v.error !== void 0;
13802
13941
  }
13803
13942
  function parseEncounterDateConstraints(req) {
@@ -13836,10 +13975,10 @@ function sendInvalidSearch4002(res, diagnostics) {
13836
13975
  });
13837
13976
  }
13838
13977
  async function listEncountersRoute(req, res) {
13839
- const patientId = singleStringQueryParam2(req, "patient");
13840
- const participantRef = singleStringQueryParam2(req, "participant");
13978
+ const patientId = singleStringQueryParam(req, "patient");
13979
+ const participantRef = singleStringQueryParam(req, "participant");
13841
13980
  const parsed = parseEncounterDateConstraints(req);
13842
- if (isError2(parsed)) {
13981
+ if (isError(parsed)) {
13843
13982
  return sendInvalidSearch4002(res, parsed.error);
13844
13983
  }
13845
13984
  const periodConstraints = parsed;
@@ -25412,560 +25551,177 @@ async function listOrganizationAffiliationsRoute(req, res) {
25412
25551
  return handleListRoute({
25413
25552
  req,
25414
25553
  res,
25415
- basePath: BASE_PATH.ORGANIZATIONAFFILIATION,
25416
- listOperation: listOrganizationAffiliationsOperation,
25417
- errorLogContext: "GET /OrganizationAffiliation list error:"
25418
- });
25419
- }
25420
-
25421
- // src/data/operations/data/organizationaffiliation/organizationaffiliation-update-operation.ts
25422
- async function updateOrganizationAffiliationOperation(params) {
25423
- const { context, id, body, tableName } = params;
25424
- const { tenantId, workspaceId, date, actorId, actorName } = context;
25425
- const service = getDynamoDataService(tableName);
25426
- return updateDataEntityById(
25427
- service.entities.organizationaffiliation,
25428
- tenantId,
25429
- workspaceId,
25430
- id,
25431
- "OrganizationAffiliation",
25432
- context,
25433
- (existingResourceStr) => buildUpdatedResourceWithAudit(
25434
- body,
25435
- id,
25436
- date,
25437
- actorId,
25438
- actorName,
25439
- existingResourceStr,
25440
- "OrganizationAffiliation"
25441
- )
25442
- );
25443
- }
25444
-
25445
- // src/data/rest-api/routes/data/organizationaffiliation/organizationaffiliation-update-route.ts
25446
- async function updateOrganizationAffiliationRoute(req, res) {
25447
- const bodyResult = requireJsonBodyAs(req, res);
25448
- if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
25449
- const id = String(req.params.id);
25450
- const ctx = req.openhiContext;
25451
- const body = bodyResult.body;
25452
- const resource = {
25453
- ...body,
25454
- resourceType: "OrganizationAffiliation",
25455
- id
25456
- };
25457
- try {
25458
- const result = await updateOrganizationAffiliationOperation({
25459
- context: ctx,
25460
- id,
25461
- body: resource
25462
- });
25463
- return res.json(result.resource);
25464
- } catch (err) {
25465
- const status = domainErrorToHttpStatus(err);
25466
- if (status === 404) {
25467
- const diagnostics = err instanceof NotFoundError ? err.message : `OrganizationAffiliation ${id} not found`;
25468
- return sendOperationOutcome404(res, diagnostics);
25469
- }
25470
- return sendOperationOutcome500(
25471
- res,
25472
- err,
25473
- "PUT OrganizationAffiliation error:"
25474
- );
25475
- }
25476
- }
25477
-
25478
- // src/data/rest-api/routes/data/organizationaffiliation/organizationaffiliation.ts
25479
- var router107 = express107.Router();
25480
- router107.get("/", listOrganizationAffiliationsRoute);
25481
- router107.get("/:id", getOrganizationAffiliationByIdRoute);
25482
- router107.post("/", createOrganizationAffiliationRoute);
25483
- router107.put("/:id", updateOrganizationAffiliationRoute);
25484
- router107.delete("/:id", deleteOrganizationAffiliationRoute);
25485
-
25486
- // src/data/rest-api/routes/data/patient/patient.ts
25487
- import express108 from "express";
25488
-
25489
- // src/data/rest-api/routes/data/patient/patient-create-route.ts
25490
- async function createPatientRoute(req, res) {
25491
- const bodyResult = requireJsonBodyAs(req, res);
25492
- if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
25493
- const ctx = req.openhiContext;
25494
- const body = bodyResult.body;
25495
- const patient = {
25496
- ...body,
25497
- resourceType: "Patient"
25498
- };
25499
- try {
25500
- const result = await createPatientOperation({
25501
- 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 ")})`);
25554
+ basePath: BASE_PATH.ORGANIZATIONAFFILIATION,
25555
+ listOperation: listOrganizationAffiliationsOperation,
25556
+ errorLogContext: "GET /OrganizationAffiliation list error:"
25557
+ });
25558
+ }
25559
+
25560
+ // src/data/operations/data/organizationaffiliation/organizationaffiliation-update-operation.ts
25561
+ async function updateOrganizationAffiliationOperation(params) {
25562
+ const { context, id, body, tableName } = params;
25563
+ const { tenantId, workspaceId, date, actorId, actorName } = context;
25564
+ const service = getDynamoDataService(tableName);
25565
+ return updateDataEntityById(
25566
+ service.entities.organizationaffiliation,
25567
+ tenantId,
25568
+ workspaceId,
25569
+ id,
25570
+ "OrganizationAffiliation",
25571
+ context,
25572
+ (existingResourceStr) => buildUpdatedResourceWithAudit(
25573
+ body,
25574
+ id,
25575
+ date,
25576
+ actorId,
25577
+ actorName,
25578
+ existingResourceStr,
25579
+ "OrganizationAffiliation"
25580
+ )
25581
+ );
25582
+ }
25583
+
25584
+ // src/data/rest-api/routes/data/organizationaffiliation/organizationaffiliation-update-route.ts
25585
+ async function updateOrganizationAffiliationRoute(req, res) {
25586
+ const bodyResult = requireJsonBodyAs(req, res);
25587
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
25588
+ const id = String(req.params.id);
25589
+ const ctx = req.openhiContext;
25590
+ const body = bodyResult.body;
25591
+ const resource = {
25592
+ ...body,
25593
+ resourceType: "OrganizationAffiliation",
25594
+ id
25595
+ };
25596
+ try {
25597
+ const result = await updateOrganizationAffiliationOperation({
25598
+ context: ctx,
25599
+ id,
25600
+ body: resource
25601
+ });
25602
+ return res.json(result.resource);
25603
+ } catch (err) {
25604
+ const status = domainErrorToHttpStatus(err);
25605
+ if (status === 404) {
25606
+ const diagnostics = err instanceof NotFoundError ? err.message : `OrganizationAffiliation ${id} not found`;
25607
+ return sendOperationOutcome404(res, diagnostics);
25863
25608
  }
25609
+ return sendOperationOutcome500(
25610
+ res,
25611
+ err,
25612
+ "PUT OrganizationAffiliation error:"
25613
+ );
25864
25614
  }
25865
- if (groupSqls.length === 0) {
25866
- return { sql: "", params: [] };
25615
+ }
25616
+
25617
+ // src/data/rest-api/routes/data/organizationaffiliation/organizationaffiliation.ts
25618
+ var router107 = express107.Router();
25619
+ router107.get("/", listOrganizationAffiliationsRoute);
25620
+ router107.get("/:id", getOrganizationAffiliationByIdRoute);
25621
+ router107.post("/", createOrganizationAffiliationRoute);
25622
+ router107.put("/:id", updateOrganizationAffiliationRoute);
25623
+ router107.delete("/:id", deleteOrganizationAffiliationRoute);
25624
+
25625
+ // src/data/rest-api/routes/data/patient/patient.ts
25626
+ import express108 from "express";
25627
+
25628
+ // src/data/rest-api/routes/data/patient/patient-create-route.ts
25629
+ async function createPatientRoute(req, res) {
25630
+ const bodyResult = requireJsonBodyAs(req, res);
25631
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
25632
+ const ctx = req.openhiContext;
25633
+ const body = bodyResult.body;
25634
+ const patient = {
25635
+ ...body,
25636
+ resourceType: "Patient"
25637
+ };
25638
+ try {
25639
+ const result = await createPatientOperation({
25640
+ context: ctx,
25641
+ body: patient
25642
+ });
25643
+ return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
25644
+ } catch (err) {
25645
+ return sendOperationOutcome500(res, err, "POST Patient error:");
25867
25646
  }
25868
- return { sql: groupSqls.join(" AND "), params: allParams };
25869
25647
  }
25870
25648
 
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})`);
25649
+ // src/data/operations/data/patient/patient-delete-operation.ts
25650
+ async function deletePatientOperation(params) {
25651
+ const { context, id, tableName } = params;
25652
+ const { tenantId, workspaceId } = context;
25653
+ const service = getDynamoDataService(tableName);
25654
+ await deleteDataEntityById(
25655
+ service.entities.patient,
25656
+ tenantId,
25657
+ workspaceId,
25658
+ id
25659
+ );
25660
+ }
25661
+
25662
+ // src/data/rest-api/routes/data/patient/patient-delete-route.ts
25663
+ async function deletePatientRoute(req, res) {
25664
+ const id = String(req.params.id);
25665
+ const ctx = req.openhiContext;
25666
+ try {
25667
+ await deletePatientOperation({ context: ctx, id });
25668
+ return res.status(204).send();
25669
+ } catch (err) {
25670
+ return sendOperationOutcome500(res, err, "DELETE Patient error:");
25884
25671
  }
25885
- lines.push("ORDER BY last_updated DESC");
25886
- lines.push("LIMIT :limit;");
25887
- return lines.join("\n");
25888
25672
  }
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 };
25673
+
25674
+ // src/data/operations/data/patient/patient-get-by-id-operation.ts
25675
+ async function getPatientByIdOperation(params) {
25676
+ const { context, id, tableName } = params;
25677
+ const { tenantId, workspaceId } = context;
25678
+ const service = getDynamoDataService(tableName);
25679
+ return getDataEntityById(
25680
+ service.entities.patient,
25681
+ tenantId,
25682
+ workspaceId,
25683
+ id,
25684
+ "Patient"
25685
+ );
25914
25686
  }
25915
25687
 
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"
25688
+ // src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
25689
+ async function getPatientByIdRoute(req, res) {
25690
+ const id = String(req.params.id);
25691
+ const ctx = req.openhiContext;
25692
+ try {
25693
+ const result = await getPatientByIdOperation({ context: ctx, id });
25694
+ return res.json(result.resource);
25695
+ } catch (err) {
25696
+ const status = domainErrorToHttpStatus(err);
25697
+ if (status === 404) {
25698
+ const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
25699
+ return sendOperationOutcome404(res, diagnostics);
25700
+ }
25701
+ return sendOperationOutcome500(res, err, "GET Patient error:");
25950
25702
  }
25951
- ];
25703
+ }
25952
25704
 
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] ?? [];
25705
+ // src/data/operations/data/patient/patient-list-operation.ts
25706
+ async function listPatientsOperation(params) {
25707
+ const { context, tableName, mode } = params;
25708
+ const { tenantId, workspaceId } = context;
25709
+ const service = getDynamoDataService(tableName);
25710
+ return listDataEntitiesByWorkspace(
25711
+ service.entities.patient,
25712
+ tenantId,
25713
+ workspaceId,
25714
+ mode
25715
+ );
25960
25716
  }
25961
25717
 
25962
25718
  // src/data/rest-api/routes/data/patient/patient-list-route.ts
25963
25719
  var PATIENT_RESOURCE_TYPE = "Patient";
25964
- function stripModifier(key) {
25720
+ function stripModifier2(key) {
25965
25721
  const idx = key.indexOf(":");
25966
25722
  return idx === -1 ? key : key.slice(0, idx);
25967
25723
  }
25968
- function isResultParameter(key) {
25724
+ function isResultParameter2(key) {
25969
25725
  return key.startsWith("_");
25970
25726
  }
25971
25727
  function sendInvalidSearch4003(res, diagnostics) {
@@ -25974,17 +25730,17 @@ function sendInvalidSearch4003(res, diagnostics) {
25974
25730
  issue: [{ severity: "error", code: "invalid", diagnostics }]
25975
25731
  });
25976
25732
  }
25977
- function extractSearchParamKeys(query) {
25733
+ function extractSearchParamKeys2(query) {
25978
25734
  const out = [];
25979
25735
  for (const rawKey of Object.keys(query)) {
25980
- if (isResultParameter(rawKey)) {
25736
+ if (isResultParameter2(rawKey)) {
25981
25737
  continue;
25982
25738
  }
25983
- out.push({ rawKey, code: stripModifier(rawKey) });
25739
+ out.push({ rawKey, code: stripModifier2(rawKey) });
25984
25740
  }
25985
25741
  return out;
25986
25742
  }
25987
- function buildUnknownParamDiagnostics(unknownCodes) {
25743
+ function buildUnknownParamDiagnostics2(unknownCodes) {
25988
25744
  const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
25989
25745
  const codes = unknownCodes.join(", ");
25990
25746
  const isPlural = unknownCodes.length !== 1;
@@ -25993,7 +25749,7 @@ function buildUnknownParamDiagnostics(unknownCodes) {
25993
25749
  `Valid codes: ${validCodes}.`
25994
25750
  ].join(" ");
25995
25751
  }
25996
- function findMalformedReference(query, searchParamKeys) {
25752
+ function findMalformedReference2(query, searchParamKeys) {
25997
25753
  const referenceCodes = new Set(
25998
25754
  getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
25999
25755
  );
@@ -26016,7 +25772,7 @@ function findMalformedReference(query, searchParamKeys) {
26016
25772
  return void 0;
26017
25773
  }
26018
25774
  async function listPatientsRoute(req, res) {
26019
- const searchParamKeys = extractSearchParamKeys(
25775
+ const searchParamKeys = extractSearchParamKeys2(
26020
25776
  req.query
26021
25777
  );
26022
25778
  if (searchParamKeys.length === 0) {
@@ -26034,10 +25790,10 @@ async function listPatientsRoute(req, res) {
26034
25790
  if (unknownCodes.length > 0) {
26035
25791
  return sendInvalidSearch4003(
26036
25792
  res,
26037
- buildUnknownParamDiagnostics([...new Set(unknownCodes)])
25793
+ buildUnknownParamDiagnostics2([...new Set(unknownCodes)])
26038
25794
  );
26039
25795
  }
26040
- const malformedRef = findMalformedReference(
25796
+ const malformedRef = findMalformedReference2(
26041
25797
  req.query,
26042
25798
  searchParamKeys
26043
25799
  );
@@ -29845,7 +29601,7 @@ async function listSchedulesOperation(params) {
29845
29601
  }
29846
29602
 
29847
29603
  // src/data/operations/data/schedule/schedule-search-by-actor-operation.ts
29848
- var DEFAULT_LIMIT8 = 100;
29604
+ var DEFAULT_LIMIT5 = 100;
29849
29605
  function buildSearchSchedulesByActorSql() {
29850
29606
  return [
29851
29607
  "SELECT resource_id AS id, resource",
@@ -29863,7 +29619,7 @@ async function searchSchedulesByActorOperation(params) {
29863
29619
  const { context, actorReference } = params;
29864
29620
  const { tenantId, workspaceId } = context;
29865
29621
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
29866
- const limit = params.limit ?? DEFAULT_LIMIT8;
29622
+ const limit = params.limit ?? DEFAULT_LIMIT5;
29867
29623
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
29868
29624
  shape: { kind: "array-of-references", field: "actor" },
29869
29625
  reference: actorReference,
@@ -29889,7 +29645,7 @@ async function searchSchedulesByActorOperation(params) {
29889
29645
  }
29890
29646
 
29891
29647
  // src/data/rest-api/routes/data/schedule/schedule-list-route.ts
29892
- function singleStringQueryParam3(req, name) {
29648
+ function singleStringQueryParam2(req, name) {
29893
29649
  const v = req.query[name];
29894
29650
  if (typeof v !== "string") {
29895
29651
  return void 0;
@@ -29904,7 +29660,7 @@ function sendInvalidSearch4004(res, diagnostics) {
29904
29660
  });
29905
29661
  }
29906
29662
  async function listSchedulesRoute(req, res) {
29907
- const actorRef = singleStringQueryParam3(req, "actor");
29663
+ const actorRef = singleStringQueryParam2(req, "actor");
29908
29664
  if (actorRef !== void 0) {
29909
29665
  if (parseTypedReference(actorRef) === void 0) {
29910
29666
  return sendInvalidSearch4004(
@@ -33609,7 +33365,7 @@ async function listTasksOperation(params) {
33609
33365
  }
33610
33366
 
33611
33367
  // src/data/operations/data/task/task-search-by-owner-operation.ts
33612
- var DEFAULT_LIMIT9 = 100;
33368
+ var DEFAULT_LIMIT6 = 100;
33613
33369
  function buildSearchTasksByOwnerSql() {
33614
33370
  return [
33615
33371
  "SELECT resource_id AS id, resource",
@@ -33627,7 +33383,7 @@ async function searchTasksByOwnerOperation(params) {
33627
33383
  const { context, ownerReference } = params;
33628
33384
  const { tenantId, workspaceId } = context;
33629
33385
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
33630
- const limit = params.limit ?? DEFAULT_LIMIT9;
33386
+ const limit = params.limit ?? DEFAULT_LIMIT6;
33631
33387
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
33632
33388
  shape: { kind: "scalar", field: "owner" },
33633
33389
  reference: ownerReference,
@@ -33653,7 +33409,7 @@ async function searchTasksByOwnerOperation(params) {
33653
33409
  }
33654
33410
 
33655
33411
  // src/data/operations/data/task/task-search-by-requester-operation.ts
33656
- var DEFAULT_LIMIT10 = 100;
33412
+ var DEFAULT_LIMIT7 = 100;
33657
33413
  function buildSearchTasksByRequesterSql() {
33658
33414
  return [
33659
33415
  "SELECT resource_id AS id, resource",
@@ -33671,7 +33427,7 @@ async function searchTasksByRequesterOperation(params) {
33671
33427
  const { context, requesterReference } = params;
33672
33428
  const { tenantId, workspaceId } = context;
33673
33429
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
33674
- const limit = params.limit ?? DEFAULT_LIMIT10;
33430
+ const limit = params.limit ?? DEFAULT_LIMIT7;
33675
33431
  const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
33676
33432
  shape: { kind: "scalar", field: "requester" },
33677
33433
  reference: requesterReference,
@@ -33697,7 +33453,7 @@ async function searchTasksByRequesterOperation(params) {
33697
33453
  }
33698
33454
 
33699
33455
  // src/data/rest-api/routes/data/task/task-list-route.ts
33700
- function singleStringQueryParam4(req, name) {
33456
+ function singleStringQueryParam3(req, name) {
33701
33457
  const v = req.query[name];
33702
33458
  if (typeof v !== "string") {
33703
33459
  return void 0;
@@ -33712,8 +33468,8 @@ function sendInvalidSearch4005(res, diagnostics) {
33712
33468
  });
33713
33469
  }
33714
33470
  async function listTasksRoute(req, res) {
33715
- const ownerRef = singleStringQueryParam4(req, "owner");
33716
- const requesterRef = singleStringQueryParam4(req, "requester");
33471
+ const ownerRef = singleStringQueryParam3(req, "owner");
33472
+ const requesterRef = singleStringQueryParam3(req, "requester");
33717
33473
  if (ownerRef !== void 0 && requesterRef !== void 0) {
33718
33474
  return sendInvalidSearch4005(
33719
33475
  res,