@openhi/constructs 0.0.97 → 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.
@@ -4743,6 +4743,59 @@ async function getAppointmentByIdRoute(req, res) {
4743
4743
  }
4744
4744
  }
4745
4745
 
4746
+ // src/data/operations/data/appointment/appointment-date-search-predicate.ts
4747
+ var APPOINTMENT_DATE_SEARCH_PREFIXES = [
4748
+ "gt",
4749
+ "lt",
4750
+ "ge",
4751
+ "le",
4752
+ "sa",
4753
+ "eb"
4754
+ ];
4755
+ function isAppointmentDateSearchPrefix(s) {
4756
+ return APPOINTMENT_DATE_SEARCH_PREFIXES.includes(
4757
+ s
4758
+ );
4759
+ }
4760
+ var APPT_START = "resource->>'start'";
4761
+ var APPT_END = "resource->>'end'";
4762
+ var HAS_ANY_BOUND_GUARD = `(${APPT_START} IS NOT NULL OR ${APPT_END} IS NOT NULL)`;
4763
+ function buildSinglePredicateSql(prefix, paramName) {
4764
+ switch (prefix) {
4765
+ case "gt":
4766
+ return `(${APPT_END} IS NULL OR ${APPT_END} > :${paramName})`;
4767
+ case "lt":
4768
+ return `(${APPT_START} IS NULL OR ${APPT_START} < :${paramName})`;
4769
+ case "ge":
4770
+ return `(${APPT_END} IS NULL OR ${APPT_END} >= :${paramName})`;
4771
+ case "le":
4772
+ return `(${APPT_START} IS NULL OR ${APPT_START} <= :${paramName})`;
4773
+ case "sa":
4774
+ return `(${APPT_START} IS NOT NULL AND ${APPT_START} > :${paramName})`;
4775
+ case "eb":
4776
+ return `(${APPT_END} IS NOT NULL AND ${APPT_END} < :${paramName})`;
4777
+ }
4778
+ }
4779
+ function appointmentDateConstraintParamName(index) {
4780
+ return `apptDateConstraint${index}`;
4781
+ }
4782
+ function buildAppointmentDateSearchPredicateSql(constraints) {
4783
+ if (constraints.length === 0) {
4784
+ return [];
4785
+ }
4786
+ const fragments = constraints.map(
4787
+ (c, i) => buildSinglePredicateSql(c.prefix, appointmentDateConstraintParamName(i))
4788
+ );
4789
+ fragments.push(HAS_ANY_BOUND_GUARD);
4790
+ return fragments;
4791
+ }
4792
+ function buildAppointmentDateSearchPredicateParams(constraints) {
4793
+ return constraints.map((c, i) => ({
4794
+ name: appointmentDateConstraintParamName(i),
4795
+ value: c.value
4796
+ }));
4797
+ }
4798
+
4746
4799
  // src/data/operations/data/appointment/appointment-list-operation.ts
4747
4800
  async function listAppointmentsOperation(params) {
4748
4801
  const { context, tableName, mode } = params;
@@ -4756,8 +4809,409 @@ async function listAppointmentsOperation(params) {
4756
4809
  );
4757
4810
  }
4758
4811
 
4812
+ // src/data/postgres/data-api-postgres-query-runner.ts
4813
+ import {
4814
+ ExecuteStatementCommand,
4815
+ RDSDataClient
4816
+ } from "@aws-sdk/client-rds-data";
4817
+ var DataApiPostgresQueryRunner = class {
4818
+ constructor(options) {
4819
+ this.client = options.client ?? new RDSDataClient({});
4820
+ this.clusterArn = options.clusterArn;
4821
+ this.secretArn = options.secretArn;
4822
+ this.database = options.database;
4823
+ this.schema = options.schema;
4824
+ }
4825
+ async query(sql, params) {
4826
+ const out = await this.client.send(
4827
+ new ExecuteStatementCommand({
4828
+ resourceArn: this.clusterArn,
4829
+ secretArn: this.secretArn,
4830
+ database: this.database,
4831
+ schema: this.schema,
4832
+ sql,
4833
+ parameters: params.map(toSqlParameter),
4834
+ // Results as named columns so we can map them back to JS objects.
4835
+ includeResultMetadata: true,
4836
+ // Encode JSONB results as strings, then parse client-side. Without
4837
+ // this, Data API returns JSON values inline as their underlying types
4838
+ // and complex JSONB columns get clipped.
4839
+ formatRecordsAs: "JSON"
4840
+ })
4841
+ );
4842
+ if (!out.formattedRecords) {
4843
+ return [];
4844
+ }
4845
+ return JSON.parse(out.formattedRecords);
4846
+ }
4847
+ };
4848
+ function toSqlParameter(param) {
4849
+ if (param.value === null) {
4850
+ return { name: param.name, value: { isNull: true } };
4851
+ }
4852
+ const v = param.value;
4853
+ let field;
4854
+ if (typeof v === "string") {
4855
+ field = { stringValue: v };
4856
+ } else if (typeof v === "boolean") {
4857
+ field = { booleanValue: v };
4858
+ } else if (Number.isInteger(v)) {
4859
+ field = { longValue: v };
4860
+ } else {
4861
+ field = { doubleValue: v };
4862
+ }
4863
+ return { name: param.name, value: field };
4864
+ }
4865
+
4866
+ // src/data/postgres/default-postgres-query-runner.ts
4867
+ var cached;
4868
+ function readEnv(name) {
4869
+ const v = process.env[name]?.trim();
4870
+ if (!v) {
4871
+ throw new Error(
4872
+ `Missing required env var for default PostgresQueryRunner: ${name}`
4873
+ );
4874
+ }
4875
+ return v;
4876
+ }
4877
+ function getDefaultPostgresQueryRunner() {
4878
+ if (!cached) {
4879
+ cached = new DataApiPostgresQueryRunner({
4880
+ clusterArn: readEnv("OPENHI_PG_CLUSTER_ARN"),
4881
+ secretArn: readEnv("OPENHI_PG_SECRET_ARN"),
4882
+ database: readEnv("OPENHI_PG_DATABASE"),
4883
+ schema: readEnv("OPENHI_PG_SCHEMA")
4884
+ });
4885
+ }
4886
+ return cached;
4887
+ }
4888
+
4889
+ // src/data/operations/data/appointment/appointment-search-by-date-operation.ts
4890
+ var DEFAULT_LIMIT = 100;
4891
+ function buildSearchAppointmentsByDateSql(opts) {
4892
+ const datePredicates = buildAppointmentDateSearchPredicateSql(
4893
+ opts.dateConstraints
4894
+ );
4895
+ const lines = [
4896
+ "SELECT resource_id AS id, resource",
4897
+ "FROM resources",
4898
+ "WHERE tenant_id = :tenantId",
4899
+ " AND workspace_id = :workspaceId",
4900
+ " AND resource_type = 'Appointment'",
4901
+ " AND deleted_at IS NULL"
4902
+ ];
4903
+ for (const fragment of datePredicates) {
4904
+ lines.push(` AND ${fragment}`);
4905
+ }
4906
+ lines.push("ORDER BY last_updated DESC");
4907
+ lines.push("LIMIT :limit;");
4908
+ return lines.join("\n");
4909
+ }
4910
+ async function searchAppointmentsByDateOperation(params) {
4911
+ const { context, dateConstraints } = params;
4912
+ if (dateConstraints.length === 0) {
4913
+ throw new Error(
4914
+ "searchAppointmentsByDateOperation requires at least one dateConstraint"
4915
+ );
4916
+ }
4917
+ const { tenantId, workspaceId } = context;
4918
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
4919
+ const limit = params.limit ?? DEFAULT_LIMIT;
4920
+ const sql = buildSearchAppointmentsByDateSql({ dateConstraints });
4921
+ const queryParams = [
4922
+ { name: "tenantId", value: tenantId },
4923
+ { name: "workspaceId", value: workspaceId },
4924
+ { name: "limit", value: limit },
4925
+ ...buildAppointmentDateSearchPredicateParams(dateConstraints)
4926
+ ];
4927
+ const rows = await runner.query(sql, queryParams);
4928
+ const entries = rows.map((row) => ({
4929
+ id: row.id,
4930
+ resource: {
4931
+ ...row.resource,
4932
+ id: row.id
4933
+ }
4934
+ }));
4935
+ return { entries, total: entries.length };
4936
+ }
4937
+
4938
+ // src/data/operations/data/encounter/encounter-period-search-predicate.ts
4939
+ var PERIOD_SEARCH_PREFIXES = [
4940
+ "gt",
4941
+ "lt",
4942
+ "ge",
4943
+ "le",
4944
+ "sa",
4945
+ "eb"
4946
+ ];
4947
+ function isPeriodSearchPrefix(s) {
4948
+ return PERIOD_SEARCH_PREFIXES.includes(s);
4949
+ }
4950
+ var PERIOD_START = "resource->'period'->>'start'";
4951
+ var PERIOD_END = "resource->'period'->>'end'";
4952
+ var HAS_ANY_BOUND_GUARD2 = `(${PERIOD_START} IS NOT NULL OR ${PERIOD_END} IS NOT NULL)`;
4953
+ function buildSinglePredicateSql2(prefix, paramName) {
4954
+ switch (prefix) {
4955
+ case "gt":
4956
+ return `(${PERIOD_END} IS NULL OR ${PERIOD_END} > :${paramName})`;
4957
+ case "lt":
4958
+ return `(${PERIOD_START} IS NULL OR ${PERIOD_START} < :${paramName})`;
4959
+ case "ge":
4960
+ return `(${PERIOD_END} IS NULL OR ${PERIOD_END} >= :${paramName})`;
4961
+ case "le":
4962
+ return `(${PERIOD_START} IS NULL OR ${PERIOD_START} <= :${paramName})`;
4963
+ case "sa":
4964
+ return `(${PERIOD_START} IS NOT NULL AND ${PERIOD_START} > :${paramName})`;
4965
+ case "eb":
4966
+ return `(${PERIOD_END} IS NOT NULL AND ${PERIOD_END} < :${paramName})`;
4967
+ }
4968
+ }
4969
+ function periodConstraintParamName(index) {
4970
+ return `periodConstraint${index}`;
4971
+ }
4972
+ function buildPeriodSearchPredicateSql(constraints) {
4973
+ if (constraints.length === 0) {
4974
+ return [];
4975
+ }
4976
+ const fragments = constraints.map(
4977
+ (c, i) => buildSinglePredicateSql2(c.prefix, periodConstraintParamName(i))
4978
+ );
4979
+ fragments.push(HAS_ANY_BOUND_GUARD2);
4980
+ return fragments;
4981
+ }
4982
+ function buildPeriodSearchPredicateParams(constraints) {
4983
+ return constraints.map((c, i) => ({
4984
+ name: periodConstraintParamName(i),
4985
+ value: c.value
4986
+ }));
4987
+ }
4988
+
4989
+ // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
4990
+ var DEFAULT_LIMIT2 = 100;
4991
+ function buildOpenHiResourceUrn(opts) {
4992
+ return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
4993
+ }
4994
+ function buildSearchEncountersByPatientSql(opts) {
4995
+ const periodPredicates = buildPeriodSearchPredicateSql(
4996
+ opts?.periodConstraints ?? []
4997
+ );
4998
+ const lines = [
4999
+ "SELECT resource_id AS id, resource",
5000
+ "FROM resources",
5001
+ "WHERE tenant_id = :tenantId",
5002
+ " AND workspace_id = :workspaceId",
5003
+ " AND resource_type = 'Encounter'",
5004
+ " AND deleted_at IS NULL",
5005
+ " AND (resource @> :containmentRelative::jsonb",
5006
+ " OR resource @> :containmentUrn::jsonb)"
5007
+ ];
5008
+ for (const fragment of periodPredicates) {
5009
+ lines.push(` AND ${fragment}`);
5010
+ }
5011
+ lines.push("ORDER BY last_updated DESC");
5012
+ lines.push("LIMIT :limit;");
5013
+ return lines.join("\n");
5014
+ }
5015
+ async function searchEncountersByPatientOperation(params) {
5016
+ const { context, patientId } = params;
5017
+ const periodConstraints = params.periodConstraints ?? [];
5018
+ const { tenantId, workspaceId } = context;
5019
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
5020
+ const limit = params.limit ?? DEFAULT_LIMIT2;
5021
+ const containmentRelative = JSON.stringify({
5022
+ subject: { reference: `Patient/${patientId}` }
5023
+ });
5024
+ const containmentUrn = JSON.stringify({
5025
+ subject: {
5026
+ reference: buildOpenHiResourceUrn({
5027
+ tenantId,
5028
+ workspaceId,
5029
+ resourceType: "Patient",
5030
+ resourceId: patientId
5031
+ })
5032
+ }
5033
+ });
5034
+ const sql = buildSearchEncountersByPatientSql({ periodConstraints });
5035
+ const queryParams = [
5036
+ { name: "tenantId", value: tenantId },
5037
+ { name: "workspaceId", value: workspaceId },
5038
+ { name: "containmentRelative", value: containmentRelative },
5039
+ { name: "containmentUrn", value: containmentUrn },
5040
+ { name: "limit", value: limit },
5041
+ ...buildPeriodSearchPredicateParams(periodConstraints)
5042
+ ];
5043
+ const rows = await runner.query(sql, queryParams);
5044
+ const entries = rows.map((row) => ({
5045
+ id: row.id,
5046
+ resource: {
5047
+ ...row.resource,
5048
+ id: row.id
5049
+ }
5050
+ }));
5051
+ return { entries, total: entries.length };
5052
+ }
5053
+
5054
+ // src/data/operations/data/appointment/appointment-search-by-patient-operation.ts
5055
+ var DEFAULT_LIMIT3 = 100;
5056
+ function buildSearchAppointmentsByPatientSql(opts) {
5057
+ const datePredicates = buildAppointmentDateSearchPredicateSql(
5058
+ opts?.dateConstraints ?? []
5059
+ );
5060
+ const lines = [
5061
+ "SELECT resource_id AS id, resource",
5062
+ "FROM resources",
5063
+ "WHERE tenant_id = :tenantId",
5064
+ " AND workspace_id = :workspaceId",
5065
+ " AND resource_type = 'Appointment'",
5066
+ " AND deleted_at IS NULL",
5067
+ " AND (resource @> :containmentRelative::jsonb",
5068
+ " OR resource @> :containmentUrn::jsonb)"
5069
+ ];
5070
+ for (const fragment of datePredicates) {
5071
+ lines.push(` AND ${fragment}`);
5072
+ }
5073
+ lines.push("ORDER BY last_updated DESC");
5074
+ lines.push("LIMIT :limit;");
5075
+ return lines.join("\n");
5076
+ }
5077
+ async function searchAppointmentsByPatientOperation(params) {
5078
+ const { context, patientId } = params;
5079
+ const dateConstraints = params.dateConstraints ?? [];
5080
+ const { tenantId, workspaceId } = context;
5081
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
5082
+ const limit = params.limit ?? DEFAULT_LIMIT3;
5083
+ const containmentRelative = JSON.stringify({
5084
+ participant: [{ actor: { reference: `Patient/${patientId}` } }]
5085
+ });
5086
+ const containmentUrn = JSON.stringify({
5087
+ participant: [
5088
+ {
5089
+ actor: {
5090
+ reference: buildOpenHiResourceUrn({
5091
+ tenantId,
5092
+ workspaceId,
5093
+ resourceType: "Patient",
5094
+ resourceId: patientId
5095
+ })
5096
+ }
5097
+ }
5098
+ ]
5099
+ });
5100
+ const sql = buildSearchAppointmentsByPatientSql({ dateConstraints });
5101
+ const queryParams = [
5102
+ { name: "tenantId", value: tenantId },
5103
+ { name: "workspaceId", value: workspaceId },
5104
+ { name: "containmentRelative", value: containmentRelative },
5105
+ { name: "containmentUrn", value: containmentUrn },
5106
+ { name: "limit", value: limit },
5107
+ ...buildAppointmentDateSearchPredicateParams(dateConstraints)
5108
+ ];
5109
+ const rows = await runner.query(sql, queryParams);
5110
+ const entries = rows.map((row) => ({
5111
+ id: row.id,
5112
+ resource: {
5113
+ ...row.resource,
5114
+ id: row.id
5115
+ }
5116
+ }));
5117
+ return { entries, total: entries.length };
5118
+ }
5119
+
4759
5120
  // src/data/rest-api/routes/data/appointment/appointment-list-route.ts
5121
+ function singleStringQueryParam(req, name) {
5122
+ const v = req.query[name];
5123
+ if (typeof v !== "string") {
5124
+ return void 0;
5125
+ }
5126
+ const trimmed = v.trim();
5127
+ return trimmed === "" ? void 0 : trimmed;
5128
+ }
5129
+ function isError(v) {
5130
+ return v.error !== void 0;
5131
+ }
5132
+ function parseAppointmentDateConstraints(req) {
5133
+ const raw = req.query.date;
5134
+ if (raw === void 0) {
5135
+ return [];
5136
+ }
5137
+ const values = Array.isArray(raw) ? raw : [raw];
5138
+ const out = [];
5139
+ for (const v of values) {
5140
+ if (typeof v !== "string") {
5141
+ return { error: "Each ?date= value must be a string." };
5142
+ }
5143
+ const trimmed = v.trim();
5144
+ if (trimmed === "") {
5145
+ return { error: "?date= value must not be empty." };
5146
+ }
5147
+ const prefix = trimmed.slice(0, 2);
5148
+ const datetime = trimmed.slice(2);
5149
+ if (!isAppointmentDateSearchPrefix(prefix)) {
5150
+ return {
5151
+ error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${APPOINTMENT_DATE_SEARCH_PREFIXES.join(", ")}.`
5152
+ };
5153
+ }
5154
+ if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
5155
+ return { error: `Invalid datetime in ?date=${trimmed}.` };
5156
+ }
5157
+ out.push({ prefix, value: datetime });
5158
+ }
5159
+ return out;
5160
+ }
5161
+ function sendInvalidSearch400(res, diagnostics) {
5162
+ return res.status(400).json({
5163
+ resourceType: "OperationOutcome",
5164
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
5165
+ });
5166
+ }
4760
5167
  async function listAppointmentsRoute(req, res) {
5168
+ const patientId = singleStringQueryParam(req, "patient");
5169
+ const parsed = parseAppointmentDateConstraints(req);
5170
+ if (isError(parsed)) {
5171
+ return sendInvalidSearch400(res, parsed.error);
5172
+ }
5173
+ const dateConstraints = parsed;
5174
+ if (patientId !== void 0) {
5175
+ const ctx = req.openhiContext;
5176
+ try {
5177
+ const result = await searchAppointmentsByPatientOperation({
5178
+ context: ctx,
5179
+ patientId,
5180
+ dateConstraints
5181
+ });
5182
+ const bundle = buildSearchsetBundle(
5183
+ BASE_PATH.APPOINTMENT,
5184
+ result.entries
5185
+ );
5186
+ return res.json(bundle);
5187
+ } catch (err) {
5188
+ return sendOperationOutcome500(
5189
+ res,
5190
+ err,
5191
+ "GET /Appointment?patient= search error:"
5192
+ );
5193
+ }
5194
+ }
5195
+ if (dateConstraints.length > 0) {
5196
+ const ctx = req.openhiContext;
5197
+ try {
5198
+ const result = await searchAppointmentsByDateOperation({
5199
+ context: ctx,
5200
+ dateConstraints
5201
+ });
5202
+ const bundle = buildSearchsetBundle(
5203
+ BASE_PATH.APPOINTMENT,
5204
+ result.entries
5205
+ );
5206
+ return res.json(bundle);
5207
+ } catch (err) {
5208
+ return sendOperationOutcome500(
5209
+ res,
5210
+ err,
5211
+ "GET /Appointment?date= search error:"
5212
+ );
5213
+ }
5214
+ }
4761
5215
  return handleListRoute({
4762
5216
  req,
4763
5217
  res,
@@ -12307,130 +12761,45 @@ async function listEncountersOperation(params) {
12307
12761
  );
12308
12762
  }
12309
12763
 
12310
- // src/data/postgres/data-api-postgres-query-runner.ts
12311
- import {
12312
- ExecuteStatementCommand,
12313
- RDSDataClient
12314
- } from "@aws-sdk/client-rds-data";
12315
- var DataApiPostgresQueryRunner = class {
12316
- constructor(options) {
12317
- this.client = options.client ?? new RDSDataClient({});
12318
- this.clusterArn = options.clusterArn;
12319
- this.secretArn = options.secretArn;
12320
- this.database = options.database;
12321
- this.schema = options.schema;
12322
- }
12323
- async query(sql, params) {
12324
- const out = await this.client.send(
12325
- new ExecuteStatementCommand({
12326
- resourceArn: this.clusterArn,
12327
- secretArn: this.secretArn,
12328
- database: this.database,
12329
- schema: this.schema,
12330
- sql,
12331
- parameters: params.map(toSqlParameter),
12332
- // Results as named columns so we can map them back to JS objects.
12333
- includeResultMetadata: true,
12334
- // Encode JSONB results as strings, then parse client-side. Without
12335
- // this, Data API returns JSON values inline as their underlying types
12336
- // and complex JSONB columns get clipped.
12337
- formatRecordsAs: "JSON"
12338
- })
12339
- );
12340
- if (!out.formattedRecords) {
12341
- return [];
12342
- }
12343
- return JSON.parse(out.formattedRecords);
12344
- }
12345
- };
12346
- function toSqlParameter(param) {
12347
- if (param.value === null) {
12348
- return { name: param.name, value: { isNull: true } };
12349
- }
12350
- const v = param.value;
12351
- let field;
12352
- if (typeof v === "string") {
12353
- field = { stringValue: v };
12354
- } else if (typeof v === "boolean") {
12355
- field = { booleanValue: v };
12356
- } else if (Number.isInteger(v)) {
12357
- field = { longValue: v };
12358
- } else {
12359
- field = { doubleValue: v };
12360
- }
12361
- return { name: param.name, value: field };
12362
- }
12363
-
12364
- // src/data/postgres/default-postgres-query-runner.ts
12365
- var cached;
12366
- function readEnv(name) {
12367
- const v = process.env[name]?.trim();
12368
- if (!v) {
12369
- throw new Error(
12370
- `Missing required env var for default PostgresQueryRunner: ${name}`
12371
- );
12372
- }
12373
- return v;
12374
- }
12375
- function getDefaultPostgresQueryRunner() {
12376
- if (!cached) {
12377
- cached = new DataApiPostgresQueryRunner({
12378
- clusterArn: readEnv("OPENHI_PG_CLUSTER_ARN"),
12379
- secretArn: readEnv("OPENHI_PG_SECRET_ARN"),
12380
- database: readEnv("OPENHI_PG_DATABASE"),
12381
- schema: readEnv("OPENHI_PG_SCHEMA")
12382
- });
12383
- }
12384
- return cached;
12385
- }
12386
-
12387
- // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
12388
- var DEFAULT_LIMIT = 100;
12389
- function buildOpenHiResourceUrn(opts) {
12390
- return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
12391
- }
12392
- function buildSearchEncountersByPatientSql() {
12393
- return [
12764
+ // src/data/operations/data/encounter/encounter-search-by-date-operation.ts
12765
+ var DEFAULT_LIMIT4 = 100;
12766
+ function buildSearchEncountersByDateSql(opts) {
12767
+ const periodPredicates = buildPeriodSearchPredicateSql(
12768
+ opts.periodConstraints
12769
+ );
12770
+ const lines = [
12394
12771
  "SELECT resource_id AS id, resource",
12395
12772
  "FROM resources",
12396
12773
  "WHERE tenant_id = :tenantId",
12397
12774
  " AND workspace_id = :workspaceId",
12398
12775
  " AND resource_type = 'Encounter'",
12399
- " AND deleted_at IS NULL",
12400
- " AND (resource @> :containmentRelative::jsonb",
12401
- " OR resource @> :containmentUrn::jsonb)",
12402
- "ORDER BY last_updated DESC",
12403
- "LIMIT :limit;"
12404
- ].join("\n");
12776
+ " AND deleted_at IS NULL"
12777
+ ];
12778
+ for (const fragment of periodPredicates) {
12779
+ lines.push(` AND ${fragment}`);
12780
+ }
12781
+ lines.push("ORDER BY last_updated DESC");
12782
+ lines.push("LIMIT :limit;");
12783
+ return lines.join("\n");
12405
12784
  }
12406
- async function searchEncountersByPatientOperation(params) {
12407
- const { context, patientId } = params;
12785
+ async function searchEncountersByDateOperation(params) {
12786
+ const { context, periodConstraints } = params;
12787
+ if (periodConstraints.length === 0) {
12788
+ throw new Error(
12789
+ "searchEncountersByDateOperation requires at least one periodConstraint"
12790
+ );
12791
+ }
12408
12792
  const { tenantId, workspaceId } = context;
12409
12793
  const runner = params.runner ?? getDefaultPostgresQueryRunner();
12410
- const limit = params.limit ?? DEFAULT_LIMIT;
12411
- const containmentRelative = JSON.stringify({
12412
- subject: { reference: `Patient/${patientId}` }
12413
- });
12414
- const containmentUrn = JSON.stringify({
12415
- subject: {
12416
- reference: buildOpenHiResourceUrn({
12417
- tenantId,
12418
- workspaceId,
12419
- resourceType: "Patient",
12420
- resourceId: patientId
12421
- })
12422
- }
12423
- });
12424
- const rows = await runner.query(
12425
- buildSearchEncountersByPatientSql(),
12426
- [
12427
- { name: "tenantId", value: tenantId },
12428
- { name: "workspaceId", value: workspaceId },
12429
- { name: "containmentRelative", value: containmentRelative },
12430
- { name: "containmentUrn", value: containmentUrn },
12431
- { name: "limit", value: limit }
12432
- ]
12433
- );
12794
+ const limit = params.limit ?? DEFAULT_LIMIT4;
12795
+ const sql = buildSearchEncountersByDateSql({ periodConstraints });
12796
+ const queryParams = [
12797
+ { name: "tenantId", value: tenantId },
12798
+ { name: "workspaceId", value: workspaceId },
12799
+ { name: "limit", value: limit },
12800
+ ...buildPeriodSearchPredicateParams(periodConstraints)
12801
+ ];
12802
+ const rows = await runner.query(sql, queryParams);
12434
12803
  const entries = rows.map((row) => ({
12435
12804
  id: row.id,
12436
12805
  resource: {
@@ -12442,7 +12811,7 @@ async function searchEncountersByPatientOperation(params) {
12442
12811
  }
12443
12812
 
12444
12813
  // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
12445
- function singleStringQueryParam(req, name) {
12814
+ function singleStringQueryParam2(req, name) {
12446
12815
  const v = req.query[name];
12447
12816
  if (typeof v !== "string") {
12448
12817
  return void 0;
@@ -12450,14 +12819,58 @@ function singleStringQueryParam(req, name) {
12450
12819
  const trimmed = v.trim();
12451
12820
  return trimmed === "" ? void 0 : trimmed;
12452
12821
  }
12822
+ function isError2(v) {
12823
+ return v.error !== void 0;
12824
+ }
12825
+ function parseEncounterDateConstraints(req) {
12826
+ const raw = req.query.date;
12827
+ if (raw === void 0) {
12828
+ return [];
12829
+ }
12830
+ const values = Array.isArray(raw) ? raw : [raw];
12831
+ const out = [];
12832
+ for (const v of values) {
12833
+ if (typeof v !== "string") {
12834
+ return { error: "Each ?date= value must be a string." };
12835
+ }
12836
+ const trimmed = v.trim();
12837
+ if (trimmed === "") {
12838
+ return { error: "?date= value must not be empty." };
12839
+ }
12840
+ const prefix = trimmed.slice(0, 2);
12841
+ const datetime = trimmed.slice(2);
12842
+ if (!isPeriodSearchPrefix(prefix)) {
12843
+ return {
12844
+ error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${PERIOD_SEARCH_PREFIXES.join(", ")}.`
12845
+ };
12846
+ }
12847
+ if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
12848
+ return { error: `Invalid datetime in ?date=${trimmed}.` };
12849
+ }
12850
+ out.push({ prefix, value: datetime });
12851
+ }
12852
+ return out;
12853
+ }
12854
+ function sendInvalidSearch4002(res, diagnostics) {
12855
+ return res.status(400).json({
12856
+ resourceType: "OperationOutcome",
12857
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
12858
+ });
12859
+ }
12453
12860
  async function listEncountersRoute(req, res) {
12454
- const patientId = singleStringQueryParam(req, "patient");
12455
- if (patientId) {
12861
+ const patientId = singleStringQueryParam2(req, "patient");
12862
+ const parsed = parseEncounterDateConstraints(req);
12863
+ if (isError2(parsed)) {
12864
+ return sendInvalidSearch4002(res, parsed.error);
12865
+ }
12866
+ const periodConstraints = parsed;
12867
+ if (patientId !== void 0) {
12456
12868
  const ctx = req.openhiContext;
12457
12869
  try {
12458
12870
  const result = await searchEncountersByPatientOperation({
12459
12871
  context: ctx,
12460
- patientId
12872
+ patientId,
12873
+ periodConstraints
12461
12874
  });
12462
12875
  const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
12463
12876
  return res.json(bundle);
@@ -12469,6 +12882,23 @@ async function listEncountersRoute(req, res) {
12469
12882
  );
12470
12883
  }
12471
12884
  }
12885
+ if (periodConstraints.length > 0) {
12886
+ const ctx = req.openhiContext;
12887
+ try {
12888
+ const result = await searchEncountersByDateOperation({
12889
+ context: ctx,
12890
+ periodConstraints
12891
+ });
12892
+ const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
12893
+ return res.json(bundle);
12894
+ } catch (err) {
12895
+ return sendOperationOutcome500(
12896
+ res,
12897
+ err,
12898
+ "GET /Encounter?date= search error:"
12899
+ );
12900
+ }
12901
+ }
12472
12902
  return handleListRoute({
12473
12903
  req,
12474
12904
  res,