@openhi/constructs 0.0.96 → 0.0.98

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.
@@ -5563,6 +5563,59 @@ async function getAppointmentByIdRoute(req, res) {
5563
5563
  }
5564
5564
  }
5565
5565
 
5566
+ // src/data/operations/data/appointment/appointment-date-search-predicate.ts
5567
+ var APPOINTMENT_DATE_SEARCH_PREFIXES = [
5568
+ "gt",
5569
+ "lt",
5570
+ "ge",
5571
+ "le",
5572
+ "sa",
5573
+ "eb"
5574
+ ];
5575
+ function isAppointmentDateSearchPrefix(s) {
5576
+ return APPOINTMENT_DATE_SEARCH_PREFIXES.includes(
5577
+ s
5578
+ );
5579
+ }
5580
+ var APPT_START = "resource->>'start'";
5581
+ var APPT_END = "resource->>'end'";
5582
+ var HAS_ANY_BOUND_GUARD = `(${APPT_START} IS NOT NULL OR ${APPT_END} IS NOT NULL)`;
5583
+ function buildSinglePredicateSql(prefix, paramName) {
5584
+ switch (prefix) {
5585
+ case "gt":
5586
+ return `(${APPT_END} IS NULL OR ${APPT_END} > :${paramName})`;
5587
+ case "lt":
5588
+ return `(${APPT_START} IS NULL OR ${APPT_START} < :${paramName})`;
5589
+ case "ge":
5590
+ return `(${APPT_END} IS NULL OR ${APPT_END} >= :${paramName})`;
5591
+ case "le":
5592
+ return `(${APPT_START} IS NULL OR ${APPT_START} <= :${paramName})`;
5593
+ case "sa":
5594
+ return `(${APPT_START} IS NOT NULL AND ${APPT_START} > :${paramName})`;
5595
+ case "eb":
5596
+ return `(${APPT_END} IS NOT NULL AND ${APPT_END} < :${paramName})`;
5597
+ }
5598
+ }
5599
+ function appointmentDateConstraintParamName(index) {
5600
+ return `apptDateConstraint${index}`;
5601
+ }
5602
+ function buildAppointmentDateSearchPredicateSql(constraints) {
5603
+ if (constraints.length === 0) {
5604
+ return [];
5605
+ }
5606
+ const fragments = constraints.map(
5607
+ (c, i) => buildSinglePredicateSql(c.prefix, appointmentDateConstraintParamName(i))
5608
+ );
5609
+ fragments.push(HAS_ANY_BOUND_GUARD);
5610
+ return fragments;
5611
+ }
5612
+ function buildAppointmentDateSearchPredicateParams(constraints) {
5613
+ return constraints.map((c, i) => ({
5614
+ name: appointmentDateConstraintParamName(i),
5615
+ value: c.value
5616
+ }));
5617
+ }
5618
+
5566
5619
  // src/data/operations/data/appointment/appointment-list-operation.ts
5567
5620
  async function listAppointmentsOperation(params) {
5568
5621
  const { context, tableName, mode } = params;
@@ -5576,8 +5629,406 @@ async function listAppointmentsOperation(params) {
5576
5629
  );
5577
5630
  }
5578
5631
 
5632
+ // src/data/postgres/data-api-postgres-query-runner.ts
5633
+ var import_client_rds_data = require("@aws-sdk/client-rds-data");
5634
+ var DataApiPostgresQueryRunner = class {
5635
+ constructor(options) {
5636
+ this.client = options.client ?? new import_client_rds_data.RDSDataClient({});
5637
+ this.clusterArn = options.clusterArn;
5638
+ this.secretArn = options.secretArn;
5639
+ this.database = options.database;
5640
+ this.schema = options.schema;
5641
+ }
5642
+ async query(sql, params) {
5643
+ const out = await this.client.send(
5644
+ new import_client_rds_data.ExecuteStatementCommand({
5645
+ resourceArn: this.clusterArn,
5646
+ secretArn: this.secretArn,
5647
+ database: this.database,
5648
+ schema: this.schema,
5649
+ sql,
5650
+ parameters: params.map(toSqlParameter),
5651
+ // Results as named columns so we can map them back to JS objects.
5652
+ includeResultMetadata: true,
5653
+ // Encode JSONB results as strings, then parse client-side. Without
5654
+ // this, Data API returns JSON values inline as their underlying types
5655
+ // and complex JSONB columns get clipped.
5656
+ formatRecordsAs: "JSON"
5657
+ })
5658
+ );
5659
+ if (!out.formattedRecords) {
5660
+ return [];
5661
+ }
5662
+ return JSON.parse(out.formattedRecords);
5663
+ }
5664
+ };
5665
+ function toSqlParameter(param) {
5666
+ if (param.value === null) {
5667
+ return { name: param.name, value: { isNull: true } };
5668
+ }
5669
+ const v = param.value;
5670
+ let field;
5671
+ if (typeof v === "string") {
5672
+ field = { stringValue: v };
5673
+ } else if (typeof v === "boolean") {
5674
+ field = { booleanValue: v };
5675
+ } else if (Number.isInteger(v)) {
5676
+ field = { longValue: v };
5677
+ } else {
5678
+ field = { doubleValue: v };
5679
+ }
5680
+ return { name: param.name, value: field };
5681
+ }
5682
+
5683
+ // src/data/postgres/default-postgres-query-runner.ts
5684
+ var cached;
5685
+ function readEnv(name) {
5686
+ const v = process.env[name]?.trim();
5687
+ if (!v) {
5688
+ throw new Error(
5689
+ `Missing required env var for default PostgresQueryRunner: ${name}`
5690
+ );
5691
+ }
5692
+ return v;
5693
+ }
5694
+ function getDefaultPostgresQueryRunner() {
5695
+ if (!cached) {
5696
+ cached = new DataApiPostgresQueryRunner({
5697
+ clusterArn: readEnv("OPENHI_PG_CLUSTER_ARN"),
5698
+ secretArn: readEnv("OPENHI_PG_SECRET_ARN"),
5699
+ database: readEnv("OPENHI_PG_DATABASE"),
5700
+ schema: readEnv("OPENHI_PG_SCHEMA")
5701
+ });
5702
+ }
5703
+ return cached;
5704
+ }
5705
+
5706
+ // src/data/operations/data/appointment/appointment-search-by-date-operation.ts
5707
+ var DEFAULT_LIMIT = 100;
5708
+ function buildSearchAppointmentsByDateSql(opts) {
5709
+ const datePredicates = buildAppointmentDateSearchPredicateSql(
5710
+ opts.dateConstraints
5711
+ );
5712
+ const lines = [
5713
+ "SELECT resource_id AS id, resource",
5714
+ "FROM resources",
5715
+ "WHERE tenant_id = :tenantId",
5716
+ " AND workspace_id = :workspaceId",
5717
+ " AND resource_type = 'Appointment'",
5718
+ " AND deleted_at IS NULL"
5719
+ ];
5720
+ for (const fragment of datePredicates) {
5721
+ lines.push(` AND ${fragment}`);
5722
+ }
5723
+ lines.push("ORDER BY last_updated DESC");
5724
+ lines.push("LIMIT :limit;");
5725
+ return lines.join("\n");
5726
+ }
5727
+ async function searchAppointmentsByDateOperation(params) {
5728
+ const { context, dateConstraints } = params;
5729
+ if (dateConstraints.length === 0) {
5730
+ throw new Error(
5731
+ "searchAppointmentsByDateOperation requires at least one dateConstraint"
5732
+ );
5733
+ }
5734
+ const { tenantId, workspaceId } = context;
5735
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
5736
+ const limit = params.limit ?? DEFAULT_LIMIT;
5737
+ const sql = buildSearchAppointmentsByDateSql({ dateConstraints });
5738
+ const queryParams = [
5739
+ { name: "tenantId", value: tenantId },
5740
+ { name: "workspaceId", value: workspaceId },
5741
+ { name: "limit", value: limit },
5742
+ ...buildAppointmentDateSearchPredicateParams(dateConstraints)
5743
+ ];
5744
+ const rows = await runner.query(sql, queryParams);
5745
+ const entries = rows.map((row) => ({
5746
+ id: row.id,
5747
+ resource: {
5748
+ ...row.resource,
5749
+ id: row.id
5750
+ }
5751
+ }));
5752
+ return { entries, total: entries.length };
5753
+ }
5754
+
5755
+ // src/data/operations/data/encounter/encounter-period-search-predicate.ts
5756
+ var PERIOD_SEARCH_PREFIXES = [
5757
+ "gt",
5758
+ "lt",
5759
+ "ge",
5760
+ "le",
5761
+ "sa",
5762
+ "eb"
5763
+ ];
5764
+ function isPeriodSearchPrefix(s) {
5765
+ return PERIOD_SEARCH_PREFIXES.includes(s);
5766
+ }
5767
+ var PERIOD_START = "resource->'period'->>'start'";
5768
+ var PERIOD_END = "resource->'period'->>'end'";
5769
+ var HAS_ANY_BOUND_GUARD2 = `(${PERIOD_START} IS NOT NULL OR ${PERIOD_END} IS NOT NULL)`;
5770
+ function buildSinglePredicateSql2(prefix, paramName) {
5771
+ switch (prefix) {
5772
+ case "gt":
5773
+ return `(${PERIOD_END} IS NULL OR ${PERIOD_END} > :${paramName})`;
5774
+ case "lt":
5775
+ return `(${PERIOD_START} IS NULL OR ${PERIOD_START} < :${paramName})`;
5776
+ case "ge":
5777
+ return `(${PERIOD_END} IS NULL OR ${PERIOD_END} >= :${paramName})`;
5778
+ case "le":
5779
+ return `(${PERIOD_START} IS NULL OR ${PERIOD_START} <= :${paramName})`;
5780
+ case "sa":
5781
+ return `(${PERIOD_START} IS NOT NULL AND ${PERIOD_START} > :${paramName})`;
5782
+ case "eb":
5783
+ return `(${PERIOD_END} IS NOT NULL AND ${PERIOD_END} < :${paramName})`;
5784
+ }
5785
+ }
5786
+ function periodConstraintParamName(index) {
5787
+ return `periodConstraint${index}`;
5788
+ }
5789
+ function buildPeriodSearchPredicateSql(constraints) {
5790
+ if (constraints.length === 0) {
5791
+ return [];
5792
+ }
5793
+ const fragments = constraints.map(
5794
+ (c, i) => buildSinglePredicateSql2(c.prefix, periodConstraintParamName(i))
5795
+ );
5796
+ fragments.push(HAS_ANY_BOUND_GUARD2);
5797
+ return fragments;
5798
+ }
5799
+ function buildPeriodSearchPredicateParams(constraints) {
5800
+ return constraints.map((c, i) => ({
5801
+ name: periodConstraintParamName(i),
5802
+ value: c.value
5803
+ }));
5804
+ }
5805
+
5806
+ // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
5807
+ var DEFAULT_LIMIT2 = 100;
5808
+ function buildOpenHiResourceUrn(opts) {
5809
+ return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
5810
+ }
5811
+ function buildSearchEncountersByPatientSql(opts) {
5812
+ const periodPredicates = buildPeriodSearchPredicateSql(
5813
+ opts?.periodConstraints ?? []
5814
+ );
5815
+ const lines = [
5816
+ "SELECT resource_id AS id, resource",
5817
+ "FROM resources",
5818
+ "WHERE tenant_id = :tenantId",
5819
+ " AND workspace_id = :workspaceId",
5820
+ " AND resource_type = 'Encounter'",
5821
+ " AND deleted_at IS NULL",
5822
+ " AND (resource @> :containmentRelative::jsonb",
5823
+ " OR resource @> :containmentUrn::jsonb)"
5824
+ ];
5825
+ for (const fragment of periodPredicates) {
5826
+ lines.push(` AND ${fragment}`);
5827
+ }
5828
+ lines.push("ORDER BY last_updated DESC");
5829
+ lines.push("LIMIT :limit;");
5830
+ return lines.join("\n");
5831
+ }
5832
+ async function searchEncountersByPatientOperation(params) {
5833
+ const { context, patientId } = params;
5834
+ const periodConstraints = params.periodConstraints ?? [];
5835
+ const { tenantId, workspaceId } = context;
5836
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
5837
+ const limit = params.limit ?? DEFAULT_LIMIT2;
5838
+ const containmentRelative = JSON.stringify({
5839
+ subject: { reference: `Patient/${patientId}` }
5840
+ });
5841
+ const containmentUrn = JSON.stringify({
5842
+ subject: {
5843
+ reference: buildOpenHiResourceUrn({
5844
+ tenantId,
5845
+ workspaceId,
5846
+ resourceType: "Patient",
5847
+ resourceId: patientId
5848
+ })
5849
+ }
5850
+ });
5851
+ const sql = buildSearchEncountersByPatientSql({ periodConstraints });
5852
+ const queryParams = [
5853
+ { name: "tenantId", value: tenantId },
5854
+ { name: "workspaceId", value: workspaceId },
5855
+ { name: "containmentRelative", value: containmentRelative },
5856
+ { name: "containmentUrn", value: containmentUrn },
5857
+ { name: "limit", value: limit },
5858
+ ...buildPeriodSearchPredicateParams(periodConstraints)
5859
+ ];
5860
+ const rows = await runner.query(sql, queryParams);
5861
+ const entries = rows.map((row) => ({
5862
+ id: row.id,
5863
+ resource: {
5864
+ ...row.resource,
5865
+ id: row.id
5866
+ }
5867
+ }));
5868
+ return { entries, total: entries.length };
5869
+ }
5870
+
5871
+ // src/data/operations/data/appointment/appointment-search-by-patient-operation.ts
5872
+ var DEFAULT_LIMIT3 = 100;
5873
+ function buildSearchAppointmentsByPatientSql(opts) {
5874
+ const datePredicates = buildAppointmentDateSearchPredicateSql(
5875
+ opts?.dateConstraints ?? []
5876
+ );
5877
+ const lines = [
5878
+ "SELECT resource_id AS id, resource",
5879
+ "FROM resources",
5880
+ "WHERE tenant_id = :tenantId",
5881
+ " AND workspace_id = :workspaceId",
5882
+ " AND resource_type = 'Appointment'",
5883
+ " AND deleted_at IS NULL",
5884
+ " AND (resource @> :containmentRelative::jsonb",
5885
+ " OR resource @> :containmentUrn::jsonb)"
5886
+ ];
5887
+ for (const fragment of datePredicates) {
5888
+ lines.push(` AND ${fragment}`);
5889
+ }
5890
+ lines.push("ORDER BY last_updated DESC");
5891
+ lines.push("LIMIT :limit;");
5892
+ return lines.join("\n");
5893
+ }
5894
+ async function searchAppointmentsByPatientOperation(params) {
5895
+ const { context, patientId } = params;
5896
+ const dateConstraints = params.dateConstraints ?? [];
5897
+ const { tenantId, workspaceId } = context;
5898
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
5899
+ const limit = params.limit ?? DEFAULT_LIMIT3;
5900
+ const containmentRelative = JSON.stringify({
5901
+ participant: [{ actor: { reference: `Patient/${patientId}` } }]
5902
+ });
5903
+ const containmentUrn = JSON.stringify({
5904
+ participant: [
5905
+ {
5906
+ actor: {
5907
+ reference: buildOpenHiResourceUrn({
5908
+ tenantId,
5909
+ workspaceId,
5910
+ resourceType: "Patient",
5911
+ resourceId: patientId
5912
+ })
5913
+ }
5914
+ }
5915
+ ]
5916
+ });
5917
+ const sql = buildSearchAppointmentsByPatientSql({ dateConstraints });
5918
+ const queryParams = [
5919
+ { name: "tenantId", value: tenantId },
5920
+ { name: "workspaceId", value: workspaceId },
5921
+ { name: "containmentRelative", value: containmentRelative },
5922
+ { name: "containmentUrn", value: containmentUrn },
5923
+ { name: "limit", value: limit },
5924
+ ...buildAppointmentDateSearchPredicateParams(dateConstraints)
5925
+ ];
5926
+ const rows = await runner.query(sql, queryParams);
5927
+ const entries = rows.map((row) => ({
5928
+ id: row.id,
5929
+ resource: {
5930
+ ...row.resource,
5931
+ id: row.id
5932
+ }
5933
+ }));
5934
+ return { entries, total: entries.length };
5935
+ }
5936
+
5579
5937
  // src/data/rest-api/routes/data/appointment/appointment-list-route.ts
5938
+ function singleStringQueryParam(req, name) {
5939
+ const v = req.query[name];
5940
+ if (typeof v !== "string") {
5941
+ return void 0;
5942
+ }
5943
+ const trimmed = v.trim();
5944
+ return trimmed === "" ? void 0 : trimmed;
5945
+ }
5946
+ function isError(v) {
5947
+ return v.error !== void 0;
5948
+ }
5949
+ function parseAppointmentDateConstraints(req) {
5950
+ const raw = req.query.date;
5951
+ if (raw === void 0) {
5952
+ return [];
5953
+ }
5954
+ const values = Array.isArray(raw) ? raw : [raw];
5955
+ const out = [];
5956
+ for (const v of values) {
5957
+ if (typeof v !== "string") {
5958
+ return { error: "Each ?date= value must be a string." };
5959
+ }
5960
+ const trimmed = v.trim();
5961
+ if (trimmed === "") {
5962
+ return { error: "?date= value must not be empty." };
5963
+ }
5964
+ const prefix = trimmed.slice(0, 2);
5965
+ const datetime = trimmed.slice(2);
5966
+ if (!isAppointmentDateSearchPrefix(prefix)) {
5967
+ return {
5968
+ error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${APPOINTMENT_DATE_SEARCH_PREFIXES.join(", ")}.`
5969
+ };
5970
+ }
5971
+ if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
5972
+ return { error: `Invalid datetime in ?date=${trimmed}.` };
5973
+ }
5974
+ out.push({ prefix, value: datetime });
5975
+ }
5976
+ return out;
5977
+ }
5978
+ function sendInvalidSearch400(res, diagnostics) {
5979
+ return res.status(400).json({
5980
+ resourceType: "OperationOutcome",
5981
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
5982
+ });
5983
+ }
5580
5984
  async function listAppointmentsRoute(req, res) {
5985
+ const patientId = singleStringQueryParam(req, "patient");
5986
+ const parsed = parseAppointmentDateConstraints(req);
5987
+ if (isError(parsed)) {
5988
+ return sendInvalidSearch400(res, parsed.error);
5989
+ }
5990
+ const dateConstraints = parsed;
5991
+ if (patientId !== void 0) {
5992
+ const ctx = req.openhiContext;
5993
+ try {
5994
+ const result = await searchAppointmentsByPatientOperation({
5995
+ context: ctx,
5996
+ patientId,
5997
+ dateConstraints
5998
+ });
5999
+ const bundle = buildSearchsetBundle(
6000
+ BASE_PATH.APPOINTMENT,
6001
+ result.entries
6002
+ );
6003
+ return res.json(bundle);
6004
+ } catch (err) {
6005
+ return sendOperationOutcome500(
6006
+ res,
6007
+ err,
6008
+ "GET /Appointment?patient= search error:"
6009
+ );
6010
+ }
6011
+ }
6012
+ if (dateConstraints.length > 0) {
6013
+ const ctx = req.openhiContext;
6014
+ try {
6015
+ const result = await searchAppointmentsByDateOperation({
6016
+ context: ctx,
6017
+ dateConstraints
6018
+ });
6019
+ const bundle = buildSearchsetBundle(
6020
+ BASE_PATH.APPOINTMENT,
6021
+ result.entries
6022
+ );
6023
+ return res.json(bundle);
6024
+ } catch (err) {
6025
+ return sendOperationOutcome500(
6026
+ res,
6027
+ err,
6028
+ "GET /Appointment?date= search error:"
6029
+ );
6030
+ }
6031
+ }
5581
6032
  return handleListRoute({
5582
6033
  req,
5583
6034
  res,
@@ -13127,127 +13578,45 @@ async function listEncountersOperation(params) {
13127
13578
  );
13128
13579
  }
13129
13580
 
13130
- // src/data/postgres/data-api-postgres-query-runner.ts
13131
- var import_client_rds_data = require("@aws-sdk/client-rds-data");
13132
- var DataApiPostgresQueryRunner = class {
13133
- constructor(options) {
13134
- this.client = options.client ?? new import_client_rds_data.RDSDataClient({});
13135
- this.clusterArn = options.clusterArn;
13136
- this.secretArn = options.secretArn;
13137
- this.database = options.database;
13138
- this.schema = options.schema;
13139
- }
13140
- async query(sql, params) {
13141
- const out = await this.client.send(
13142
- new import_client_rds_data.ExecuteStatementCommand({
13143
- resourceArn: this.clusterArn,
13144
- secretArn: this.secretArn,
13145
- database: this.database,
13146
- schema: this.schema,
13147
- sql,
13148
- parameters: params.map(toSqlParameter),
13149
- // Results as named columns so we can map them back to JS objects.
13150
- includeResultMetadata: true,
13151
- // Encode JSONB results as strings, then parse client-side. Without
13152
- // this, Data API returns JSON values inline as their underlying types
13153
- // and complex JSONB columns get clipped.
13154
- formatRecordsAs: "JSON"
13155
- })
13156
- );
13157
- if (!out.formattedRecords) {
13158
- return [];
13159
- }
13160
- return JSON.parse(out.formattedRecords);
13161
- }
13162
- };
13163
- function toSqlParameter(param) {
13164
- if (param.value === null) {
13165
- return { name: param.name, value: { isNull: true } };
13166
- }
13167
- const v = param.value;
13168
- let field;
13169
- if (typeof v === "string") {
13170
- field = { stringValue: v };
13171
- } else if (typeof v === "boolean") {
13172
- field = { booleanValue: v };
13173
- } else if (Number.isInteger(v)) {
13174
- field = { longValue: v };
13175
- } else {
13176
- field = { doubleValue: v };
13177
- }
13178
- return { name: param.name, value: field };
13179
- }
13180
-
13181
- // src/data/postgres/default-postgres-query-runner.ts
13182
- var cached;
13183
- function readEnv(name) {
13184
- const v = process.env[name]?.trim();
13185
- if (!v) {
13186
- throw new Error(
13187
- `Missing required env var for default PostgresQueryRunner: ${name}`
13188
- );
13189
- }
13190
- return v;
13191
- }
13192
- function getDefaultPostgresQueryRunner() {
13193
- if (!cached) {
13194
- cached = new DataApiPostgresQueryRunner({
13195
- clusterArn: readEnv("OPENHI_PG_CLUSTER_ARN"),
13196
- secretArn: readEnv("OPENHI_PG_SECRET_ARN"),
13197
- database: readEnv("OPENHI_PG_DATABASE"),
13198
- schema: readEnv("OPENHI_PG_SCHEMA")
13199
- });
13200
- }
13201
- return cached;
13202
- }
13203
-
13204
- // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
13205
- var DEFAULT_LIMIT = 100;
13206
- function buildOpenHiResourceUrn(opts) {
13207
- return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
13208
- }
13209
- function buildSearchEncountersByPatientSql() {
13210
- return [
13581
+ // src/data/operations/data/encounter/encounter-search-by-date-operation.ts
13582
+ var DEFAULT_LIMIT4 = 100;
13583
+ function buildSearchEncountersByDateSql(opts) {
13584
+ const periodPredicates = buildPeriodSearchPredicateSql(
13585
+ opts.periodConstraints
13586
+ );
13587
+ const lines = [
13211
13588
  "SELECT resource_id AS id, resource",
13212
13589
  "FROM resources",
13213
13590
  "WHERE tenant_id = :tenantId",
13214
13591
  " AND workspace_id = :workspaceId",
13215
13592
  " AND resource_type = 'Encounter'",
13216
- " AND deleted_at IS NULL",
13217
- " AND (resource @> :containmentRelative::jsonb",
13218
- " OR resource @> :containmentUrn::jsonb)",
13219
- "ORDER BY last_updated DESC",
13220
- "LIMIT :limit;"
13221
- ].join("\n");
13593
+ " AND deleted_at IS NULL"
13594
+ ];
13595
+ for (const fragment of periodPredicates) {
13596
+ lines.push(` AND ${fragment}`);
13597
+ }
13598
+ lines.push("ORDER BY last_updated DESC");
13599
+ lines.push("LIMIT :limit;");
13600
+ return lines.join("\n");
13222
13601
  }
13223
- async function searchEncountersByPatientOperation(params) {
13224
- const { context, patientId } = params;
13602
+ async function searchEncountersByDateOperation(params) {
13603
+ const { context, periodConstraints } = params;
13604
+ if (periodConstraints.length === 0) {
13605
+ throw new Error(
13606
+ "searchEncountersByDateOperation requires at least one periodConstraint"
13607
+ );
13608
+ }
13225
13609
  const { tenantId, workspaceId } = context;
13226
13610
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
13227
- const limit = params.limit ?? DEFAULT_LIMIT;
13228
- const containmentRelative = JSON.stringify({
13229
- subject: { reference: `Patient/${patientId}` }
13230
- });
13231
- const containmentUrn = JSON.stringify({
13232
- subject: {
13233
- reference: buildOpenHiResourceUrn({
13234
- tenantId,
13235
- workspaceId,
13236
- resourceType: "Patient",
13237
- resourceId: patientId
13238
- })
13239
- }
13240
- });
13241
- const rows = await runner.query(
13242
- buildSearchEncountersByPatientSql(),
13243
- [
13244
- { name: "tenantId", value: tenantId },
13245
- { name: "workspaceId", value: workspaceId },
13246
- { name: "containmentRelative", value: containmentRelative },
13247
- { name: "containmentUrn", value: containmentUrn },
13248
- { name: "limit", value: limit }
13249
- ]
13250
- );
13611
+ const limit = params.limit ?? DEFAULT_LIMIT4;
13612
+ const sql = buildSearchEncountersByDateSql({ periodConstraints });
13613
+ const queryParams = [
13614
+ { name: "tenantId", value: tenantId },
13615
+ { name: "workspaceId", value: workspaceId },
13616
+ { name: "limit", value: limit },
13617
+ ...buildPeriodSearchPredicateParams(periodConstraints)
13618
+ ];
13619
+ const rows = await runner.query(sql, queryParams);
13251
13620
  const entries = rows.map((row) => ({
13252
13621
  id: row.id,
13253
13622
  resource: {
@@ -13259,7 +13628,7 @@ async function searchEncountersByPatientOperation(params) {
13259
13628
  }
13260
13629
 
13261
13630
  // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
13262
- function singleStringQueryParam(req, name) {
13631
+ function singleStringQueryParam2(req, name) {
13263
13632
  const v = req.query[name];
13264
13633
  if (typeof v !== "string") {
13265
13634
  return void 0;
@@ -13267,14 +13636,58 @@ function singleStringQueryParam(req, name) {
13267
13636
  const trimmed = v.trim();
13268
13637
  return trimmed === "" ? void 0 : trimmed;
13269
13638
  }
13639
+ function isError2(v) {
13640
+ return v.error !== void 0;
13641
+ }
13642
+ function parseEncounterDateConstraints(req) {
13643
+ const raw = req.query.date;
13644
+ if (raw === void 0) {
13645
+ return [];
13646
+ }
13647
+ const values = Array.isArray(raw) ? raw : [raw];
13648
+ const out = [];
13649
+ for (const v of values) {
13650
+ if (typeof v !== "string") {
13651
+ return { error: "Each ?date= value must be a string." };
13652
+ }
13653
+ const trimmed = v.trim();
13654
+ if (trimmed === "") {
13655
+ return { error: "?date= value must not be empty." };
13656
+ }
13657
+ const prefix = trimmed.slice(0, 2);
13658
+ const datetime = trimmed.slice(2);
13659
+ if (!isPeriodSearchPrefix(prefix)) {
13660
+ return {
13661
+ error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${PERIOD_SEARCH_PREFIXES.join(", ")}.`
13662
+ };
13663
+ }
13664
+ if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
13665
+ return { error: `Invalid datetime in ?date=${trimmed}.` };
13666
+ }
13667
+ out.push({ prefix, value: datetime });
13668
+ }
13669
+ return out;
13670
+ }
13671
+ function sendInvalidSearch4002(res, diagnostics) {
13672
+ return res.status(400).json({
13673
+ resourceType: "OperationOutcome",
13674
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
13675
+ });
13676
+ }
13270
13677
  async function listEncountersRoute(req, res) {
13271
- const patientId = singleStringQueryParam(req, "patient");
13272
- if (patientId) {
13678
+ const patientId = singleStringQueryParam2(req, "patient");
13679
+ const parsed = parseEncounterDateConstraints(req);
13680
+ if (isError2(parsed)) {
13681
+ return sendInvalidSearch4002(res, parsed.error);
13682
+ }
13683
+ const periodConstraints = parsed;
13684
+ if (patientId !== void 0) {
13273
13685
  const ctx = req.openhiContext;
13274
13686
  try {
13275
13687
  const result = await searchEncountersByPatientOperation({
13276
13688
  context: ctx,
13277
- patientId
13689
+ patientId,
13690
+ periodConstraints
13278
13691
  });
13279
13692
  const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13280
13693
  return res.json(bundle);
@@ -13286,6 +13699,23 @@ async function listEncountersRoute(req, res) {
13286
13699
  );
13287
13700
  }
13288
13701
  }
13702
+ if (periodConstraints.length > 0) {
13703
+ const ctx = req.openhiContext;
13704
+ try {
13705
+ const result = await searchEncountersByDateOperation({
13706
+ context: ctx,
13707
+ periodConstraints
13708
+ });
13709
+ const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13710
+ return res.json(bundle);
13711
+ } catch (err) {
13712
+ return sendOperationOutcome500(
13713
+ res,
13714
+ err,
13715
+ "GET /Encounter?date= search error:"
13716
+ );
13717
+ }
13718
+ }
13289
13719
  return handleListRoute({
13290
13720
  req,
13291
13721
  res,