@openhi/constructs 0.0.139 → 0.0.141
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/rest-api-lambda.handler.js +1007 -1230
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +1007 -1230
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -5405,6 +5405,89 @@ async function listAppointmentsOperation(params) {
|
|
|
5405
5405
|
);
|
|
5406
5406
|
}
|
|
5407
5407
|
|
|
5408
|
+
// src/data/search/engine/reference-predicate.ts
|
|
5409
|
+
function buildOpenHiResourceUrn(opts) {
|
|
5410
|
+
return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
|
|
5411
|
+
}
|
|
5412
|
+
var REFERENCE_CONTAINMENT_SQL_FRAGMENT = "(resource @> :containmentRelative::jsonb OR resource @> :containmentUrn::jsonb)";
|
|
5413
|
+
function parseTypedReference(s) {
|
|
5414
|
+
const match = /^([A-Za-z][A-Za-z0-9_]*)\/([^\s/]+)$/.exec(s);
|
|
5415
|
+
if (!match) {
|
|
5416
|
+
return void 0;
|
|
5417
|
+
}
|
|
5418
|
+
return { resourceType: match[1], resourceId: match[2] };
|
|
5419
|
+
}
|
|
5420
|
+
function wrapReferenceInShape(shape, reference) {
|
|
5421
|
+
switch (shape.kind) {
|
|
5422
|
+
case "scalar":
|
|
5423
|
+
return { [shape.field]: { reference } };
|
|
5424
|
+
case "array-of-references":
|
|
5425
|
+
return { [shape.field]: [{ reference }] };
|
|
5426
|
+
case "array-of-objects":
|
|
5427
|
+
return { [shape.field]: [{ [shape.subfield]: { reference } }] };
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
function buildReferenceContainmentPayload(params) {
|
|
5431
|
+
const parsed = parseTypedReference(params.reference);
|
|
5432
|
+
if (!parsed) {
|
|
5433
|
+
throw new Error(
|
|
5434
|
+
`Reference "${params.reference}" is not a valid typed reference (<ResourceType>/<id>).`
|
|
5435
|
+
);
|
|
5436
|
+
}
|
|
5437
|
+
const urn = buildOpenHiResourceUrn({
|
|
5438
|
+
tenantId: params.tenantId,
|
|
5439
|
+
workspaceId: params.workspaceId,
|
|
5440
|
+
resourceType: parsed.resourceType,
|
|
5441
|
+
resourceId: parsed.resourceId
|
|
5442
|
+
});
|
|
5443
|
+
return {
|
|
5444
|
+
containmentRelative: JSON.stringify(
|
|
5445
|
+
wrapReferenceInShape(params.shape, params.reference)
|
|
5446
|
+
),
|
|
5447
|
+
containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
function jsonbPathToReferenceShape(jsonbPath) {
|
|
5451
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5452
|
+
if (scalar) {
|
|
5453
|
+
return { kind: "scalar", field: scalar[1] };
|
|
5454
|
+
}
|
|
5455
|
+
const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
5456
|
+
if (arrayOfRefs) {
|
|
5457
|
+
return { kind: "array-of-references", field: arrayOfRefs[1] };
|
|
5458
|
+
}
|
|
5459
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
5460
|
+
jsonbPath
|
|
5461
|
+
);
|
|
5462
|
+
if (arrayOfObjs) {
|
|
5463
|
+
return {
|
|
5464
|
+
kind: "array-of-objects",
|
|
5465
|
+
field: arrayOfObjs[1],
|
|
5466
|
+
subfield: arrayOfObjs[2]
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
throw new Error(
|
|
5470
|
+
`Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
5471
|
+
);
|
|
5472
|
+
}
|
|
5473
|
+
function emitReferencePredicate(opts) {
|
|
5474
|
+
const shape = jsonbPathToReferenceShape(opts.jsonbPath);
|
|
5475
|
+
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
5476
|
+
shape,
|
|
5477
|
+
reference: opts.rawValue,
|
|
5478
|
+
tenantId: opts.context.tenantId,
|
|
5479
|
+
workspaceId: opts.context.workspaceId
|
|
5480
|
+
});
|
|
5481
|
+
const relName = `${opts.paramName}R`;
|
|
5482
|
+
const urnName = `${opts.paramName}U`;
|
|
5483
|
+
const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
|
|
5484
|
+
const params = [
|
|
5485
|
+
{ name: relName, value: containmentRelative },
|
|
5486
|
+
{ name: urnName, value: containmentUrn }
|
|
5487
|
+
];
|
|
5488
|
+
return { sql, params };
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5408
5491
|
// src/data/postgres/data-api-postgres-query-runner.ts
|
|
5409
5492
|
import {
|
|
5410
5493
|
ExecuteStatementCommand,
|
|
@@ -5554,13 +5637,17 @@ function parseDateSearchValue(raw) {
|
|
|
5554
5637
|
return { prefix: "eq", value: raw };
|
|
5555
5638
|
}
|
|
5556
5639
|
function flatJsonbExtract(jsonbPath) {
|
|
5557
|
-
const
|
|
5558
|
-
if (
|
|
5559
|
-
|
|
5560
|
-
`Generic date predicate requires a flat top-level JSONPath like "$.fieldName"; received "${jsonbPath}".`
|
|
5561
|
-
);
|
|
5640
|
+
const topLevel = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5641
|
+
if (topLevel) {
|
|
5642
|
+
return `resource->>'${topLevel[1]}'`;
|
|
5562
5643
|
}
|
|
5563
|
-
|
|
5644
|
+
const nested = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5645
|
+
if (nested) {
|
|
5646
|
+
return `resource->'${nested[1]}'->>'${nested[2]}'`;
|
|
5647
|
+
}
|
|
5648
|
+
throw new Error(
|
|
5649
|
+
`Generic date predicate requires a scalar JSONPath like "$.fieldName" or "$.field.subfield"; received "${jsonbPath}".`
|
|
5650
|
+
);
|
|
5564
5651
|
}
|
|
5565
5652
|
function emitDatePredicate(opts) {
|
|
5566
5653
|
const { jsonbPath, rawValue, paramName } = opts;
|
|
@@ -5596,129 +5683,95 @@ function emitFieldMissingPredicate(opts) {
|
|
|
5596
5683
|
const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
|
|
5597
5684
|
return { sql, params: [] };
|
|
5598
5685
|
}
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5686
|
+
|
|
5687
|
+
// src/data/search/engine/string-predicate.ts
|
|
5688
|
+
var STRING_MODIFIERS = ["exact", "contains"];
|
|
5689
|
+
function isStringModifier(s) {
|
|
5690
|
+
return STRING_MODIFIERS.includes(s);
|
|
5602
5691
|
}
|
|
5603
|
-
function
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
return `(${startExtract} IS NULL OR ${startExtract} <= :${paramName})`;
|
|
5615
|
-
case "sa":
|
|
5616
|
-
return `(${startExtract} IS NOT NULL AND ${startExtract} > :${paramName})`;
|
|
5617
|
-
case "eb":
|
|
5618
|
-
return `(${endExtract} IS NOT NULL AND ${endExtract} < :${paramName})`;
|
|
5619
|
-
}
|
|
5620
|
-
}
|
|
5621
|
-
function buildIntervalDateSearchPredicateSql(constraints, opts) {
|
|
5622
|
-
if (constraints.length === 0) {
|
|
5623
|
-
return [];
|
|
5624
|
-
}
|
|
5625
|
-
const paramPrefix = opts.paramNamePrefix ?? DEFAULT_INTERVAL_PARAM_PREFIX;
|
|
5626
|
-
const startExtract = flatJsonbExtract(opts.startPath);
|
|
5627
|
-
const endExtract = flatJsonbExtract(opts.endPath);
|
|
5628
|
-
const fragments = constraints.map(
|
|
5629
|
-
(c, i) => buildIntervalSingleSql(
|
|
5630
|
-
c.prefix,
|
|
5631
|
-
startExtract,
|
|
5632
|
-
endExtract,
|
|
5633
|
-
intervalConstraintParamName(paramPrefix, i)
|
|
5634
|
-
)
|
|
5692
|
+
function jsonbPathToStringShape(jsonbPath) {
|
|
5693
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5694
|
+
if (scalar) {
|
|
5695
|
+
return { kind: "scalar", field: scalar[1] };
|
|
5696
|
+
}
|
|
5697
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
5698
|
+
if (arrayOfScalars) {
|
|
5699
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
5700
|
+
}
|
|
5701
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
5702
|
+
jsonbPath
|
|
5635
5703
|
);
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
}
|
|
5646
|
-
var APPOINTMENT_DATE_SEARCH_PREFIXES = [
|
|
5647
|
-
"gt",
|
|
5648
|
-
"lt",
|
|
5649
|
-
"ge",
|
|
5650
|
-
"le",
|
|
5651
|
-
"sa",
|
|
5652
|
-
"eb"
|
|
5653
|
-
];
|
|
5654
|
-
function isAppointmentDateSearchPrefix(s) {
|
|
5655
|
-
return APPOINTMENT_DATE_SEARCH_PREFIXES.includes(
|
|
5656
|
-
s
|
|
5704
|
+
if (arrayOfObjs) {
|
|
5705
|
+
return {
|
|
5706
|
+
kind: "array-of-objects",
|
|
5707
|
+
field: arrayOfObjs[1],
|
|
5708
|
+
subfield: arrayOfObjs[2]
|
|
5709
|
+
};
|
|
5710
|
+
}
|
|
5711
|
+
throw new Error(
|
|
5712
|
+
`String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
5657
5713
|
);
|
|
5658
5714
|
}
|
|
5659
|
-
function
|
|
5660
|
-
return
|
|
5661
|
-
startPath: "$.start",
|
|
5662
|
-
endPath: "$.end",
|
|
5663
|
-
paramNamePrefix: "apptDateConstraint"
|
|
5664
|
-
});
|
|
5665
|
-
}
|
|
5666
|
-
function buildAppointmentDateSearchPredicateParams(constraints) {
|
|
5667
|
-
return buildIntervalDateSearchPredicateParams(constraints, {
|
|
5668
|
-
paramNamePrefix: "apptDateConstraint"
|
|
5669
|
-
});
|
|
5670
|
-
}
|
|
5671
|
-
|
|
5672
|
-
// src/data/search/engine/reference-predicate.ts
|
|
5673
|
-
function buildOpenHiResourceUrn(opts) {
|
|
5674
|
-
return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
|
|
5715
|
+
function escapeLikePattern(value) {
|
|
5716
|
+
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
5675
5717
|
}
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5718
|
+
function buildLikePattern(value, modifier) {
|
|
5719
|
+
const escaped = escapeLikePattern(value);
|
|
5720
|
+
switch (modifier) {
|
|
5721
|
+
case "exact":
|
|
5722
|
+
return escaped;
|
|
5723
|
+
case "contains":
|
|
5724
|
+
return `%${escaped}%`;
|
|
5725
|
+
case void 0:
|
|
5726
|
+
return `${escaped}%`;
|
|
5681
5727
|
}
|
|
5682
|
-
return { resourceType: match[1], resourceId: match[2] };
|
|
5683
5728
|
}
|
|
5684
|
-
function
|
|
5729
|
+
function buildIlikeExtractSql(shape, paramName) {
|
|
5685
5730
|
switch (shape.kind) {
|
|
5686
5731
|
case "scalar":
|
|
5687
|
-
return {
|
|
5688
|
-
case "array-of-
|
|
5689
|
-
return
|
|
5732
|
+
return `resource->>'${shape.field}' ILIKE :${paramName}`;
|
|
5733
|
+
case "array-of-scalars":
|
|
5734
|
+
return [
|
|
5735
|
+
"EXISTS (SELECT 1 FROM",
|
|
5736
|
+
`jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
|
|
5737
|
+
`WHERE s_elem.text_val ILIKE :${paramName})`
|
|
5738
|
+
].join(" ");
|
|
5690
5739
|
case "array-of-objects":
|
|
5691
|
-
return
|
|
5740
|
+
return [
|
|
5741
|
+
"EXISTS (SELECT 1 FROM",
|
|
5742
|
+
`jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
|
|
5743
|
+
`WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
|
|
5744
|
+
].join(" ");
|
|
5692
5745
|
}
|
|
5693
5746
|
}
|
|
5694
|
-
function
|
|
5695
|
-
const
|
|
5696
|
-
|
|
5747
|
+
function emitStringPredicate(opts) {
|
|
5748
|
+
const shape = jsonbPathToStringShape(opts.jsonbPath);
|
|
5749
|
+
const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
|
|
5750
|
+
const sql = buildIlikeExtractSql(shape, opts.paramName);
|
|
5751
|
+
const pattern = buildLikePattern(opts.rawValue, modifier);
|
|
5752
|
+
const params = [
|
|
5753
|
+
{ name: opts.paramName, value: pattern }
|
|
5754
|
+
];
|
|
5755
|
+
return { sql, params };
|
|
5756
|
+
}
|
|
5757
|
+
function checkModifier(modifier) {
|
|
5758
|
+
if (!isStringModifier(modifier)) {
|
|
5697
5759
|
throw new Error(
|
|
5698
|
-
`
|
|
5760
|
+
`String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
|
|
5699
5761
|
);
|
|
5700
5762
|
}
|
|
5701
|
-
|
|
5702
|
-
tenantId: params.tenantId,
|
|
5703
|
-
workspaceId: params.workspaceId,
|
|
5704
|
-
resourceType: parsed.resourceType,
|
|
5705
|
-
resourceId: parsed.resourceId
|
|
5706
|
-
});
|
|
5707
|
-
return {
|
|
5708
|
-
containmentRelative: JSON.stringify(
|
|
5709
|
-
wrapReferenceInShape(params.shape, params.reference)
|
|
5710
|
-
),
|
|
5711
|
-
containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
|
|
5712
|
-
};
|
|
5763
|
+
return modifier;
|
|
5713
5764
|
}
|
|
5714
|
-
|
|
5765
|
+
|
|
5766
|
+
// src/data/search/engine/token-predicate.ts
|
|
5767
|
+
function jsonbPathToTokenShape(jsonbPath) {
|
|
5715
5768
|
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5716
5769
|
if (scalar) {
|
|
5717
5770
|
return { kind: "scalar", field: scalar[1] };
|
|
5718
5771
|
}
|
|
5719
|
-
const
|
|
5720
|
-
if (
|
|
5721
|
-
return { kind: "array-of-
|
|
5772
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
5773
|
+
if (arrayOfScalars) {
|
|
5774
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
5722
5775
|
}
|
|
5723
5776
|
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
5724
5777
|
jsonbPath
|
|
@@ -5731,240 +5784,408 @@ function jsonbPathToReferenceShape(jsonbPath) {
|
|
|
5731
5784
|
};
|
|
5732
5785
|
}
|
|
5733
5786
|
throw new Error(
|
|
5734
|
-
`
|
|
5787
|
+
`Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
5735
5788
|
);
|
|
5736
5789
|
}
|
|
5737
|
-
function
|
|
5738
|
-
const
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
}
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5790
|
+
function parseTokenValue(raw) {
|
|
5791
|
+
const idx = raw.indexOf("|");
|
|
5792
|
+
if (idx === -1) {
|
|
5793
|
+
return { code: raw };
|
|
5794
|
+
}
|
|
5795
|
+
const system = raw.slice(0, idx);
|
|
5796
|
+
const code = raw.slice(idx + 1);
|
|
5797
|
+
return system.length > 0 ? { system, code } : { code };
|
|
5798
|
+
}
|
|
5799
|
+
function wrapTokenInShape(shape, value) {
|
|
5800
|
+
switch (shape.kind) {
|
|
5801
|
+
case "scalar":
|
|
5802
|
+
return { [shape.field]: value };
|
|
5803
|
+
case "array-of-scalars":
|
|
5804
|
+
return { [shape.field]: [value] };
|
|
5805
|
+
case "array-of-objects":
|
|
5806
|
+
return { [shape.field]: [{ [shape.subfield]: value }] };
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
function emitTokenPredicate(opts) {
|
|
5810
|
+
const shape = jsonbPathToTokenShape(opts.jsonbPath);
|
|
5811
|
+
const { code } = parseTokenValue(opts.rawValue);
|
|
5812
|
+
const payload = wrapTokenInShape(shape, code);
|
|
5813
|
+
const sql = `resource @> :${opts.paramName}::jsonb`;
|
|
5748
5814
|
const params = [
|
|
5749
|
-
{ name:
|
|
5750
|
-
{ name: urnName, value: containmentUrn }
|
|
5815
|
+
{ name: opts.paramName, value: JSON.stringify(payload) }
|
|
5751
5816
|
];
|
|
5752
5817
|
return { sql, params };
|
|
5753
5818
|
}
|
|
5754
5819
|
|
|
5755
|
-
// src/data/
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
);
|
|
5761
|
-
const lines = [
|
|
5762
|
-
"SELECT resource_id AS id, resource",
|
|
5763
|
-
"FROM resources",
|
|
5764
|
-
"WHERE tenant_id = :tenantId",
|
|
5765
|
-
" AND workspace_id = :workspaceId",
|
|
5766
|
-
" AND resource_type = 'Appointment'",
|
|
5767
|
-
" AND deleted_at IS NULL",
|
|
5768
|
-
` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`
|
|
5769
|
-
];
|
|
5770
|
-
for (const fragment of datePredicates) {
|
|
5771
|
-
lines.push(` AND ${fragment}`);
|
|
5820
|
+
// src/data/search/engine/combinator.ts
|
|
5821
|
+
function parseQueryKey(key) {
|
|
5822
|
+
const idx = key.indexOf(":");
|
|
5823
|
+
if (idx === -1) {
|
|
5824
|
+
return { code: key, modifier: void 0 };
|
|
5772
5825
|
}
|
|
5773
|
-
|
|
5774
|
-
lines.push("LIMIT :limit;");
|
|
5775
|
-
return lines.join("\n");
|
|
5826
|
+
return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
|
|
5776
5827
|
}
|
|
5777
|
-
|
|
5778
|
-
const
|
|
5779
|
-
const
|
|
5780
|
-
const
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
5784
|
-
shape: {
|
|
5785
|
-
kind: "array-of-objects",
|
|
5786
|
-
field: "participant",
|
|
5787
|
-
subfield: "actor"
|
|
5788
|
-
},
|
|
5789
|
-
reference: actorReference,
|
|
5790
|
-
tenantId,
|
|
5791
|
-
workspaceId
|
|
5792
|
-
});
|
|
5793
|
-
const sql = buildSearchAppointmentsByActorSql({ dateConstraints });
|
|
5794
|
-
const queryParams = [
|
|
5795
|
-
{ name: "tenantId", value: tenantId },
|
|
5796
|
-
{ name: "workspaceId", value: workspaceId },
|
|
5797
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
5798
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
5799
|
-
{ name: "limit", value: limit },
|
|
5800
|
-
...buildAppointmentDateSearchPredicateParams(dateConstraints)
|
|
5801
|
-
];
|
|
5802
|
-
const rows = await runner.query(sql, queryParams);
|
|
5803
|
-
const entries = rows.map((row) => ({
|
|
5804
|
-
id: row.id,
|
|
5805
|
-
resource: {
|
|
5806
|
-
...row.resource,
|
|
5807
|
-
id: row.id
|
|
5828
|
+
function flattenQueryValues(raw) {
|
|
5829
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
5830
|
+
const out = [];
|
|
5831
|
+
for (const v of list) {
|
|
5832
|
+
for (const piece of v.split(",")) {
|
|
5833
|
+
out.push(piece);
|
|
5808
5834
|
}
|
|
5809
|
-
}
|
|
5810
|
-
return
|
|
5835
|
+
}
|
|
5836
|
+
return out;
|
|
5811
5837
|
}
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5838
|
+
function parseQueryEntries(query) {
|
|
5839
|
+
const entries = [];
|
|
5840
|
+
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
5841
|
+
if (rawValue === void 0) continue;
|
|
5842
|
+
const { code, modifier } = parseQueryKey(rawKey);
|
|
5843
|
+
entries.push({
|
|
5844
|
+
code,
|
|
5845
|
+
modifier,
|
|
5846
|
+
values: flattenQueryValues(rawValue)
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
return entries;
|
|
5850
|
+
}
|
|
5851
|
+
function findParam(params, code) {
|
|
5852
|
+
return params.find((p) => p.code === code);
|
|
5853
|
+
}
|
|
5854
|
+
function checkModifierAllowed(modifier, param) {
|
|
5855
|
+
const universal = modifier === "missing" || modifier === "not";
|
|
5856
|
+
const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
|
|
5857
|
+
if (universal || stringNative) {
|
|
5858
|
+
if (param.modifiers && !param.modifiers.includes(modifier)) {
|
|
5859
|
+
throw new Error(
|
|
5860
|
+
`Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
return;
|
|
5864
|
+
}
|
|
5865
|
+
throw new Error(
|
|
5866
|
+
`Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
|
|
5818
5867
|
);
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
lines.push(` AND ${fragment}`);
|
|
5868
|
+
}
|
|
5869
|
+
function emitOne(opts) {
|
|
5870
|
+
const { param, modifier, rawValue, paramName, context } = opts;
|
|
5871
|
+
if (modifier === "missing") {
|
|
5872
|
+
const missing = parseMissingValue(rawValue);
|
|
5873
|
+
return emitFieldMissingPredicate({
|
|
5874
|
+
jsonbPath: param.jsonbPath,
|
|
5875
|
+
missing
|
|
5876
|
+
});
|
|
5829
5877
|
}
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5878
|
+
const negate = modifier === "not";
|
|
5879
|
+
const effectiveModifier = negate ? void 0 : modifier;
|
|
5880
|
+
const inner = emitForType({
|
|
5881
|
+
paramType: param.type,
|
|
5882
|
+
jsonbPath: param.jsonbPath,
|
|
5883
|
+
rawValue,
|
|
5884
|
+
paramName,
|
|
5885
|
+
modifier: effectiveModifier,
|
|
5886
|
+
context
|
|
5887
|
+
});
|
|
5888
|
+
if (!negate) {
|
|
5889
|
+
return inner;
|
|
5890
|
+
}
|
|
5891
|
+
return { sql: `NOT (${inner.sql})`, params: inner.params };
|
|
5833
5892
|
}
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
if (
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5893
|
+
function parseMissingValue(raw) {
|
|
5894
|
+
if (raw === "true") return true;
|
|
5895
|
+
if (raw === "false") return false;
|
|
5896
|
+
throw new Error(
|
|
5897
|
+
`:missing requires a value of "true" or "false"; received "${raw}".`
|
|
5898
|
+
);
|
|
5899
|
+
}
|
|
5900
|
+
function emitForType(opts) {
|
|
5901
|
+
switch (opts.paramType) {
|
|
5902
|
+
case "token":
|
|
5903
|
+
return emitTokenPredicate({
|
|
5904
|
+
jsonbPath: opts.jsonbPath,
|
|
5905
|
+
rawValue: opts.rawValue,
|
|
5906
|
+
paramName: opts.paramName,
|
|
5907
|
+
context: opts.context
|
|
5908
|
+
});
|
|
5909
|
+
case "date":
|
|
5910
|
+
return emitDatePredicate({
|
|
5911
|
+
jsonbPath: opts.jsonbPath,
|
|
5912
|
+
rawValue: opts.rawValue,
|
|
5913
|
+
paramName: opts.paramName,
|
|
5914
|
+
context: opts.context
|
|
5915
|
+
});
|
|
5916
|
+
case "reference":
|
|
5917
|
+
return emitReferencePredicate({
|
|
5918
|
+
jsonbPath: opts.jsonbPath,
|
|
5919
|
+
rawValue: opts.rawValue,
|
|
5920
|
+
paramName: opts.paramName,
|
|
5921
|
+
context: opts.context
|
|
5922
|
+
});
|
|
5923
|
+
case "string":
|
|
5924
|
+
return emitStringPredicate({
|
|
5925
|
+
jsonbPath: opts.jsonbPath,
|
|
5926
|
+
rawValue: opts.rawValue,
|
|
5927
|
+
paramName: opts.paramName,
|
|
5928
|
+
modifier: opts.modifier,
|
|
5929
|
+
context: opts.context
|
|
5930
|
+
});
|
|
5840
5931
|
}
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
const
|
|
5844
|
-
const
|
|
5845
|
-
const
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
const rows = await runner.query(sql, queryParams);
|
|
5852
|
-
const entries = rows.map((row) => ({
|
|
5853
|
-
id: row.id,
|
|
5854
|
-
resource: {
|
|
5855
|
-
...row.resource,
|
|
5856
|
-
id: row.id
|
|
5932
|
+
}
|
|
5933
|
+
function combineSearchPredicates(opts) {
|
|
5934
|
+
const entries = parseQueryEntries(opts.query);
|
|
5935
|
+
const groupSqls = [];
|
|
5936
|
+
const allParams = [];
|
|
5937
|
+
let paramIdx = 0;
|
|
5938
|
+
for (const entry of entries) {
|
|
5939
|
+
const registered = findParam(opts.registeredParams, entry.code);
|
|
5940
|
+
if (!registered) {
|
|
5941
|
+
continue;
|
|
5857
5942
|
}
|
|
5858
|
-
|
|
5859
|
-
|
|
5943
|
+
if (entry.modifier !== void 0) {
|
|
5944
|
+
checkModifierAllowed(entry.modifier, registered);
|
|
5945
|
+
}
|
|
5946
|
+
if (entry.values.length === 0) {
|
|
5947
|
+
continue;
|
|
5948
|
+
}
|
|
5949
|
+
const fragmentSqls = [];
|
|
5950
|
+
let valueIdx = 0;
|
|
5951
|
+
for (const rawValue of entry.values) {
|
|
5952
|
+
const paramName = `p${paramIdx}v${valueIdx}`;
|
|
5953
|
+
const frag = emitOne({
|
|
5954
|
+
param: registered,
|
|
5955
|
+
modifier: entry.modifier,
|
|
5956
|
+
rawValue,
|
|
5957
|
+
paramName,
|
|
5958
|
+
context: opts.context
|
|
5959
|
+
});
|
|
5960
|
+
fragmentSqls.push(frag.sql);
|
|
5961
|
+
for (const p of frag.params) {
|
|
5962
|
+
allParams.push(p);
|
|
5963
|
+
}
|
|
5964
|
+
valueIdx++;
|
|
5965
|
+
}
|
|
5966
|
+
paramIdx++;
|
|
5967
|
+
if (fragmentSqls.length === 1) {
|
|
5968
|
+
groupSqls.push(fragmentSqls[0]);
|
|
5969
|
+
} else {
|
|
5970
|
+
groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
if (groupSqls.length === 0) {
|
|
5974
|
+
return { sql: "", params: [] };
|
|
5975
|
+
}
|
|
5976
|
+
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
5860
5977
|
}
|
|
5861
5978
|
|
|
5862
|
-
// src/data/operations/
|
|
5863
|
-
var
|
|
5864
|
-
function
|
|
5865
|
-
const datePredicates = buildAppointmentDateSearchPredicateSql(
|
|
5866
|
-
opts?.dateConstraints ?? []
|
|
5867
|
-
);
|
|
5979
|
+
// src/data/search/operations/generic-search-operation.ts
|
|
5980
|
+
var DEFAULT_LIMIT = 100;
|
|
5981
|
+
function buildGenericSearchSql(opts) {
|
|
5868
5982
|
const lines = [
|
|
5869
5983
|
"SELECT resource_id AS id, resource",
|
|
5870
5984
|
"FROM resources",
|
|
5871
5985
|
"WHERE tenant_id = :tenantId",
|
|
5872
5986
|
" AND workspace_id = :workspaceId",
|
|
5873
|
-
" AND resource_type =
|
|
5874
|
-
" AND deleted_at IS NULL"
|
|
5875
|
-
" AND (resource @> :containmentRelative::jsonb",
|
|
5876
|
-
" OR resource @> :containmentUrn::jsonb)"
|
|
5987
|
+
" AND resource_type = :resourceType",
|
|
5988
|
+
" AND deleted_at IS NULL"
|
|
5877
5989
|
];
|
|
5878
|
-
|
|
5879
|
-
lines.push(` AND ${
|
|
5990
|
+
if (opts.combinedSql.length > 0) {
|
|
5991
|
+
lines.push(` AND (${opts.combinedSql})`);
|
|
5880
5992
|
}
|
|
5881
5993
|
lines.push("ORDER BY last_updated DESC");
|
|
5882
5994
|
lines.push("LIMIT :limit;");
|
|
5883
5995
|
return lines.join("\n");
|
|
5884
5996
|
}
|
|
5885
|
-
async function
|
|
5886
|
-
const {
|
|
5887
|
-
const dateConstraints = params.dateConstraints ?? [];
|
|
5888
|
-
const { tenantId, workspaceId } = context;
|
|
5997
|
+
async function genericSearchOperation(params) {
|
|
5998
|
+
const { resourceType, tenantId, workspaceId, query, resolver } = params;
|
|
5889
5999
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
5890
|
-
const limit = params.limit ??
|
|
5891
|
-
const
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
actor: {
|
|
5898
|
-
reference: buildOpenHiResourceUrn({
|
|
5899
|
-
tenantId,
|
|
5900
|
-
workspaceId,
|
|
5901
|
-
resourceType: "Patient",
|
|
5902
|
-
resourceId: patientId
|
|
5903
|
-
})
|
|
5904
|
-
}
|
|
5905
|
-
}
|
|
5906
|
-
]
|
|
6000
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
6001
|
+
const registeredParams = resolver(resourceType, tenantId);
|
|
6002
|
+
const context = { tenantId, workspaceId, resourceType };
|
|
6003
|
+
const combined = combineSearchPredicates({
|
|
6004
|
+
query,
|
|
6005
|
+
registeredParams,
|
|
6006
|
+
context
|
|
5907
6007
|
});
|
|
5908
|
-
const sql =
|
|
6008
|
+
const sql = buildGenericSearchSql({ combinedSql: combined.sql });
|
|
5909
6009
|
const queryParams = [
|
|
5910
6010
|
{ name: "tenantId", value: tenantId },
|
|
5911
6011
|
{ name: "workspaceId", value: workspaceId },
|
|
5912
|
-
{ name: "
|
|
5913
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
6012
|
+
{ name: "resourceType", value: resourceType },
|
|
5914
6013
|
{ name: "limit", value: limit },
|
|
5915
|
-
...
|
|
6014
|
+
...combined.params
|
|
5916
6015
|
];
|
|
5917
6016
|
const rows = await runner.query(sql, queryParams);
|
|
5918
6017
|
const entries = rows.map((row) => ({
|
|
5919
6018
|
id: row.id,
|
|
5920
|
-
resource: {
|
|
5921
|
-
...row.resource,
|
|
5922
|
-
id: row.id
|
|
5923
|
-
}
|
|
6019
|
+
resource: { ...row.resource, id: row.id }
|
|
5924
6020
|
}));
|
|
5925
6021
|
return { entries, total: entries.length };
|
|
5926
6022
|
}
|
|
5927
6023
|
|
|
5928
|
-
// src/data/
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
}
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
6024
|
+
// src/data/search/registry/appointment-search-parameters.ts
|
|
6025
|
+
var APPOINTMENT_SEARCH_PARAMETERS = [
|
|
6026
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
6027
|
+
{ code: "date", type: "date", jsonbPath: "$.start" },
|
|
6028
|
+
{
|
|
6029
|
+
code: "patient",
|
|
6030
|
+
type: "reference",
|
|
6031
|
+
jsonbPath: "$.participant[*].actor"
|
|
6032
|
+
},
|
|
6033
|
+
{
|
|
6034
|
+
code: "actor",
|
|
6035
|
+
type: "reference",
|
|
6036
|
+
jsonbPath: "$.participant[*].actor"
|
|
6037
|
+
},
|
|
6038
|
+
{
|
|
6039
|
+
code: "practitioner",
|
|
6040
|
+
type: "reference",
|
|
6041
|
+
jsonbPath: "$.participant[*].actor"
|
|
6042
|
+
},
|
|
6043
|
+
{ code: "service-type", type: "token", jsonbPath: "$.serviceType[*]" },
|
|
6044
|
+
{
|
|
6045
|
+
code: "service-category",
|
|
6046
|
+
type: "token",
|
|
6047
|
+
jsonbPath: "$.serviceCategory[*]"
|
|
6048
|
+
},
|
|
6049
|
+
{ code: "specialty", type: "token", jsonbPath: "$.specialty[*]" },
|
|
6050
|
+
{
|
|
6051
|
+
code: "appointment-type",
|
|
6052
|
+
type: "token",
|
|
6053
|
+
jsonbPath: "$.appointmentType"
|
|
6054
|
+
},
|
|
6055
|
+
{ code: "slot", type: "reference", jsonbPath: "$.slot[*]" }
|
|
6056
|
+
];
|
|
6057
|
+
|
|
6058
|
+
// src/data/search/registry/encounter-search-parameters.ts
|
|
6059
|
+
var ENCOUNTER_SEARCH_PARAMETERS = [
|
|
6060
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
6061
|
+
{ code: "class", type: "token", jsonbPath: "$.class" },
|
|
6062
|
+
{ code: "type", type: "token", jsonbPath: "$.type[*]" },
|
|
6063
|
+
{ code: "subject", type: "reference", jsonbPath: "$.subject" },
|
|
6064
|
+
{ code: "patient", type: "reference", jsonbPath: "$.subject" },
|
|
6065
|
+
{
|
|
6066
|
+
code: "participant",
|
|
6067
|
+
type: "reference",
|
|
6068
|
+
jsonbPath: "$.participant[*].individual"
|
|
6069
|
+
},
|
|
6070
|
+
{ code: "date", type: "date", jsonbPath: "$.period.start" },
|
|
6071
|
+
{
|
|
6072
|
+
code: "service-provider",
|
|
6073
|
+
type: "reference",
|
|
6074
|
+
jsonbPath: "$.serviceProvider"
|
|
6075
|
+
},
|
|
6076
|
+
{ code: "appointment", type: "reference", jsonbPath: "$.appointment[*]" },
|
|
6077
|
+
{
|
|
6078
|
+
code: "episode-of-care",
|
|
6079
|
+
type: "reference",
|
|
6080
|
+
jsonbPath: "$.episodeOfCare[*]"
|
|
5966
6081
|
}
|
|
5967
|
-
|
|
6082
|
+
];
|
|
6083
|
+
|
|
6084
|
+
// src/data/search/registry/observation-search-parameters.ts
|
|
6085
|
+
var OBSERVATION_SEARCH_PARAMETERS = [
|
|
6086
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
6087
|
+
{ code: "category", type: "token", jsonbPath: "$.category[*]" },
|
|
6088
|
+
{ code: "code", type: "token", jsonbPath: "$.code" },
|
|
6089
|
+
{ code: "subject", type: "reference", jsonbPath: "$.subject" },
|
|
6090
|
+
{ code: "patient", type: "reference", jsonbPath: "$.subject" },
|
|
6091
|
+
{ code: "encounter", type: "reference", jsonbPath: "$.encounter" },
|
|
6092
|
+
{ code: "performer", type: "reference", jsonbPath: "$.performer[*]" },
|
|
6093
|
+
{ code: "date", type: "date", jsonbPath: "$.effectiveDateTime" },
|
|
6094
|
+
{
|
|
6095
|
+
code: "value-string",
|
|
6096
|
+
type: "string",
|
|
6097
|
+
jsonbPath: "$.valueString",
|
|
6098
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
6099
|
+
},
|
|
6100
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*]" },
|
|
6101
|
+
{ code: "based-on", type: "reference", jsonbPath: "$.basedOn[*]" },
|
|
6102
|
+
{ code: "part-of", type: "reference", jsonbPath: "$.partOf[*]" }
|
|
6103
|
+
];
|
|
6104
|
+
|
|
6105
|
+
// src/data/search/registry/patient-search-parameters.ts
|
|
6106
|
+
var PATIENT_SEARCH_PARAMETERS = [
|
|
6107
|
+
{ code: "gender", type: "token", jsonbPath: "$.gender" },
|
|
6108
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
|
|
6109
|
+
{ code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
|
|
6110
|
+
{
|
|
6111
|
+
code: "name",
|
|
6112
|
+
type: "string",
|
|
6113
|
+
jsonbPath: "$.name[*].text",
|
|
6114
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
6115
|
+
},
|
|
6116
|
+
{
|
|
6117
|
+
code: "family",
|
|
6118
|
+
type: "string",
|
|
6119
|
+
jsonbPath: "$.name[*].family",
|
|
6120
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
6121
|
+
},
|
|
6122
|
+
{
|
|
6123
|
+
code: "given",
|
|
6124
|
+
type: "string",
|
|
6125
|
+
jsonbPath: "$.name[*].given",
|
|
6126
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
6127
|
+
},
|
|
6128
|
+
{ code: "active", type: "token", jsonbPath: "$.active" },
|
|
6129
|
+
{ code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
|
|
6130
|
+
{
|
|
6131
|
+
code: "general-practitioner",
|
|
6132
|
+
type: "reference",
|
|
6133
|
+
jsonbPath: "$.generalPractitioner[*]"
|
|
6134
|
+
},
|
|
6135
|
+
{
|
|
6136
|
+
code: "organization",
|
|
6137
|
+
type: "reference",
|
|
6138
|
+
jsonbPath: "$.managingOrganization"
|
|
6139
|
+
}
|
|
6140
|
+
];
|
|
6141
|
+
|
|
6142
|
+
// src/data/search/registry/procedure-search-parameters.ts
|
|
6143
|
+
var PROCEDURE_SEARCH_PARAMETERS = [
|
|
6144
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
6145
|
+
{ code: "category", type: "token", jsonbPath: "$.category" },
|
|
6146
|
+
{ code: "code", type: "token", jsonbPath: "$.code" },
|
|
6147
|
+
{ code: "subject", type: "reference", jsonbPath: "$.subject" },
|
|
6148
|
+
{ code: "patient", type: "reference", jsonbPath: "$.subject" },
|
|
6149
|
+
{ code: "encounter", type: "reference", jsonbPath: "$.encounter" },
|
|
6150
|
+
{
|
|
6151
|
+
code: "performer",
|
|
6152
|
+
type: "reference",
|
|
6153
|
+
jsonbPath: "$.performer[*].actor"
|
|
6154
|
+
},
|
|
6155
|
+
{ code: "date", type: "date", jsonbPath: "$.performedDateTime" },
|
|
6156
|
+
{ code: "location", type: "reference", jsonbPath: "$.location" },
|
|
6157
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*]" },
|
|
6158
|
+
{ code: "based-on", type: "reference", jsonbPath: "$.basedOn[*]" },
|
|
6159
|
+
{ code: "part-of", type: "reference", jsonbPath: "$.partOf[*]" },
|
|
6160
|
+
{ code: "reason-code", type: "token", jsonbPath: "$.reasonCode[*]" },
|
|
6161
|
+
{
|
|
6162
|
+
code: "reason-reference",
|
|
6163
|
+
type: "reference",
|
|
6164
|
+
jsonbPath: "$.reasonReference[*]"
|
|
6165
|
+
}
|
|
6166
|
+
];
|
|
6167
|
+
|
|
6168
|
+
// src/data/search/registry/resolver.ts
|
|
6169
|
+
var STATIC_SEARCH_PARAMETER_MAP = {
|
|
6170
|
+
Appointment: APPOINTMENT_SEARCH_PARAMETERS,
|
|
6171
|
+
Encounter: ENCOUNTER_SEARCH_PARAMETERS,
|
|
6172
|
+
Observation: OBSERVATION_SEARCH_PARAMETERS,
|
|
6173
|
+
Patient: PATIENT_SEARCH_PARAMETERS,
|
|
6174
|
+
Procedure: PROCEDURE_SEARCH_PARAMETERS
|
|
6175
|
+
};
|
|
6176
|
+
var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
6177
|
+
function getRegisteredSearchParameters(resourceType) {
|
|
6178
|
+
return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
// src/data/rest-api/routes/data/appointment/appointment-list-route.ts
|
|
6182
|
+
var APPOINTMENT_RESOURCE_TYPE = "Appointment";
|
|
6183
|
+
function stripModifier(key) {
|
|
6184
|
+
const idx = key.indexOf(":");
|
|
6185
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
6186
|
+
}
|
|
6187
|
+
function isResultParameter(key) {
|
|
6188
|
+
return key.startsWith("_");
|
|
5968
6189
|
}
|
|
5969
6190
|
function sendInvalidSearch400(res, diagnostics) {
|
|
5970
6191
|
return res.status(400).json({
|
|
@@ -5972,95 +6193,93 @@ function sendInvalidSearch400(res, diagnostics) {
|
|
|
5972
6193
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
5973
6194
|
});
|
|
5974
6195
|
}
|
|
6196
|
+
function extractSearchParamKeys(query) {
|
|
6197
|
+
const out = [];
|
|
6198
|
+
for (const rawKey of Object.keys(query)) {
|
|
6199
|
+
if (isResultParameter(rawKey)) {
|
|
6200
|
+
continue;
|
|
6201
|
+
}
|
|
6202
|
+
out.push({ rawKey, code: stripModifier(rawKey) });
|
|
6203
|
+
}
|
|
6204
|
+
return out;
|
|
6205
|
+
}
|
|
6206
|
+
function buildUnknownParamDiagnostics(unknownCodes) {
|
|
6207
|
+
const validCodes = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
6208
|
+
const codes = unknownCodes.join(", ");
|
|
6209
|
+
const isPlural = unknownCodes.length !== 1;
|
|
6210
|
+
return [
|
|
6211
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Appointment: ${codes}.`,
|
|
6212
|
+
`Valid codes: ${validCodes}.`
|
|
6213
|
+
].join(" ");
|
|
6214
|
+
}
|
|
6215
|
+
function findMalformedReference(query, searchParamKeys) {
|
|
6216
|
+
const referenceCodes = new Set(
|
|
6217
|
+
getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
6218
|
+
);
|
|
6219
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
6220
|
+
if (!referenceCodes.has(code)) {
|
|
6221
|
+
continue;
|
|
6222
|
+
}
|
|
6223
|
+
const raw = query[rawKey];
|
|
6224
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
6225
|
+
for (const v of values) {
|
|
6226
|
+
const trimmed = v.trim();
|
|
6227
|
+
if (trimmed.length === 0) {
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
6231
|
+
return { rawKey, value: trimmed };
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
return void 0;
|
|
6236
|
+
}
|
|
5975
6237
|
async function listAppointmentsRoute(req, res) {
|
|
5976
|
-
const
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
if (
|
|
5980
|
-
return
|
|
6238
|
+
const searchParamKeys = extractSearchParamKeys(
|
|
6239
|
+
req.query
|
|
6240
|
+
);
|
|
6241
|
+
if (searchParamKeys.length === 0) {
|
|
6242
|
+
return handleListRoute({
|
|
6243
|
+
req,
|
|
6244
|
+
res,
|
|
6245
|
+
basePath: BASE_PATH.APPOINTMENT,
|
|
6246
|
+
listOperation: listAppointmentsOperation,
|
|
6247
|
+
errorLogContext: "GET /Appointment list error:"
|
|
6248
|
+
});
|
|
5981
6249
|
}
|
|
5982
|
-
const
|
|
5983
|
-
|
|
6250
|
+
const registered = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE);
|
|
6251
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
6252
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
6253
|
+
if (unknownCodes.length > 0) {
|
|
5984
6254
|
return sendInvalidSearch400(
|
|
5985
6255
|
res,
|
|
5986
|
-
|
|
6256
|
+
buildUnknownParamDiagnostics([...new Set(unknownCodes)])
|
|
5987
6257
|
);
|
|
5988
6258
|
}
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
const result = await searchAppointmentsByActorOperation({
|
|
5999
|
-
context: ctx,
|
|
6000
|
-
actorReference: actorRef,
|
|
6001
|
-
dateConstraints
|
|
6002
|
-
});
|
|
6003
|
-
const bundle = buildSearchsetBundle(
|
|
6004
|
-
BASE_PATH.APPOINTMENT,
|
|
6005
|
-
result.entries
|
|
6006
|
-
);
|
|
6007
|
-
return res.json(bundle);
|
|
6008
|
-
} catch (err) {
|
|
6009
|
-
return sendOperationOutcome500(
|
|
6010
|
-
res,
|
|
6011
|
-
err,
|
|
6012
|
-
"GET /Appointment?actor= search error:"
|
|
6013
|
-
);
|
|
6014
|
-
}
|
|
6015
|
-
}
|
|
6016
|
-
if (patientId !== void 0) {
|
|
6017
|
-
const ctx = req.openhiContext;
|
|
6018
|
-
try {
|
|
6019
|
-
const result = await searchAppointmentsByPatientOperation({
|
|
6020
|
-
context: ctx,
|
|
6021
|
-
patientId,
|
|
6022
|
-
dateConstraints
|
|
6023
|
-
});
|
|
6024
|
-
const bundle = buildSearchsetBundle(
|
|
6025
|
-
BASE_PATH.APPOINTMENT,
|
|
6026
|
-
result.entries
|
|
6027
|
-
);
|
|
6028
|
-
return res.json(bundle);
|
|
6029
|
-
} catch (err) {
|
|
6030
|
-
return sendOperationOutcome500(
|
|
6031
|
-
res,
|
|
6032
|
-
err,
|
|
6033
|
-
"GET /Appointment?patient= search error:"
|
|
6034
|
-
);
|
|
6035
|
-
}
|
|
6259
|
+
const malformedRef = findMalformedReference(
|
|
6260
|
+
req.query,
|
|
6261
|
+
searchParamKeys
|
|
6262
|
+
);
|
|
6263
|
+
if (malformedRef !== void 0) {
|
|
6264
|
+
return sendInvalidSearch400(
|
|
6265
|
+
res,
|
|
6266
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
6267
|
+
);
|
|
6036
6268
|
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
return sendOperationOutcome500(
|
|
6051
|
-
res,
|
|
6052
|
-
err,
|
|
6053
|
-
"GET /Appointment?date= search error:"
|
|
6054
|
-
);
|
|
6055
|
-
}
|
|
6269
|
+
const ctx = req.openhiContext;
|
|
6270
|
+
try {
|
|
6271
|
+
const result = await genericSearchOperation({
|
|
6272
|
+
resourceType: APPOINTMENT_RESOURCE_TYPE,
|
|
6273
|
+
tenantId: ctx.tenantId,
|
|
6274
|
+
workspaceId: ctx.workspaceId,
|
|
6275
|
+
query: req.query,
|
|
6276
|
+
resolver: defaultSearchParameterResolver
|
|
6277
|
+
});
|
|
6278
|
+
const bundle = buildSearchsetBundle(BASE_PATH.APPOINTMENT, result.entries);
|
|
6279
|
+
return res.json(bundle);
|
|
6280
|
+
} catch (err) {
|
|
6281
|
+
return sendOperationOutcome500(res, err, "GET /Appointment search error:");
|
|
6056
6282
|
}
|
|
6057
|
-
return handleListRoute({
|
|
6058
|
-
req,
|
|
6059
|
-
res,
|
|
6060
|
-
basePath: BASE_PATH.APPOINTMENT,
|
|
6061
|
-
listOperation: listAppointmentsOperation,
|
|
6062
|
-
errorLogContext: "GET /Appointment list error:"
|
|
6063
|
-
});
|
|
6064
6283
|
}
|
|
6065
6284
|
|
|
6066
6285
|
// src/data/operations/data/appointment/appointment-update-operation.ts
|
|
@@ -13568,353 +13787,108 @@ async function listEncountersOperation(params) {
|
|
|
13568
13787
|
);
|
|
13569
13788
|
}
|
|
13570
13789
|
|
|
13571
|
-
// src/data/
|
|
13572
|
-
var
|
|
13573
|
-
|
|
13574
|
-
"
|
|
13575
|
-
|
|
13576
|
-
"le",
|
|
13577
|
-
"sa",
|
|
13578
|
-
"eb"
|
|
13579
|
-
];
|
|
13580
|
-
function isPeriodSearchPrefix(s) {
|
|
13581
|
-
return PERIOD_SEARCH_PREFIXES.includes(s);
|
|
13790
|
+
// src/data/rest-api/routes/data/encounter/encounter-list-route.ts
|
|
13791
|
+
var ENCOUNTER_RESOURCE_TYPE = "Encounter";
|
|
13792
|
+
function stripModifier2(key) {
|
|
13793
|
+
const idx = key.indexOf(":");
|
|
13794
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
13582
13795
|
}
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
var HAS_ANY_BOUND_GUARD = `(${PERIOD_START} IS NOT NULL OR ${PERIOD_END} IS NOT NULL)`;
|
|
13586
|
-
function buildSinglePredicateSql(prefix, paramName) {
|
|
13587
|
-
switch (prefix) {
|
|
13588
|
-
case "gt":
|
|
13589
|
-
return `(${PERIOD_END} IS NULL OR ${PERIOD_END} > :${paramName})`;
|
|
13590
|
-
case "lt":
|
|
13591
|
-
return `(${PERIOD_START} IS NULL OR ${PERIOD_START} < :${paramName})`;
|
|
13592
|
-
case "ge":
|
|
13593
|
-
return `(${PERIOD_END} IS NULL OR ${PERIOD_END} >= :${paramName})`;
|
|
13594
|
-
case "le":
|
|
13595
|
-
return `(${PERIOD_START} IS NULL OR ${PERIOD_START} <= :${paramName})`;
|
|
13596
|
-
case "sa":
|
|
13597
|
-
return `(${PERIOD_START} IS NOT NULL AND ${PERIOD_START} > :${paramName})`;
|
|
13598
|
-
case "eb":
|
|
13599
|
-
return `(${PERIOD_END} IS NOT NULL AND ${PERIOD_END} < :${paramName})`;
|
|
13600
|
-
}
|
|
13796
|
+
function isResultParameter2(key) {
|
|
13797
|
+
return key.startsWith("_");
|
|
13601
13798
|
}
|
|
13602
|
-
function
|
|
13603
|
-
return
|
|
13799
|
+
function sendInvalidSearch4002(res, diagnostics) {
|
|
13800
|
+
return res.status(400).json({
|
|
13801
|
+
resourceType: "OperationOutcome",
|
|
13802
|
+
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
13803
|
+
});
|
|
13604
13804
|
}
|
|
13605
|
-
function
|
|
13606
|
-
|
|
13607
|
-
|
|
13805
|
+
function extractSearchParamKeys2(query) {
|
|
13806
|
+
const out = [];
|
|
13807
|
+
for (const rawKey of Object.keys(query)) {
|
|
13808
|
+
if (isResultParameter2(rawKey)) {
|
|
13809
|
+
continue;
|
|
13810
|
+
}
|
|
13811
|
+
out.push({ rawKey, code: stripModifier2(rawKey) });
|
|
13608
13812
|
}
|
|
13609
|
-
|
|
13610
|
-
(c, i) => buildSinglePredicateSql(c.prefix, periodConstraintParamName(i))
|
|
13611
|
-
);
|
|
13612
|
-
fragments.push(HAS_ANY_BOUND_GUARD);
|
|
13613
|
-
return fragments;
|
|
13813
|
+
return out;
|
|
13614
13814
|
}
|
|
13615
|
-
function
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13815
|
+
function buildUnknownParamDiagnostics2(unknownCodes) {
|
|
13816
|
+
const validCodes = getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
13817
|
+
const codes = unknownCodes.join(", ");
|
|
13818
|
+
const isPlural = unknownCodes.length !== 1;
|
|
13819
|
+
return [
|
|
13820
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Encounter: ${codes}.`,
|
|
13821
|
+
`Valid codes: ${validCodes}.`
|
|
13822
|
+
].join(" ");
|
|
13620
13823
|
}
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
function buildSearchEncountersByDateSql(opts) {
|
|
13625
|
-
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
13626
|
-
opts.periodConstraints
|
|
13824
|
+
function findMalformedReference2(query, searchParamKeys) {
|
|
13825
|
+
const referenceCodes = new Set(
|
|
13826
|
+
getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
13627
13827
|
);
|
|
13628
|
-
const
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
"WHERE tenant_id = :tenantId",
|
|
13632
|
-
" AND workspace_id = :workspaceId",
|
|
13633
|
-
" AND resource_type = 'Encounter'",
|
|
13634
|
-
" AND deleted_at IS NULL"
|
|
13635
|
-
];
|
|
13636
|
-
for (const fragment of periodPredicates) {
|
|
13637
|
-
lines.push(` AND ${fragment}`);
|
|
13638
|
-
}
|
|
13639
|
-
lines.push("ORDER BY last_updated DESC");
|
|
13640
|
-
lines.push("LIMIT :limit;");
|
|
13641
|
-
return lines.join("\n");
|
|
13642
|
-
}
|
|
13643
|
-
async function searchEncountersByDateOperation(params) {
|
|
13644
|
-
const { context, periodConstraints } = params;
|
|
13645
|
-
if (periodConstraints.length === 0) {
|
|
13646
|
-
throw new Error(
|
|
13647
|
-
"searchEncountersByDateOperation requires at least one periodConstraint"
|
|
13648
|
-
);
|
|
13649
|
-
}
|
|
13650
|
-
const { tenantId, workspaceId } = context;
|
|
13651
|
-
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
13652
|
-
const limit = params.limit ?? DEFAULT_LIMIT4;
|
|
13653
|
-
const sql = buildSearchEncountersByDateSql({ periodConstraints });
|
|
13654
|
-
const queryParams = [
|
|
13655
|
-
{ name: "tenantId", value: tenantId },
|
|
13656
|
-
{ name: "workspaceId", value: workspaceId },
|
|
13657
|
-
{ name: "limit", value: limit },
|
|
13658
|
-
...buildPeriodSearchPredicateParams(periodConstraints)
|
|
13659
|
-
];
|
|
13660
|
-
const rows = await runner.query(sql, queryParams);
|
|
13661
|
-
const entries = rows.map((row) => ({
|
|
13662
|
-
id: row.id,
|
|
13663
|
-
resource: {
|
|
13664
|
-
...row.resource,
|
|
13665
|
-
id: row.id
|
|
13666
|
-
}
|
|
13667
|
-
}));
|
|
13668
|
-
return { entries, total: entries.length };
|
|
13669
|
-
}
|
|
13670
|
-
|
|
13671
|
-
// src/data/operations/data/encounter/encounter-search-by-participant-operation.ts
|
|
13672
|
-
var DEFAULT_LIMIT5 = 100;
|
|
13673
|
-
function buildSearchEncountersByParticipantSql(opts) {
|
|
13674
|
-
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
13675
|
-
opts?.periodConstraints ?? []
|
|
13676
|
-
);
|
|
13677
|
-
const lines = [
|
|
13678
|
-
"SELECT resource_id AS id, resource",
|
|
13679
|
-
"FROM resources",
|
|
13680
|
-
"WHERE tenant_id = :tenantId",
|
|
13681
|
-
" AND workspace_id = :workspaceId",
|
|
13682
|
-
" AND resource_type = 'Encounter'",
|
|
13683
|
-
" AND deleted_at IS NULL",
|
|
13684
|
-
` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`
|
|
13685
|
-
];
|
|
13686
|
-
for (const fragment of periodPredicates) {
|
|
13687
|
-
lines.push(` AND ${fragment}`);
|
|
13688
|
-
}
|
|
13689
|
-
lines.push("ORDER BY last_updated DESC");
|
|
13690
|
-
lines.push("LIMIT :limit;");
|
|
13691
|
-
return lines.join("\n");
|
|
13692
|
-
}
|
|
13693
|
-
async function searchEncountersByParticipantOperation(params) {
|
|
13694
|
-
const { context, participantReference } = params;
|
|
13695
|
-
const periodConstraints = params.periodConstraints ?? [];
|
|
13696
|
-
const { tenantId, workspaceId } = context;
|
|
13697
|
-
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
13698
|
-
const limit = params.limit ?? DEFAULT_LIMIT5;
|
|
13699
|
-
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
13700
|
-
shape: {
|
|
13701
|
-
kind: "array-of-objects",
|
|
13702
|
-
field: "participant",
|
|
13703
|
-
subfield: "individual"
|
|
13704
|
-
},
|
|
13705
|
-
reference: participantReference,
|
|
13706
|
-
tenantId,
|
|
13707
|
-
workspaceId
|
|
13708
|
-
});
|
|
13709
|
-
const sql = buildSearchEncountersByParticipantSql({ periodConstraints });
|
|
13710
|
-
const queryParams = [
|
|
13711
|
-
{ name: "tenantId", value: tenantId },
|
|
13712
|
-
{ name: "workspaceId", value: workspaceId },
|
|
13713
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
13714
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
13715
|
-
{ name: "limit", value: limit },
|
|
13716
|
-
...buildPeriodSearchPredicateParams(periodConstraints)
|
|
13717
|
-
];
|
|
13718
|
-
const rows = await runner.query(sql, queryParams);
|
|
13719
|
-
const entries = rows.map((row) => ({
|
|
13720
|
-
id: row.id,
|
|
13721
|
-
resource: {
|
|
13722
|
-
...row.resource,
|
|
13723
|
-
id: row.id
|
|
13724
|
-
}
|
|
13725
|
-
}));
|
|
13726
|
-
return { entries, total: entries.length };
|
|
13727
|
-
}
|
|
13728
|
-
|
|
13729
|
-
// src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
|
|
13730
|
-
var DEFAULT_LIMIT6 = 100;
|
|
13731
|
-
function buildSearchEncountersByPatientSql(opts) {
|
|
13732
|
-
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
13733
|
-
opts?.periodConstraints ?? []
|
|
13734
|
-
);
|
|
13735
|
-
const lines = [
|
|
13736
|
-
"SELECT resource_id AS id, resource",
|
|
13737
|
-
"FROM resources",
|
|
13738
|
-
"WHERE tenant_id = :tenantId",
|
|
13739
|
-
" AND workspace_id = :workspaceId",
|
|
13740
|
-
" AND resource_type = 'Encounter'",
|
|
13741
|
-
" AND deleted_at IS NULL",
|
|
13742
|
-
" AND (resource @> :containmentRelative::jsonb",
|
|
13743
|
-
" OR resource @> :containmentUrn::jsonb)"
|
|
13744
|
-
];
|
|
13745
|
-
for (const fragment of periodPredicates) {
|
|
13746
|
-
lines.push(` AND ${fragment}`);
|
|
13747
|
-
}
|
|
13748
|
-
lines.push("ORDER BY last_updated DESC");
|
|
13749
|
-
lines.push("LIMIT :limit;");
|
|
13750
|
-
return lines.join("\n");
|
|
13751
|
-
}
|
|
13752
|
-
async function searchEncountersByPatientOperation(params) {
|
|
13753
|
-
const { context, patientId } = params;
|
|
13754
|
-
const periodConstraints = params.periodConstraints ?? [];
|
|
13755
|
-
const { tenantId, workspaceId } = context;
|
|
13756
|
-
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
13757
|
-
const limit = params.limit ?? DEFAULT_LIMIT6;
|
|
13758
|
-
const containmentRelative = JSON.stringify({
|
|
13759
|
-
subject: { reference: `Patient/${patientId}` }
|
|
13760
|
-
});
|
|
13761
|
-
const containmentUrn = JSON.stringify({
|
|
13762
|
-
subject: {
|
|
13763
|
-
reference: buildOpenHiResourceUrn({
|
|
13764
|
-
tenantId,
|
|
13765
|
-
workspaceId,
|
|
13766
|
-
resourceType: "Patient",
|
|
13767
|
-
resourceId: patientId
|
|
13768
|
-
})
|
|
13769
|
-
}
|
|
13770
|
-
});
|
|
13771
|
-
const sql = buildSearchEncountersByPatientSql({ periodConstraints });
|
|
13772
|
-
const queryParams = [
|
|
13773
|
-
{ name: "tenantId", value: tenantId },
|
|
13774
|
-
{ name: "workspaceId", value: workspaceId },
|
|
13775
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
13776
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
13777
|
-
{ name: "limit", value: limit },
|
|
13778
|
-
...buildPeriodSearchPredicateParams(periodConstraints)
|
|
13779
|
-
];
|
|
13780
|
-
const rows = await runner.query(sql, queryParams);
|
|
13781
|
-
const entries = rows.map((row) => ({
|
|
13782
|
-
id: row.id,
|
|
13783
|
-
resource: {
|
|
13784
|
-
...row.resource,
|
|
13785
|
-
id: row.id
|
|
13786
|
-
}
|
|
13787
|
-
}));
|
|
13788
|
-
return { entries, total: entries.length };
|
|
13789
|
-
}
|
|
13790
|
-
|
|
13791
|
-
// src/data/rest-api/routes/data/encounter/encounter-list-route.ts
|
|
13792
|
-
function singleStringQueryParam2(req, name) {
|
|
13793
|
-
const v = req.query[name];
|
|
13794
|
-
if (typeof v !== "string") {
|
|
13795
|
-
return void 0;
|
|
13796
|
-
}
|
|
13797
|
-
const trimmed = v.trim();
|
|
13798
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
13799
|
-
}
|
|
13800
|
-
function isError2(v) {
|
|
13801
|
-
return v.error !== void 0;
|
|
13802
|
-
}
|
|
13803
|
-
function parseEncounterDateConstraints(req) {
|
|
13804
|
-
const raw = req.query.date;
|
|
13805
|
-
if (raw === void 0) {
|
|
13806
|
-
return [];
|
|
13807
|
-
}
|
|
13808
|
-
const values = Array.isArray(raw) ? raw : [raw];
|
|
13809
|
-
const out = [];
|
|
13810
|
-
for (const v of values) {
|
|
13811
|
-
if (typeof v !== "string") {
|
|
13812
|
-
return { error: "Each ?date= value must be a string." };
|
|
13813
|
-
}
|
|
13814
|
-
const trimmed = v.trim();
|
|
13815
|
-
if (trimmed === "") {
|
|
13816
|
-
return { error: "?date= value must not be empty." };
|
|
13817
|
-
}
|
|
13818
|
-
const prefix = trimmed.slice(0, 2);
|
|
13819
|
-
const datetime = trimmed.slice(2);
|
|
13820
|
-
if (!isPeriodSearchPrefix(prefix)) {
|
|
13821
|
-
return {
|
|
13822
|
-
error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${PERIOD_SEARCH_PREFIXES.join(", ")}.`
|
|
13823
|
-
};
|
|
13828
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
13829
|
+
if (!referenceCodes.has(code)) {
|
|
13830
|
+
continue;
|
|
13824
13831
|
}
|
|
13825
|
-
|
|
13826
|
-
|
|
13832
|
+
const raw = query[rawKey];
|
|
13833
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
13834
|
+
for (const v of values) {
|
|
13835
|
+
const trimmed = v.trim();
|
|
13836
|
+
if (trimmed.length === 0) {
|
|
13837
|
+
continue;
|
|
13838
|
+
}
|
|
13839
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
13840
|
+
return { rawKey, value: trimmed };
|
|
13841
|
+
}
|
|
13827
13842
|
}
|
|
13828
|
-
out.push({ prefix, value: datetime });
|
|
13829
13843
|
}
|
|
13830
|
-
return
|
|
13831
|
-
}
|
|
13832
|
-
function sendInvalidSearch4002(res, diagnostics) {
|
|
13833
|
-
return res.status(400).json({
|
|
13834
|
-
resourceType: "OperationOutcome",
|
|
13835
|
-
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
13836
|
-
});
|
|
13844
|
+
return void 0;
|
|
13837
13845
|
}
|
|
13838
13846
|
async function listEncountersRoute(req, res) {
|
|
13839
|
-
const
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
if (
|
|
13843
|
-
return
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
+
const searchParamKeys = extractSearchParamKeys2(
|
|
13848
|
+
req.query
|
|
13849
|
+
);
|
|
13850
|
+
if (searchParamKeys.length === 0) {
|
|
13851
|
+
return handleListRoute({
|
|
13852
|
+
req,
|
|
13853
|
+
res,
|
|
13854
|
+
basePath: BASE_PATH.ENCOUNTER,
|
|
13855
|
+
listOperation: listEncountersOperation,
|
|
13856
|
+
errorLogContext: "GET /Encounter list error:"
|
|
13857
|
+
});
|
|
13858
|
+
}
|
|
13859
|
+
const registered = getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE);
|
|
13860
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
13861
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
13862
|
+
if (unknownCodes.length > 0) {
|
|
13847
13863
|
return sendInvalidSearch4002(
|
|
13848
13864
|
res,
|
|
13849
|
-
|
|
13865
|
+
buildUnknownParamDiagnostics2([...new Set(unknownCodes)])
|
|
13850
13866
|
);
|
|
13851
13867
|
}
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
const result = await searchEncountersByParticipantOperation({
|
|
13862
|
-
context: ctx,
|
|
13863
|
-
participantReference: participantRef,
|
|
13864
|
-
periodConstraints
|
|
13865
|
-
});
|
|
13866
|
-
const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
|
|
13867
|
-
return res.json(bundle);
|
|
13868
|
-
} catch (err) {
|
|
13869
|
-
return sendOperationOutcome500(
|
|
13870
|
-
res,
|
|
13871
|
-
err,
|
|
13872
|
-
"GET /Encounter?participant= search error:"
|
|
13873
|
-
);
|
|
13874
|
-
}
|
|
13875
|
-
}
|
|
13876
|
-
if (patientId !== void 0) {
|
|
13877
|
-
const ctx = req.openhiContext;
|
|
13878
|
-
try {
|
|
13879
|
-
const result = await searchEncountersByPatientOperation({
|
|
13880
|
-
context: ctx,
|
|
13881
|
-
patientId,
|
|
13882
|
-
periodConstraints
|
|
13883
|
-
});
|
|
13884
|
-
const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
|
|
13885
|
-
return res.json(bundle);
|
|
13886
|
-
} catch (err) {
|
|
13887
|
-
return sendOperationOutcome500(
|
|
13888
|
-
res,
|
|
13889
|
-
err,
|
|
13890
|
-
"GET /Encounter?patient= search error:"
|
|
13891
|
-
);
|
|
13892
|
-
}
|
|
13868
|
+
const malformedRef = findMalformedReference2(
|
|
13869
|
+
req.query,
|
|
13870
|
+
searchParamKeys
|
|
13871
|
+
);
|
|
13872
|
+
if (malformedRef !== void 0) {
|
|
13873
|
+
return sendInvalidSearch4002(
|
|
13874
|
+
res,
|
|
13875
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
13876
|
+
);
|
|
13893
13877
|
}
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
"GET /Encounter?date= search error:"
|
|
13908
|
-
);
|
|
13909
|
-
}
|
|
13878
|
+
const ctx = req.openhiContext;
|
|
13879
|
+
try {
|
|
13880
|
+
const result = await genericSearchOperation({
|
|
13881
|
+
resourceType: ENCOUNTER_RESOURCE_TYPE,
|
|
13882
|
+
tenantId: ctx.tenantId,
|
|
13883
|
+
workspaceId: ctx.workspaceId,
|
|
13884
|
+
query: req.query,
|
|
13885
|
+
resolver: defaultSearchParameterResolver
|
|
13886
|
+
});
|
|
13887
|
+
const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
|
|
13888
|
+
return res.json(bundle);
|
|
13889
|
+
} catch (err) {
|
|
13890
|
+
return sendOperationOutcome500(res, err, "GET /Encounter search error:");
|
|
13910
13891
|
}
|
|
13911
|
-
return handleListRoute({
|
|
13912
|
-
req,
|
|
13913
|
-
res,
|
|
13914
|
-
basePath: BASE_PATH.ENCOUNTER,
|
|
13915
|
-
listOperation: listEncountersOperation,
|
|
13916
|
-
errorLogContext: "GET /Encounter list error:"
|
|
13917
|
-
});
|
|
13918
13892
|
}
|
|
13919
13893
|
|
|
13920
13894
|
// src/data/operations/data/encounter/encounter-update-operation.ts
|
|
@@ -24594,44 +24568,137 @@ async function listObservationsOperation(params) {
|
|
|
24594
24568
|
}
|
|
24595
24569
|
|
|
24596
24570
|
// src/data/rest-api/routes/data/observation/observation-list-route.ts
|
|
24597
|
-
|
|
24598
|
-
|
|
24599
|
-
|
|
24600
|
-
|
|
24601
|
-
|
|
24602
|
-
|
|
24603
|
-
|
|
24571
|
+
var OBSERVATION_RESOURCE_TYPE = "Observation";
|
|
24572
|
+
function stripModifier3(key) {
|
|
24573
|
+
const idx = key.indexOf(":");
|
|
24574
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
24575
|
+
}
|
|
24576
|
+
function isResultParameter3(key) {
|
|
24577
|
+
return key.startsWith("_");
|
|
24578
|
+
}
|
|
24579
|
+
function sendInvalidSearch4003(res, diagnostics) {
|
|
24580
|
+
return res.status(400).json({
|
|
24581
|
+
resourceType: "OperationOutcome",
|
|
24582
|
+
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
24604
24583
|
});
|
|
24605
24584
|
}
|
|
24606
|
-
|
|
24607
|
-
|
|
24608
|
-
|
|
24609
|
-
|
|
24610
|
-
|
|
24611
|
-
|
|
24612
|
-
|
|
24613
|
-
|
|
24614
|
-
|
|
24615
|
-
workspaceId,
|
|
24616
|
-
id,
|
|
24617
|
-
"Observation",
|
|
24618
|
-
context,
|
|
24619
|
-
(existingResourceStr) => buildUpdatedResourceWithAudit(
|
|
24620
|
-
body,
|
|
24621
|
-
id,
|
|
24622
|
-
date,
|
|
24623
|
-
actorId,
|
|
24624
|
-
actorName,
|
|
24625
|
-
existingResourceStr,
|
|
24626
|
-
"Observation"
|
|
24627
|
-
)
|
|
24628
|
-
);
|
|
24585
|
+
function extractSearchParamKeys3(query) {
|
|
24586
|
+
const out = [];
|
|
24587
|
+
for (const rawKey of Object.keys(query)) {
|
|
24588
|
+
if (isResultParameter3(rawKey)) {
|
|
24589
|
+
continue;
|
|
24590
|
+
}
|
|
24591
|
+
out.push({ rawKey, code: stripModifier3(rawKey) });
|
|
24592
|
+
}
|
|
24593
|
+
return out;
|
|
24629
24594
|
}
|
|
24630
|
-
|
|
24631
|
-
|
|
24632
|
-
|
|
24633
|
-
const
|
|
24634
|
-
|
|
24595
|
+
function buildUnknownParamDiagnostics3(unknownCodes) {
|
|
24596
|
+
const validCodes = getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
24597
|
+
const codes = unknownCodes.join(", ");
|
|
24598
|
+
const isPlural = unknownCodes.length !== 1;
|
|
24599
|
+
return [
|
|
24600
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Observation: ${codes}.`,
|
|
24601
|
+
`Valid codes: ${validCodes}.`
|
|
24602
|
+
].join(" ");
|
|
24603
|
+
}
|
|
24604
|
+
function findMalformedReference3(query, searchParamKeys) {
|
|
24605
|
+
const referenceCodes = new Set(
|
|
24606
|
+
getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
24607
|
+
);
|
|
24608
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
24609
|
+
if (!referenceCodes.has(code)) {
|
|
24610
|
+
continue;
|
|
24611
|
+
}
|
|
24612
|
+
const raw = query[rawKey];
|
|
24613
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
24614
|
+
for (const v of values) {
|
|
24615
|
+
const trimmed = v.trim();
|
|
24616
|
+
if (trimmed.length === 0) {
|
|
24617
|
+
continue;
|
|
24618
|
+
}
|
|
24619
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
24620
|
+
return { rawKey, value: trimmed };
|
|
24621
|
+
}
|
|
24622
|
+
}
|
|
24623
|
+
}
|
|
24624
|
+
return void 0;
|
|
24625
|
+
}
|
|
24626
|
+
async function listObservationsRoute(req, res) {
|
|
24627
|
+
const searchParamKeys = extractSearchParamKeys3(
|
|
24628
|
+
req.query
|
|
24629
|
+
);
|
|
24630
|
+
if (searchParamKeys.length === 0) {
|
|
24631
|
+
return handleListRoute({
|
|
24632
|
+
req,
|
|
24633
|
+
res,
|
|
24634
|
+
basePath: BASE_PATH.OBSERVATION,
|
|
24635
|
+
listOperation: listObservationsOperation,
|
|
24636
|
+
errorLogContext: "GET /Observation list error:"
|
|
24637
|
+
});
|
|
24638
|
+
}
|
|
24639
|
+
const registered = getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE);
|
|
24640
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
24641
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
24642
|
+
if (unknownCodes.length > 0) {
|
|
24643
|
+
return sendInvalidSearch4003(
|
|
24644
|
+
res,
|
|
24645
|
+
buildUnknownParamDiagnostics3([...new Set(unknownCodes)])
|
|
24646
|
+
);
|
|
24647
|
+
}
|
|
24648
|
+
const malformedRef = findMalformedReference3(
|
|
24649
|
+
req.query,
|
|
24650
|
+
searchParamKeys
|
|
24651
|
+
);
|
|
24652
|
+
if (malformedRef !== void 0) {
|
|
24653
|
+
return sendInvalidSearch4003(
|
|
24654
|
+
res,
|
|
24655
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
24656
|
+
);
|
|
24657
|
+
}
|
|
24658
|
+
const ctx = req.openhiContext;
|
|
24659
|
+
try {
|
|
24660
|
+
const result = await genericSearchOperation({
|
|
24661
|
+
resourceType: OBSERVATION_RESOURCE_TYPE,
|
|
24662
|
+
tenantId: ctx.tenantId,
|
|
24663
|
+
workspaceId: ctx.workspaceId,
|
|
24664
|
+
query: req.query,
|
|
24665
|
+
resolver: defaultSearchParameterResolver
|
|
24666
|
+
});
|
|
24667
|
+
const bundle = buildSearchsetBundle(BASE_PATH.OBSERVATION, result.entries);
|
|
24668
|
+
return res.json(bundle);
|
|
24669
|
+
} catch (err) {
|
|
24670
|
+
return sendOperationOutcome500(res, err, "GET /Observation search error:");
|
|
24671
|
+
}
|
|
24672
|
+
}
|
|
24673
|
+
|
|
24674
|
+
// src/data/operations/data/observation/observation-update-operation.ts
|
|
24675
|
+
async function updateObservationOperation(params) {
|
|
24676
|
+
const { context, id, body, tableName } = params;
|
|
24677
|
+
const { tenantId, workspaceId, date, actorId, actorName } = context;
|
|
24678
|
+
const service = getDynamoDataService(tableName);
|
|
24679
|
+
return updateDataEntityById(
|
|
24680
|
+
service.entities.observation,
|
|
24681
|
+
tenantId,
|
|
24682
|
+
workspaceId,
|
|
24683
|
+
id,
|
|
24684
|
+
"Observation",
|
|
24685
|
+
context,
|
|
24686
|
+
(existingResourceStr) => buildUpdatedResourceWithAudit(
|
|
24687
|
+
body,
|
|
24688
|
+
id,
|
|
24689
|
+
date,
|
|
24690
|
+
actorId,
|
|
24691
|
+
actorName,
|
|
24692
|
+
existingResourceStr,
|
|
24693
|
+
"Observation"
|
|
24694
|
+
)
|
|
24695
|
+
);
|
|
24696
|
+
}
|
|
24697
|
+
|
|
24698
|
+
// src/data/rest-api/routes/data/observation/observation-update-route.ts
|
|
24699
|
+
async function updateObservationRoute(req, res) {
|
|
24700
|
+
const bodyResult = requireJsonBodyAs(req, res);
|
|
24701
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
24635
24702
|
const id = String(req.params.id);
|
|
24636
24703
|
const ctx = req.openhiContext;
|
|
24637
24704
|
const body = bodyResult.body;
|
|
@@ -25499,492 +25566,109 @@ async function createPatientRoute(req, res) {
|
|
|
25499
25566
|
try {
|
|
25500
25567
|
const result = await createPatientOperation({
|
|
25501
25568
|
context: ctx,
|
|
25502
|
-
body: patient
|
|
25503
|
-
});
|
|
25504
|
-
return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
|
|
25505
|
-
} catch (err) {
|
|
25506
|
-
return sendOperationOutcome500(res, err, "POST Patient error:");
|
|
25507
|
-
}
|
|
25508
|
-
}
|
|
25509
|
-
|
|
25510
|
-
// src/data/operations/data/patient/patient-delete-operation.ts
|
|
25511
|
-
async function deletePatientOperation(params) {
|
|
25512
|
-
const { context, id, tableName } = params;
|
|
25513
|
-
const { tenantId, workspaceId } = context;
|
|
25514
|
-
const service = getDynamoDataService(tableName);
|
|
25515
|
-
await deleteDataEntityById(
|
|
25516
|
-
service.entities.patient,
|
|
25517
|
-
tenantId,
|
|
25518
|
-
workspaceId,
|
|
25519
|
-
id
|
|
25520
|
-
);
|
|
25521
|
-
}
|
|
25522
|
-
|
|
25523
|
-
// src/data/rest-api/routes/data/patient/patient-delete-route.ts
|
|
25524
|
-
async function deletePatientRoute(req, res) {
|
|
25525
|
-
const id = String(req.params.id);
|
|
25526
|
-
const ctx = req.openhiContext;
|
|
25527
|
-
try {
|
|
25528
|
-
await deletePatientOperation({ context: ctx, id });
|
|
25529
|
-
return res.status(204).send();
|
|
25530
|
-
} catch (err) {
|
|
25531
|
-
return sendOperationOutcome500(res, err, "DELETE Patient error:");
|
|
25532
|
-
}
|
|
25533
|
-
}
|
|
25534
|
-
|
|
25535
|
-
// src/data/operations/data/patient/patient-get-by-id-operation.ts
|
|
25536
|
-
async function getPatientByIdOperation(params) {
|
|
25537
|
-
const { context, id, tableName } = params;
|
|
25538
|
-
const { tenantId, workspaceId } = context;
|
|
25539
|
-
const service = getDynamoDataService(tableName);
|
|
25540
|
-
return getDataEntityById(
|
|
25541
|
-
service.entities.patient,
|
|
25542
|
-
tenantId,
|
|
25543
|
-
workspaceId,
|
|
25544
|
-
id,
|
|
25545
|
-
"Patient"
|
|
25546
|
-
);
|
|
25547
|
-
}
|
|
25548
|
-
|
|
25549
|
-
// src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
|
|
25550
|
-
async function getPatientByIdRoute(req, res) {
|
|
25551
|
-
const id = String(req.params.id);
|
|
25552
|
-
const ctx = req.openhiContext;
|
|
25553
|
-
try {
|
|
25554
|
-
const result = await getPatientByIdOperation({ context: ctx, id });
|
|
25555
|
-
return res.json(result.resource);
|
|
25556
|
-
} catch (err) {
|
|
25557
|
-
const status = domainErrorToHttpStatus(err);
|
|
25558
|
-
if (status === 404) {
|
|
25559
|
-
const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
|
|
25560
|
-
return sendOperationOutcome404(res, diagnostics);
|
|
25561
|
-
}
|
|
25562
|
-
return sendOperationOutcome500(res, err, "GET Patient error:");
|
|
25563
|
-
}
|
|
25564
|
-
}
|
|
25565
|
-
|
|
25566
|
-
// src/data/operations/data/patient/patient-list-operation.ts
|
|
25567
|
-
async function listPatientsOperation(params) {
|
|
25568
|
-
const { context, tableName, mode } = params;
|
|
25569
|
-
const { tenantId, workspaceId } = context;
|
|
25570
|
-
const service = getDynamoDataService(tableName);
|
|
25571
|
-
return listDataEntitiesByWorkspace(
|
|
25572
|
-
service.entities.patient,
|
|
25573
|
-
tenantId,
|
|
25574
|
-
workspaceId,
|
|
25575
|
-
mode
|
|
25576
|
-
);
|
|
25577
|
-
}
|
|
25578
|
-
|
|
25579
|
-
// src/data/search/engine/string-predicate.ts
|
|
25580
|
-
var STRING_MODIFIERS = ["exact", "contains"];
|
|
25581
|
-
function isStringModifier(s) {
|
|
25582
|
-
return STRING_MODIFIERS.includes(s);
|
|
25583
|
-
}
|
|
25584
|
-
function jsonbPathToStringShape(jsonbPath) {
|
|
25585
|
-
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
25586
|
-
if (scalar) {
|
|
25587
|
-
return { kind: "scalar", field: scalar[1] };
|
|
25588
|
-
}
|
|
25589
|
-
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
25590
|
-
if (arrayOfScalars) {
|
|
25591
|
-
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
25592
|
-
}
|
|
25593
|
-
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
25594
|
-
jsonbPath
|
|
25595
|
-
);
|
|
25596
|
-
if (arrayOfObjs) {
|
|
25597
|
-
return {
|
|
25598
|
-
kind: "array-of-objects",
|
|
25599
|
-
field: arrayOfObjs[1],
|
|
25600
|
-
subfield: arrayOfObjs[2]
|
|
25601
|
-
};
|
|
25602
|
-
}
|
|
25603
|
-
throw new Error(
|
|
25604
|
-
`String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
25605
|
-
);
|
|
25606
|
-
}
|
|
25607
|
-
function escapeLikePattern(value) {
|
|
25608
|
-
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
25609
|
-
}
|
|
25610
|
-
function buildLikePattern(value, modifier) {
|
|
25611
|
-
const escaped = escapeLikePattern(value);
|
|
25612
|
-
switch (modifier) {
|
|
25613
|
-
case "exact":
|
|
25614
|
-
return escaped;
|
|
25615
|
-
case "contains":
|
|
25616
|
-
return `%${escaped}%`;
|
|
25617
|
-
case void 0:
|
|
25618
|
-
return `${escaped}%`;
|
|
25619
|
-
}
|
|
25620
|
-
}
|
|
25621
|
-
function buildIlikeExtractSql(shape, paramName) {
|
|
25622
|
-
switch (shape.kind) {
|
|
25623
|
-
case "scalar":
|
|
25624
|
-
return `resource->>'${shape.field}' ILIKE :${paramName}`;
|
|
25625
|
-
case "array-of-scalars":
|
|
25626
|
-
return [
|
|
25627
|
-
"EXISTS (SELECT 1 FROM",
|
|
25628
|
-
`jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
|
|
25629
|
-
`WHERE s_elem.text_val ILIKE :${paramName})`
|
|
25630
|
-
].join(" ");
|
|
25631
|
-
case "array-of-objects":
|
|
25632
|
-
return [
|
|
25633
|
-
"EXISTS (SELECT 1 FROM",
|
|
25634
|
-
`jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
|
|
25635
|
-
`WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
|
|
25636
|
-
].join(" ");
|
|
25637
|
-
}
|
|
25638
|
-
}
|
|
25639
|
-
function emitStringPredicate(opts) {
|
|
25640
|
-
const shape = jsonbPathToStringShape(opts.jsonbPath);
|
|
25641
|
-
const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
|
|
25642
|
-
const sql = buildIlikeExtractSql(shape, opts.paramName);
|
|
25643
|
-
const pattern = buildLikePattern(opts.rawValue, modifier);
|
|
25644
|
-
const params = [
|
|
25645
|
-
{ name: opts.paramName, value: pattern }
|
|
25646
|
-
];
|
|
25647
|
-
return { sql, params };
|
|
25648
|
-
}
|
|
25649
|
-
function checkModifier(modifier) {
|
|
25650
|
-
if (!isStringModifier(modifier)) {
|
|
25651
|
-
throw new Error(
|
|
25652
|
-
`String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
|
|
25653
|
-
);
|
|
25654
|
-
}
|
|
25655
|
-
return modifier;
|
|
25656
|
-
}
|
|
25657
|
-
|
|
25658
|
-
// src/data/search/engine/token-predicate.ts
|
|
25659
|
-
function jsonbPathToTokenShape(jsonbPath) {
|
|
25660
|
-
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
25661
|
-
if (scalar) {
|
|
25662
|
-
return { kind: "scalar", field: scalar[1] };
|
|
25663
|
-
}
|
|
25664
|
-
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
25665
|
-
if (arrayOfScalars) {
|
|
25666
|
-
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
25667
|
-
}
|
|
25668
|
-
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
25669
|
-
jsonbPath
|
|
25670
|
-
);
|
|
25671
|
-
if (arrayOfObjs) {
|
|
25672
|
-
return {
|
|
25673
|
-
kind: "array-of-objects",
|
|
25674
|
-
field: arrayOfObjs[1],
|
|
25675
|
-
subfield: arrayOfObjs[2]
|
|
25676
|
-
};
|
|
25677
|
-
}
|
|
25678
|
-
throw new Error(
|
|
25679
|
-
`Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
25680
|
-
);
|
|
25681
|
-
}
|
|
25682
|
-
function parseTokenValue(raw) {
|
|
25683
|
-
const idx = raw.indexOf("|");
|
|
25684
|
-
if (idx === -1) {
|
|
25685
|
-
return { code: raw };
|
|
25686
|
-
}
|
|
25687
|
-
const system = raw.slice(0, idx);
|
|
25688
|
-
const code = raw.slice(idx + 1);
|
|
25689
|
-
return system.length > 0 ? { system, code } : { code };
|
|
25690
|
-
}
|
|
25691
|
-
function wrapTokenInShape(shape, value) {
|
|
25692
|
-
switch (shape.kind) {
|
|
25693
|
-
case "scalar":
|
|
25694
|
-
return { [shape.field]: value };
|
|
25695
|
-
case "array-of-scalars":
|
|
25696
|
-
return { [shape.field]: [value] };
|
|
25697
|
-
case "array-of-objects":
|
|
25698
|
-
return { [shape.field]: [{ [shape.subfield]: value }] };
|
|
25699
|
-
}
|
|
25700
|
-
}
|
|
25701
|
-
function emitTokenPredicate(opts) {
|
|
25702
|
-
const shape = jsonbPathToTokenShape(opts.jsonbPath);
|
|
25703
|
-
const { code } = parseTokenValue(opts.rawValue);
|
|
25704
|
-
const payload = wrapTokenInShape(shape, code);
|
|
25705
|
-
const sql = `resource @> :${opts.paramName}::jsonb`;
|
|
25706
|
-
const params = [
|
|
25707
|
-
{ name: opts.paramName, value: JSON.stringify(payload) }
|
|
25708
|
-
];
|
|
25709
|
-
return { sql, params };
|
|
25710
|
-
}
|
|
25711
|
-
|
|
25712
|
-
// src/data/search/engine/combinator.ts
|
|
25713
|
-
function parseQueryKey(key) {
|
|
25714
|
-
const idx = key.indexOf(":");
|
|
25715
|
-
if (idx === -1) {
|
|
25716
|
-
return { code: key, modifier: void 0 };
|
|
25717
|
-
}
|
|
25718
|
-
return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
|
|
25719
|
-
}
|
|
25720
|
-
function flattenQueryValues(raw) {
|
|
25721
|
-
const list = Array.isArray(raw) ? raw : [raw];
|
|
25722
|
-
const out = [];
|
|
25723
|
-
for (const v of list) {
|
|
25724
|
-
for (const piece of v.split(",")) {
|
|
25725
|
-
out.push(piece);
|
|
25726
|
-
}
|
|
25727
|
-
}
|
|
25728
|
-
return out;
|
|
25729
|
-
}
|
|
25730
|
-
function parseQueryEntries(query) {
|
|
25731
|
-
const entries = [];
|
|
25732
|
-
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
25733
|
-
if (rawValue === void 0) continue;
|
|
25734
|
-
const { code, modifier } = parseQueryKey(rawKey);
|
|
25735
|
-
entries.push({
|
|
25736
|
-
code,
|
|
25737
|
-
modifier,
|
|
25738
|
-
values: flattenQueryValues(rawValue)
|
|
25739
|
-
});
|
|
25740
|
-
}
|
|
25741
|
-
return entries;
|
|
25742
|
-
}
|
|
25743
|
-
function findParam(params, code) {
|
|
25744
|
-
return params.find((p) => p.code === code);
|
|
25745
|
-
}
|
|
25746
|
-
function checkModifierAllowed(modifier, param) {
|
|
25747
|
-
const universal = modifier === "missing" || modifier === "not";
|
|
25748
|
-
const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
|
|
25749
|
-
if (universal || stringNative) {
|
|
25750
|
-
if (param.modifiers && !param.modifiers.includes(modifier)) {
|
|
25751
|
-
throw new Error(
|
|
25752
|
-
`Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
|
|
25753
|
-
);
|
|
25754
|
-
}
|
|
25755
|
-
return;
|
|
25756
|
-
}
|
|
25757
|
-
throw new Error(
|
|
25758
|
-
`Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
|
|
25759
|
-
);
|
|
25760
|
-
}
|
|
25761
|
-
function emitOne(opts) {
|
|
25762
|
-
const { param, modifier, rawValue, paramName, context } = opts;
|
|
25763
|
-
if (modifier === "missing") {
|
|
25764
|
-
const missing = parseMissingValue(rawValue);
|
|
25765
|
-
return emitFieldMissingPredicate({
|
|
25766
|
-
jsonbPath: param.jsonbPath,
|
|
25767
|
-
missing
|
|
25768
|
-
});
|
|
25769
|
-
}
|
|
25770
|
-
const negate = modifier === "not";
|
|
25771
|
-
const effectiveModifier = negate ? void 0 : modifier;
|
|
25772
|
-
const inner = emitForType({
|
|
25773
|
-
paramType: param.type,
|
|
25774
|
-
jsonbPath: param.jsonbPath,
|
|
25775
|
-
rawValue,
|
|
25776
|
-
paramName,
|
|
25777
|
-
modifier: effectiveModifier,
|
|
25778
|
-
context
|
|
25779
|
-
});
|
|
25780
|
-
if (!negate) {
|
|
25781
|
-
return inner;
|
|
25782
|
-
}
|
|
25783
|
-
return { sql: `NOT (${inner.sql})`, params: inner.params };
|
|
25784
|
-
}
|
|
25785
|
-
function parseMissingValue(raw) {
|
|
25786
|
-
if (raw === "true") return true;
|
|
25787
|
-
if (raw === "false") return false;
|
|
25788
|
-
throw new Error(
|
|
25789
|
-
`:missing requires a value of "true" or "false"; received "${raw}".`
|
|
25790
|
-
);
|
|
25791
|
-
}
|
|
25792
|
-
function emitForType(opts) {
|
|
25793
|
-
switch (opts.paramType) {
|
|
25794
|
-
case "token":
|
|
25795
|
-
return emitTokenPredicate({
|
|
25796
|
-
jsonbPath: opts.jsonbPath,
|
|
25797
|
-
rawValue: opts.rawValue,
|
|
25798
|
-
paramName: opts.paramName,
|
|
25799
|
-
context: opts.context
|
|
25800
|
-
});
|
|
25801
|
-
case "date":
|
|
25802
|
-
return emitDatePredicate({
|
|
25803
|
-
jsonbPath: opts.jsonbPath,
|
|
25804
|
-
rawValue: opts.rawValue,
|
|
25805
|
-
paramName: opts.paramName,
|
|
25806
|
-
context: opts.context
|
|
25807
|
-
});
|
|
25808
|
-
case "reference":
|
|
25809
|
-
return emitReferencePredicate({
|
|
25810
|
-
jsonbPath: opts.jsonbPath,
|
|
25811
|
-
rawValue: opts.rawValue,
|
|
25812
|
-
paramName: opts.paramName,
|
|
25813
|
-
context: opts.context
|
|
25814
|
-
});
|
|
25815
|
-
case "string":
|
|
25816
|
-
return emitStringPredicate({
|
|
25817
|
-
jsonbPath: opts.jsonbPath,
|
|
25818
|
-
rawValue: opts.rawValue,
|
|
25819
|
-
paramName: opts.paramName,
|
|
25820
|
-
modifier: opts.modifier,
|
|
25821
|
-
context: opts.context
|
|
25822
|
-
});
|
|
25823
|
-
}
|
|
25824
|
-
}
|
|
25825
|
-
function combineSearchPredicates(opts) {
|
|
25826
|
-
const entries = parseQueryEntries(opts.query);
|
|
25827
|
-
const groupSqls = [];
|
|
25828
|
-
const allParams = [];
|
|
25829
|
-
let paramIdx = 0;
|
|
25830
|
-
for (const entry of entries) {
|
|
25831
|
-
const registered = findParam(opts.registeredParams, entry.code);
|
|
25832
|
-
if (!registered) {
|
|
25833
|
-
continue;
|
|
25834
|
-
}
|
|
25835
|
-
if (entry.modifier !== void 0) {
|
|
25836
|
-
checkModifierAllowed(entry.modifier, registered);
|
|
25837
|
-
}
|
|
25838
|
-
if (entry.values.length === 0) {
|
|
25839
|
-
continue;
|
|
25840
|
-
}
|
|
25841
|
-
const fragmentSqls = [];
|
|
25842
|
-
let valueIdx = 0;
|
|
25843
|
-
for (const rawValue of entry.values) {
|
|
25844
|
-
const paramName = `p${paramIdx}v${valueIdx}`;
|
|
25845
|
-
const frag = emitOne({
|
|
25846
|
-
param: registered,
|
|
25847
|
-
modifier: entry.modifier,
|
|
25848
|
-
rawValue,
|
|
25849
|
-
paramName,
|
|
25850
|
-
context: opts.context
|
|
25851
|
-
});
|
|
25852
|
-
fragmentSqls.push(frag.sql);
|
|
25853
|
-
for (const p of frag.params) {
|
|
25854
|
-
allParams.push(p);
|
|
25855
|
-
}
|
|
25856
|
-
valueIdx++;
|
|
25857
|
-
}
|
|
25858
|
-
paramIdx++;
|
|
25859
|
-
if (fragmentSqls.length === 1) {
|
|
25860
|
-
groupSqls.push(fragmentSqls[0]);
|
|
25861
|
-
} else {
|
|
25862
|
-
groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
|
|
25863
|
-
}
|
|
25864
|
-
}
|
|
25865
|
-
if (groupSqls.length === 0) {
|
|
25866
|
-
return { sql: "", params: [] };
|
|
25569
|
+
body: patient
|
|
25570
|
+
});
|
|
25571
|
+
return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
|
|
25572
|
+
} catch (err) {
|
|
25573
|
+
return sendOperationOutcome500(res, err, "POST Patient error:");
|
|
25867
25574
|
}
|
|
25868
|
-
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
25869
25575
|
}
|
|
25870
25576
|
|
|
25871
|
-
// src/data/
|
|
25872
|
-
|
|
25873
|
-
|
|
25874
|
-
const
|
|
25875
|
-
|
|
25876
|
-
|
|
25877
|
-
|
|
25878
|
-
|
|
25879
|
-
|
|
25880
|
-
|
|
25881
|
-
|
|
25882
|
-
|
|
25883
|
-
|
|
25577
|
+
// src/data/operations/data/patient/patient-delete-operation.ts
|
|
25578
|
+
async function deletePatientOperation(params) {
|
|
25579
|
+
const { context, id, tableName } = params;
|
|
25580
|
+
const { tenantId, workspaceId } = context;
|
|
25581
|
+
const service = getDynamoDataService(tableName);
|
|
25582
|
+
await deleteDataEntityById(
|
|
25583
|
+
service.entities.patient,
|
|
25584
|
+
tenantId,
|
|
25585
|
+
workspaceId,
|
|
25586
|
+
id
|
|
25587
|
+
);
|
|
25588
|
+
}
|
|
25589
|
+
|
|
25590
|
+
// src/data/rest-api/routes/data/patient/patient-delete-route.ts
|
|
25591
|
+
async function deletePatientRoute(req, res) {
|
|
25592
|
+
const id = String(req.params.id);
|
|
25593
|
+
const ctx = req.openhiContext;
|
|
25594
|
+
try {
|
|
25595
|
+
await deletePatientOperation({ context: ctx, id });
|
|
25596
|
+
return res.status(204).send();
|
|
25597
|
+
} catch (err) {
|
|
25598
|
+
return sendOperationOutcome500(res, err, "DELETE Patient error:");
|
|
25884
25599
|
}
|
|
25885
|
-
lines.push("ORDER BY last_updated DESC");
|
|
25886
|
-
lines.push("LIMIT :limit;");
|
|
25887
|
-
return lines.join("\n");
|
|
25888
25600
|
}
|
|
25889
|
-
|
|
25890
|
-
|
|
25891
|
-
|
|
25892
|
-
const
|
|
25893
|
-
const
|
|
25894
|
-
const
|
|
25895
|
-
|
|
25896
|
-
|
|
25897
|
-
|
|
25898
|
-
|
|
25899
|
-
|
|
25900
|
-
|
|
25901
|
-
|
|
25902
|
-
{ name: "tenantId", value: tenantId },
|
|
25903
|
-
{ name: "workspaceId", value: workspaceId },
|
|
25904
|
-
{ name: "resourceType", value: resourceType },
|
|
25905
|
-
{ name: "limit", value: limit },
|
|
25906
|
-
...combined.params
|
|
25907
|
-
];
|
|
25908
|
-
const rows = await runner.query(sql, queryParams);
|
|
25909
|
-
const entries = rows.map((row) => ({
|
|
25910
|
-
id: row.id,
|
|
25911
|
-
resource: { ...row.resource, id: row.id }
|
|
25912
|
-
}));
|
|
25913
|
-
return { entries, total: entries.length };
|
|
25601
|
+
|
|
25602
|
+
// src/data/operations/data/patient/patient-get-by-id-operation.ts
|
|
25603
|
+
async function getPatientByIdOperation(params) {
|
|
25604
|
+
const { context, id, tableName } = params;
|
|
25605
|
+
const { tenantId, workspaceId } = context;
|
|
25606
|
+
const service = getDynamoDataService(tableName);
|
|
25607
|
+
return getDataEntityById(
|
|
25608
|
+
service.entities.patient,
|
|
25609
|
+
tenantId,
|
|
25610
|
+
workspaceId,
|
|
25611
|
+
id,
|
|
25612
|
+
"Patient"
|
|
25613
|
+
);
|
|
25914
25614
|
}
|
|
25915
25615
|
|
|
25916
|
-
// src/data/
|
|
25917
|
-
|
|
25918
|
-
|
|
25919
|
-
|
|
25920
|
-
{
|
|
25921
|
-
|
|
25922
|
-
|
|
25923
|
-
|
|
25924
|
-
|
|
25925
|
-
|
|
25926
|
-
|
|
25927
|
-
|
|
25928
|
-
|
|
25929
|
-
|
|
25930
|
-
jsonbPath: "$.name[*].family",
|
|
25931
|
-
modifiers: ["exact", "contains", "missing", "not"]
|
|
25932
|
-
},
|
|
25933
|
-
{
|
|
25934
|
-
code: "given",
|
|
25935
|
-
type: "string",
|
|
25936
|
-
jsonbPath: "$.name[*].given",
|
|
25937
|
-
modifiers: ["exact", "contains", "missing", "not"]
|
|
25938
|
-
},
|
|
25939
|
-
{ code: "active", type: "token", jsonbPath: "$.active" },
|
|
25940
|
-
{ code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
|
|
25941
|
-
{
|
|
25942
|
-
code: "general-practitioner",
|
|
25943
|
-
type: "reference",
|
|
25944
|
-
jsonbPath: "$.generalPractitioner[*]"
|
|
25945
|
-
},
|
|
25946
|
-
{
|
|
25947
|
-
code: "organization",
|
|
25948
|
-
type: "reference",
|
|
25949
|
-
jsonbPath: "$.managingOrganization"
|
|
25616
|
+
// src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
|
|
25617
|
+
async function getPatientByIdRoute(req, res) {
|
|
25618
|
+
const id = String(req.params.id);
|
|
25619
|
+
const ctx = req.openhiContext;
|
|
25620
|
+
try {
|
|
25621
|
+
const result = await getPatientByIdOperation({ context: ctx, id });
|
|
25622
|
+
return res.json(result.resource);
|
|
25623
|
+
} catch (err) {
|
|
25624
|
+
const status = domainErrorToHttpStatus(err);
|
|
25625
|
+
if (status === 404) {
|
|
25626
|
+
const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
|
|
25627
|
+
return sendOperationOutcome404(res, diagnostics);
|
|
25628
|
+
}
|
|
25629
|
+
return sendOperationOutcome500(res, err, "GET Patient error:");
|
|
25950
25630
|
}
|
|
25951
|
-
|
|
25631
|
+
}
|
|
25952
25632
|
|
|
25953
|
-
// src/data/
|
|
25954
|
-
|
|
25955
|
-
|
|
25956
|
-
};
|
|
25957
|
-
|
|
25958
|
-
|
|
25959
|
-
|
|
25633
|
+
// src/data/operations/data/patient/patient-list-operation.ts
|
|
25634
|
+
async function listPatientsOperation(params) {
|
|
25635
|
+
const { context, tableName, mode } = params;
|
|
25636
|
+
const { tenantId, workspaceId } = context;
|
|
25637
|
+
const service = getDynamoDataService(tableName);
|
|
25638
|
+
return listDataEntitiesByWorkspace(
|
|
25639
|
+
service.entities.patient,
|
|
25640
|
+
tenantId,
|
|
25641
|
+
workspaceId,
|
|
25642
|
+
mode
|
|
25643
|
+
);
|
|
25960
25644
|
}
|
|
25961
25645
|
|
|
25962
25646
|
// src/data/rest-api/routes/data/patient/patient-list-route.ts
|
|
25963
25647
|
var PATIENT_RESOURCE_TYPE = "Patient";
|
|
25964
|
-
function
|
|
25648
|
+
function stripModifier4(key) {
|
|
25965
25649
|
const idx = key.indexOf(":");
|
|
25966
25650
|
return idx === -1 ? key : key.slice(0, idx);
|
|
25967
25651
|
}
|
|
25968
|
-
function
|
|
25652
|
+
function isResultParameter4(key) {
|
|
25969
25653
|
return key.startsWith("_");
|
|
25970
25654
|
}
|
|
25971
|
-
function
|
|
25655
|
+
function sendInvalidSearch4004(res, diagnostics) {
|
|
25972
25656
|
return res.status(400).json({
|
|
25973
25657
|
resourceType: "OperationOutcome",
|
|
25974
25658
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
25975
25659
|
});
|
|
25976
25660
|
}
|
|
25977
|
-
function
|
|
25661
|
+
function extractSearchParamKeys4(query) {
|
|
25978
25662
|
const out = [];
|
|
25979
25663
|
for (const rawKey of Object.keys(query)) {
|
|
25980
|
-
if (
|
|
25664
|
+
if (isResultParameter4(rawKey)) {
|
|
25981
25665
|
continue;
|
|
25982
25666
|
}
|
|
25983
|
-
out.push({ rawKey, code:
|
|
25667
|
+
out.push({ rawKey, code: stripModifier4(rawKey) });
|
|
25984
25668
|
}
|
|
25985
25669
|
return out;
|
|
25986
25670
|
}
|
|
25987
|
-
function
|
|
25671
|
+
function buildUnknownParamDiagnostics4(unknownCodes) {
|
|
25988
25672
|
const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
25989
25673
|
const codes = unknownCodes.join(", ");
|
|
25990
25674
|
const isPlural = unknownCodes.length !== 1;
|
|
@@ -25993,7 +25677,7 @@ function buildUnknownParamDiagnostics(unknownCodes) {
|
|
|
25993
25677
|
`Valid codes: ${validCodes}.`
|
|
25994
25678
|
].join(" ");
|
|
25995
25679
|
}
|
|
25996
|
-
function
|
|
25680
|
+
function findMalformedReference4(query, searchParamKeys) {
|
|
25997
25681
|
const referenceCodes = new Set(
|
|
25998
25682
|
getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
25999
25683
|
);
|
|
@@ -26016,7 +25700,7 @@ function findMalformedReference(query, searchParamKeys) {
|
|
|
26016
25700
|
return void 0;
|
|
26017
25701
|
}
|
|
26018
25702
|
async function listPatientsRoute(req, res) {
|
|
26019
|
-
const searchParamKeys =
|
|
25703
|
+
const searchParamKeys = extractSearchParamKeys4(
|
|
26020
25704
|
req.query
|
|
26021
25705
|
);
|
|
26022
25706
|
if (searchParamKeys.length === 0) {
|
|
@@ -26032,17 +25716,17 @@ async function listPatientsRoute(req, res) {
|
|
|
26032
25716
|
const validCodes = new Set(registered.map((p) => p.code));
|
|
26033
25717
|
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
26034
25718
|
if (unknownCodes.length > 0) {
|
|
26035
|
-
return
|
|
25719
|
+
return sendInvalidSearch4004(
|
|
26036
25720
|
res,
|
|
26037
|
-
|
|
25721
|
+
buildUnknownParamDiagnostics4([...new Set(unknownCodes)])
|
|
26038
25722
|
);
|
|
26039
25723
|
}
|
|
26040
|
-
const malformedRef =
|
|
25724
|
+
const malformedRef = findMalformedReference4(
|
|
26041
25725
|
req.query,
|
|
26042
25726
|
searchParamKeys
|
|
26043
25727
|
);
|
|
26044
25728
|
if (malformedRef !== void 0) {
|
|
26045
|
-
return
|
|
25729
|
+
return sendInvalidSearch4004(
|
|
26046
25730
|
res,
|
|
26047
25731
|
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
26048
25732
|
);
|
|
@@ -27381,15 +27065,108 @@ async function listProceduresOperation(params) {
|
|
|
27381
27065
|
}
|
|
27382
27066
|
|
|
27383
27067
|
// src/data/rest-api/routes/data/procedure/procedure-list-route.ts
|
|
27384
|
-
|
|
27385
|
-
|
|
27386
|
-
|
|
27387
|
-
|
|
27388
|
-
|
|
27389
|
-
|
|
27390
|
-
|
|
27068
|
+
var PROCEDURE_RESOURCE_TYPE = "Procedure";
|
|
27069
|
+
function stripModifier5(key) {
|
|
27070
|
+
const idx = key.indexOf(":");
|
|
27071
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
27072
|
+
}
|
|
27073
|
+
function isResultParameter5(key) {
|
|
27074
|
+
return key.startsWith("_");
|
|
27075
|
+
}
|
|
27076
|
+
function sendInvalidSearch4005(res, diagnostics) {
|
|
27077
|
+
return res.status(400).json({
|
|
27078
|
+
resourceType: "OperationOutcome",
|
|
27079
|
+
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
27391
27080
|
});
|
|
27392
27081
|
}
|
|
27082
|
+
function extractSearchParamKeys5(query) {
|
|
27083
|
+
const out = [];
|
|
27084
|
+
for (const rawKey of Object.keys(query)) {
|
|
27085
|
+
if (isResultParameter5(rawKey)) {
|
|
27086
|
+
continue;
|
|
27087
|
+
}
|
|
27088
|
+
out.push({ rawKey, code: stripModifier5(rawKey) });
|
|
27089
|
+
}
|
|
27090
|
+
return out;
|
|
27091
|
+
}
|
|
27092
|
+
function buildUnknownParamDiagnostics5(unknownCodes) {
|
|
27093
|
+
const validCodes = getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
27094
|
+
const codes = unknownCodes.join(", ");
|
|
27095
|
+
const isPlural = unknownCodes.length !== 1;
|
|
27096
|
+
return [
|
|
27097
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Procedure: ${codes}.`,
|
|
27098
|
+
`Valid codes: ${validCodes}.`
|
|
27099
|
+
].join(" ");
|
|
27100
|
+
}
|
|
27101
|
+
function findMalformedReference5(query, searchParamKeys) {
|
|
27102
|
+
const referenceCodes = new Set(
|
|
27103
|
+
getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
27104
|
+
);
|
|
27105
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
27106
|
+
if (!referenceCodes.has(code)) {
|
|
27107
|
+
continue;
|
|
27108
|
+
}
|
|
27109
|
+
const raw = query[rawKey];
|
|
27110
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
27111
|
+
for (const v of values) {
|
|
27112
|
+
const trimmed = v.trim();
|
|
27113
|
+
if (trimmed.length === 0) {
|
|
27114
|
+
continue;
|
|
27115
|
+
}
|
|
27116
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
27117
|
+
return { rawKey, value: trimmed };
|
|
27118
|
+
}
|
|
27119
|
+
}
|
|
27120
|
+
}
|
|
27121
|
+
return void 0;
|
|
27122
|
+
}
|
|
27123
|
+
async function listProceduresRoute(req, res) {
|
|
27124
|
+
const searchParamKeys = extractSearchParamKeys5(
|
|
27125
|
+
req.query
|
|
27126
|
+
);
|
|
27127
|
+
if (searchParamKeys.length === 0) {
|
|
27128
|
+
return handleListRoute({
|
|
27129
|
+
req,
|
|
27130
|
+
res,
|
|
27131
|
+
basePath: BASE_PATH.PROCEDURE,
|
|
27132
|
+
listOperation: listProceduresOperation,
|
|
27133
|
+
errorLogContext: "GET /Procedure list error:"
|
|
27134
|
+
});
|
|
27135
|
+
}
|
|
27136
|
+
const registered = getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE);
|
|
27137
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
27138
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
27139
|
+
if (unknownCodes.length > 0) {
|
|
27140
|
+
return sendInvalidSearch4005(
|
|
27141
|
+
res,
|
|
27142
|
+
buildUnknownParamDiagnostics5([...new Set(unknownCodes)])
|
|
27143
|
+
);
|
|
27144
|
+
}
|
|
27145
|
+
const malformedRef = findMalformedReference5(
|
|
27146
|
+
req.query,
|
|
27147
|
+
searchParamKeys
|
|
27148
|
+
);
|
|
27149
|
+
if (malformedRef !== void 0) {
|
|
27150
|
+
return sendInvalidSearch4005(
|
|
27151
|
+
res,
|
|
27152
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
27153
|
+
);
|
|
27154
|
+
}
|
|
27155
|
+
const ctx = req.openhiContext;
|
|
27156
|
+
try {
|
|
27157
|
+
const result = await genericSearchOperation({
|
|
27158
|
+
resourceType: PROCEDURE_RESOURCE_TYPE,
|
|
27159
|
+
tenantId: ctx.tenantId,
|
|
27160
|
+
workspaceId: ctx.workspaceId,
|
|
27161
|
+
query: req.query,
|
|
27162
|
+
resolver: defaultSearchParameterResolver
|
|
27163
|
+
});
|
|
27164
|
+
const bundle = buildSearchsetBundle(BASE_PATH.PROCEDURE, result.entries);
|
|
27165
|
+
return res.json(bundle);
|
|
27166
|
+
} catch (err) {
|
|
27167
|
+
return sendOperationOutcome500(res, err, "GET /Procedure search error:");
|
|
27168
|
+
}
|
|
27169
|
+
}
|
|
27393
27170
|
|
|
27394
27171
|
// src/data/operations/data/procedure/procedure-update-operation.ts
|
|
27395
27172
|
async function updateProcedureOperation(params) {
|
|
@@ -29845,7 +29622,7 @@ async function listSchedulesOperation(params) {
|
|
|
29845
29622
|
}
|
|
29846
29623
|
|
|
29847
29624
|
// src/data/operations/data/schedule/schedule-search-by-actor-operation.ts
|
|
29848
|
-
var
|
|
29625
|
+
var DEFAULT_LIMIT2 = 100;
|
|
29849
29626
|
function buildSearchSchedulesByActorSql() {
|
|
29850
29627
|
return [
|
|
29851
29628
|
"SELECT resource_id AS id, resource",
|
|
@@ -29863,7 +29640,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
29863
29640
|
const { context, actorReference } = params;
|
|
29864
29641
|
const { tenantId, workspaceId } = context;
|
|
29865
29642
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
29866
|
-
const limit = params.limit ??
|
|
29643
|
+
const limit = params.limit ?? DEFAULT_LIMIT2;
|
|
29867
29644
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
29868
29645
|
shape: { kind: "array-of-references", field: "actor" },
|
|
29869
29646
|
reference: actorReference,
|
|
@@ -29889,7 +29666,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
29889
29666
|
}
|
|
29890
29667
|
|
|
29891
29668
|
// src/data/rest-api/routes/data/schedule/schedule-list-route.ts
|
|
29892
|
-
function
|
|
29669
|
+
function singleStringQueryParam(req, name) {
|
|
29893
29670
|
const v = req.query[name];
|
|
29894
29671
|
if (typeof v !== "string") {
|
|
29895
29672
|
return void 0;
|
|
@@ -29897,17 +29674,17 @@ function singleStringQueryParam3(req, name) {
|
|
|
29897
29674
|
const trimmed = v.trim();
|
|
29898
29675
|
return trimmed === "" ? void 0 : trimmed;
|
|
29899
29676
|
}
|
|
29900
|
-
function
|
|
29677
|
+
function sendInvalidSearch4006(res, diagnostics) {
|
|
29901
29678
|
return res.status(400).json({
|
|
29902
29679
|
resourceType: "OperationOutcome",
|
|
29903
29680
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
29904
29681
|
});
|
|
29905
29682
|
}
|
|
29906
29683
|
async function listSchedulesRoute(req, res) {
|
|
29907
|
-
const actorRef =
|
|
29684
|
+
const actorRef = singleStringQueryParam(req, "actor");
|
|
29908
29685
|
if (actorRef !== void 0) {
|
|
29909
29686
|
if (parseTypedReference(actorRef) === void 0) {
|
|
29910
|
-
return
|
|
29687
|
+
return sendInvalidSearch4006(
|
|
29911
29688
|
res,
|
|
29912
29689
|
`?actor must be a typed reference like "Practitioner/<id>"; got "${actorRef}".`
|
|
29913
29690
|
);
|
|
@@ -33609,7 +33386,7 @@ async function listTasksOperation(params) {
|
|
|
33609
33386
|
}
|
|
33610
33387
|
|
|
33611
33388
|
// src/data/operations/data/task/task-search-by-owner-operation.ts
|
|
33612
|
-
var
|
|
33389
|
+
var DEFAULT_LIMIT3 = 100;
|
|
33613
33390
|
function buildSearchTasksByOwnerSql() {
|
|
33614
33391
|
return [
|
|
33615
33392
|
"SELECT resource_id AS id, resource",
|
|
@@ -33627,7 +33404,7 @@ async function searchTasksByOwnerOperation(params) {
|
|
|
33627
33404
|
const { context, ownerReference } = params;
|
|
33628
33405
|
const { tenantId, workspaceId } = context;
|
|
33629
33406
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
33630
|
-
const limit = params.limit ??
|
|
33407
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
33631
33408
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
33632
33409
|
shape: { kind: "scalar", field: "owner" },
|
|
33633
33410
|
reference: ownerReference,
|
|
@@ -33653,7 +33430,7 @@ async function searchTasksByOwnerOperation(params) {
|
|
|
33653
33430
|
}
|
|
33654
33431
|
|
|
33655
33432
|
// src/data/operations/data/task/task-search-by-requester-operation.ts
|
|
33656
|
-
var
|
|
33433
|
+
var DEFAULT_LIMIT4 = 100;
|
|
33657
33434
|
function buildSearchTasksByRequesterSql() {
|
|
33658
33435
|
return [
|
|
33659
33436
|
"SELECT resource_id AS id, resource",
|
|
@@ -33671,7 +33448,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
33671
33448
|
const { context, requesterReference } = params;
|
|
33672
33449
|
const { tenantId, workspaceId } = context;
|
|
33673
33450
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
33674
|
-
const limit = params.limit ??
|
|
33451
|
+
const limit = params.limit ?? DEFAULT_LIMIT4;
|
|
33675
33452
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
33676
33453
|
shape: { kind: "scalar", field: "requester" },
|
|
33677
33454
|
reference: requesterReference,
|
|
@@ -33697,7 +33474,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
33697
33474
|
}
|
|
33698
33475
|
|
|
33699
33476
|
// src/data/rest-api/routes/data/task/task-list-route.ts
|
|
33700
|
-
function
|
|
33477
|
+
function singleStringQueryParam2(req, name) {
|
|
33701
33478
|
const v = req.query[name];
|
|
33702
33479
|
if (typeof v !== "string") {
|
|
33703
33480
|
return void 0;
|
|
@@ -33705,24 +33482,24 @@ function singleStringQueryParam4(req, name) {
|
|
|
33705
33482
|
const trimmed = v.trim();
|
|
33706
33483
|
return trimmed === "" ? void 0 : trimmed;
|
|
33707
33484
|
}
|
|
33708
|
-
function
|
|
33485
|
+
function sendInvalidSearch4007(res, diagnostics) {
|
|
33709
33486
|
return res.status(400).json({
|
|
33710
33487
|
resourceType: "OperationOutcome",
|
|
33711
33488
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
33712
33489
|
});
|
|
33713
33490
|
}
|
|
33714
33491
|
async function listTasksRoute(req, res) {
|
|
33715
|
-
const ownerRef =
|
|
33716
|
-
const requesterRef =
|
|
33492
|
+
const ownerRef = singleStringQueryParam2(req, "owner");
|
|
33493
|
+
const requesterRef = singleStringQueryParam2(req, "requester");
|
|
33717
33494
|
if (ownerRef !== void 0 && requesterRef !== void 0) {
|
|
33718
|
-
return
|
|
33495
|
+
return sendInvalidSearch4007(
|
|
33719
33496
|
res,
|
|
33720
33497
|
"?owner= and ?requester= cannot be combined on the same request."
|
|
33721
33498
|
);
|
|
33722
33499
|
}
|
|
33723
33500
|
if (ownerRef !== void 0) {
|
|
33724
33501
|
if (parseTypedReference(ownerRef) === void 0) {
|
|
33725
|
-
return
|
|
33502
|
+
return sendInvalidSearch4007(
|
|
33726
33503
|
res,
|
|
33727
33504
|
`?owner must be a typed reference like "Practitioner/<id>"; got "${ownerRef}".`
|
|
33728
33505
|
);
|
|
@@ -33745,7 +33522,7 @@ async function listTasksRoute(req, res) {
|
|
|
33745
33522
|
}
|
|
33746
33523
|
if (requesterRef !== void 0) {
|
|
33747
33524
|
if (parseTypedReference(requesterRef) === void 0) {
|
|
33748
|
-
return
|
|
33525
|
+
return sendInvalidSearch4007(
|
|
33749
33526
|
res,
|
|
33750
33527
|
`?requester must be a typed reference like "Practitioner/<id>"; got "${requesterRef}".`
|
|
33751
33528
|
);
|