@openhi/constructs 0.0.97 → 0.0.99
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.
- package/lib/index.d.mts +37 -35
- package/lib/index.d.ts +37 -35
- package/lib/index.js +127 -125
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +132 -130
- package/lib/index.mjs.map +1 -1
- package/lib/rest-api-lambda.handler.js +547 -117
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +550 -120
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -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/
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
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
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
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
|
|
12407
|
-
const { context,
|
|
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 ??
|
|
12411
|
-
const
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
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
|
|
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 =
|
|
12455
|
-
|
|
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,
|