@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
|
@@ -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/
|
|
13131
|
-
var
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13136
|
-
|
|
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
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
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
|
|
13224
|
-
const { context,
|
|
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 ??
|
|
13228
|
-
const
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
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
|
|
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 =
|
|
13272
|
-
|
|
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,
|