@openhi/constructs 0.0.135 → 0.0.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.mts +145 -37
- package/lib/index.d.ts +146 -38
- package/lib/index.js +177 -49
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +175 -49
- package/lib/index.mjs.map +1 -1
- package/lib/rest-api-lambda.handler.js +554 -66
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +554 -66
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -2495,7 +2495,6 @@ var ENV_VAR_NAMES = [
|
|
|
2495
2495
|
"OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID",
|
|
2496
2496
|
"OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID",
|
|
2497
2497
|
"OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL",
|
|
2498
|
-
"OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI",
|
|
2499
2498
|
"OPENHI_RUNTIME_CONFIG_API_BASE_URL"
|
|
2500
2499
|
];
|
|
2501
2500
|
var CACHE_CONTROL_HEADER = "public, max-age=300, s-maxage=300";
|
|
@@ -2512,7 +2511,6 @@ function runtimeConfigGetRoute(_req, res) {
|
|
|
2512
2511
|
cognitoUserPoolId: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID,
|
|
2513
2512
|
cognitoUserPoolClientId: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID,
|
|
2514
2513
|
cognitoDomainUrl: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL,
|
|
2515
|
-
cognitoRedirectUri: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI,
|
|
2516
2514
|
apiBaseUrl: process.env.OPENHI_RUNTIME_CONFIG_API_BASE_URL
|
|
2517
2515
|
};
|
|
2518
2516
|
res.setHeader("Cache-Control", CACHE_CONTROL_HEADER);
|
|
@@ -5532,6 +5530,29 @@ function getDefaultPostgresQueryRunner() {
|
|
|
5532
5530
|
}
|
|
5533
5531
|
|
|
5534
5532
|
// src/data/search/engine/date-predicate.ts
|
|
5533
|
+
var DATE_SEARCH_PREFIXES = [
|
|
5534
|
+
"eq",
|
|
5535
|
+
"gt",
|
|
5536
|
+
"lt",
|
|
5537
|
+
"ge",
|
|
5538
|
+
"le",
|
|
5539
|
+
"sa",
|
|
5540
|
+
"eb"
|
|
5541
|
+
];
|
|
5542
|
+
function isDateSearchPrefix(s) {
|
|
5543
|
+
return DATE_SEARCH_PREFIXES.includes(s);
|
|
5544
|
+
}
|
|
5545
|
+
var GENERIC_DATE_PREFIXES = ["eq", "gt", "ge", "lt", "le"];
|
|
5546
|
+
function isGenericDatePrefix(s) {
|
|
5547
|
+
return GENERIC_DATE_PREFIXES.includes(s);
|
|
5548
|
+
}
|
|
5549
|
+
function parseDateSearchValue(raw) {
|
|
5550
|
+
const head = raw.slice(0, 2);
|
|
5551
|
+
if (isDateSearchPrefix(head) && raw.length > 2) {
|
|
5552
|
+
return { prefix: head, value: raw.slice(2) };
|
|
5553
|
+
}
|
|
5554
|
+
return { prefix: "eq", value: raw };
|
|
5555
|
+
}
|
|
5535
5556
|
function flatJsonbExtract(jsonbPath) {
|
|
5536
5557
|
const match = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5537
5558
|
if (!match) {
|
|
@@ -5541,6 +5562,40 @@ function flatJsonbExtract(jsonbPath) {
|
|
|
5541
5562
|
}
|
|
5542
5563
|
return `resource->>'${match[1]}'`;
|
|
5543
5564
|
}
|
|
5565
|
+
function emitDatePredicate(opts) {
|
|
5566
|
+
const { jsonbPath, rawValue, paramName } = opts;
|
|
5567
|
+
const extract = flatJsonbExtract(jsonbPath);
|
|
5568
|
+
const { prefix, value } = parseDateSearchValue(rawValue);
|
|
5569
|
+
if (!isGenericDatePrefix(prefix)) {
|
|
5570
|
+
throw new Error(
|
|
5571
|
+
`Generic date predicate does not support prefix "${prefix}". Supported: ${GENERIC_DATE_PREFIXES.join(", ")}.`
|
|
5572
|
+
);
|
|
5573
|
+
}
|
|
5574
|
+
const sql = buildSingleBoundSql(extract, prefix, paramName);
|
|
5575
|
+
const params = [
|
|
5576
|
+
{ name: paramName, value }
|
|
5577
|
+
];
|
|
5578
|
+
return { sql, params };
|
|
5579
|
+
}
|
|
5580
|
+
function buildSingleBoundSql(extract, prefix, paramName) {
|
|
5581
|
+
switch (prefix) {
|
|
5582
|
+
case "eq":
|
|
5583
|
+
return `${extract} = :${paramName}`;
|
|
5584
|
+
case "gt":
|
|
5585
|
+
return `${extract} > :${paramName}`;
|
|
5586
|
+
case "ge":
|
|
5587
|
+
return `${extract} >= :${paramName}`;
|
|
5588
|
+
case "lt":
|
|
5589
|
+
return `${extract} < :${paramName}`;
|
|
5590
|
+
case "le":
|
|
5591
|
+
return `${extract} <= :${paramName}`;
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
function emitFieldMissingPredicate(opts) {
|
|
5595
|
+
const extract = flatJsonbExtract(opts.jsonbPath);
|
|
5596
|
+
const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
|
|
5597
|
+
return { sql, params: [] };
|
|
5598
|
+
}
|
|
5544
5599
|
var DEFAULT_INTERVAL_PARAM_PREFIX = "intervalDateConstraint";
|
|
5545
5600
|
function intervalConstraintParamName(prefix, index) {
|
|
5546
5601
|
return `${prefix}${index}`;
|
|
@@ -5656,6 +5711,46 @@ function buildReferenceContainmentPayload(params) {
|
|
|
5656
5711
|
containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
|
|
5657
5712
|
};
|
|
5658
5713
|
}
|
|
5714
|
+
function jsonbPathToReferenceShape(jsonbPath) {
|
|
5715
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
5716
|
+
if (scalar) {
|
|
5717
|
+
return { kind: "scalar", field: scalar[1] };
|
|
5718
|
+
}
|
|
5719
|
+
const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
5720
|
+
if (arrayOfRefs) {
|
|
5721
|
+
return { kind: "array-of-references", field: arrayOfRefs[1] };
|
|
5722
|
+
}
|
|
5723
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
5724
|
+
jsonbPath
|
|
5725
|
+
);
|
|
5726
|
+
if (arrayOfObjs) {
|
|
5727
|
+
return {
|
|
5728
|
+
kind: "array-of-objects",
|
|
5729
|
+
field: arrayOfObjs[1],
|
|
5730
|
+
subfield: arrayOfObjs[2]
|
|
5731
|
+
};
|
|
5732
|
+
}
|
|
5733
|
+
throw new Error(
|
|
5734
|
+
`Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
5735
|
+
);
|
|
5736
|
+
}
|
|
5737
|
+
function emitReferencePredicate(opts) {
|
|
5738
|
+
const shape = jsonbPathToReferenceShape(opts.jsonbPath);
|
|
5739
|
+
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
5740
|
+
shape,
|
|
5741
|
+
reference: opts.rawValue,
|
|
5742
|
+
tenantId: opts.context.tenantId,
|
|
5743
|
+
workspaceId: opts.context.workspaceId
|
|
5744
|
+
});
|
|
5745
|
+
const relName = `${opts.paramName}R`;
|
|
5746
|
+
const urnName = `${opts.paramName}U`;
|
|
5747
|
+
const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
|
|
5748
|
+
const params = [
|
|
5749
|
+
{ name: relName, value: containmentRelative },
|
|
5750
|
+
{ name: urnName, value: containmentUrn }
|
|
5751
|
+
];
|
|
5752
|
+
return { sql, params };
|
|
5753
|
+
}
|
|
5659
5754
|
|
|
5660
5755
|
// src/data/operations/data/appointment/appointment-search-by-actor-operation.ts
|
|
5661
5756
|
var DEFAULT_LIMIT = 100;
|
|
@@ -25481,39 +25576,334 @@ async function listPatientsOperation(params) {
|
|
|
25481
25576
|
);
|
|
25482
25577
|
}
|
|
25483
25578
|
|
|
25484
|
-
// src/data/
|
|
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: [] };
|
|
25867
|
+
}
|
|
25868
|
+
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
25869
|
+
}
|
|
25870
|
+
|
|
25871
|
+
// src/data/search/operations/generic-search-operation.ts
|
|
25485
25872
|
var DEFAULT_LIMIT7 = 100;
|
|
25486
|
-
function
|
|
25487
|
-
|
|
25873
|
+
function buildGenericSearchSql(opts) {
|
|
25874
|
+
const lines = [
|
|
25488
25875
|
"SELECT resource_id AS id, resource",
|
|
25489
25876
|
"FROM resources",
|
|
25490
25877
|
"WHERE tenant_id = :tenantId",
|
|
25491
25878
|
" AND workspace_id = :workspaceId",
|
|
25492
|
-
" AND resource_type =
|
|
25493
|
-
" AND deleted_at IS NULL"
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25497
|
-
|
|
25879
|
+
" AND resource_type = :resourceType",
|
|
25880
|
+
" AND deleted_at IS NULL"
|
|
25881
|
+
];
|
|
25882
|
+
if (opts.combinedSql.length > 0) {
|
|
25883
|
+
lines.push(` AND (${opts.combinedSql})`);
|
|
25884
|
+
}
|
|
25885
|
+
lines.push("ORDER BY last_updated DESC");
|
|
25886
|
+
lines.push("LIMIT :limit;");
|
|
25887
|
+
return lines.join("\n");
|
|
25498
25888
|
}
|
|
25499
|
-
async function
|
|
25500
|
-
const {
|
|
25501
|
-
const { tenantId, workspaceId } = context;
|
|
25889
|
+
async function genericSearchOperation(params) {
|
|
25890
|
+
const { resourceType, tenantId, workspaceId, query, resolver } = params;
|
|
25502
25891
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
25503
25892
|
const limit = params.limit ?? DEFAULT_LIMIT7;
|
|
25504
|
-
const
|
|
25505
|
-
|
|
25506
|
-
|
|
25507
|
-
|
|
25508
|
-
|
|
25893
|
+
const registeredParams = resolver(resourceType, tenantId);
|
|
25894
|
+
const context = { tenantId, workspaceId, resourceType };
|
|
25895
|
+
const combined = combineSearchPredicates({
|
|
25896
|
+
query,
|
|
25897
|
+
registeredParams,
|
|
25898
|
+
context
|
|
25509
25899
|
});
|
|
25510
|
-
const sql =
|
|
25900
|
+
const sql = buildGenericSearchSql({ combinedSql: combined.sql });
|
|
25511
25901
|
const queryParams = [
|
|
25512
25902
|
{ name: "tenantId", value: tenantId },
|
|
25513
25903
|
{ name: "workspaceId", value: workspaceId },
|
|
25514
|
-
{ name: "
|
|
25515
|
-
{ name: "
|
|
25516
|
-
|
|
25904
|
+
{ name: "resourceType", value: resourceType },
|
|
25905
|
+
{ name: "limit", value: limit },
|
|
25906
|
+
...combined.params
|
|
25517
25907
|
];
|
|
25518
25908
|
const rows = await runner.query(sql, queryParams);
|
|
25519
25909
|
const entries = rows.map((row) => ({
|
|
@@ -25523,14 +25913,60 @@ async function searchPatientsByGeneralPractitionerOperation(params) {
|
|
|
25523
25913
|
return { entries, total: entries.length };
|
|
25524
25914
|
}
|
|
25525
25915
|
|
|
25526
|
-
// src/data/
|
|
25527
|
-
|
|
25528
|
-
|
|
25529
|
-
|
|
25530
|
-
|
|
25916
|
+
// src/data/search/registry/patient-search-parameters.ts
|
|
25917
|
+
var PATIENT_SEARCH_PARAMETERS = [
|
|
25918
|
+
{ code: "gender", type: "token", jsonbPath: "$.gender" },
|
|
25919
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
|
|
25920
|
+
{ code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
|
|
25921
|
+
{
|
|
25922
|
+
code: "name",
|
|
25923
|
+
type: "string",
|
|
25924
|
+
jsonbPath: "$.name[*].text",
|
|
25925
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
25926
|
+
},
|
|
25927
|
+
{
|
|
25928
|
+
code: "family",
|
|
25929
|
+
type: "string",
|
|
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"
|
|
25531
25950
|
}
|
|
25532
|
-
|
|
25533
|
-
|
|
25951
|
+
];
|
|
25952
|
+
|
|
25953
|
+
// src/data/search/registry/resolver.ts
|
|
25954
|
+
var STATIC_SEARCH_PARAMETER_MAP = {
|
|
25955
|
+
Patient: PATIENT_SEARCH_PARAMETERS
|
|
25956
|
+
};
|
|
25957
|
+
var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
25958
|
+
function getRegisteredSearchParameters(resourceType) {
|
|
25959
|
+
return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
25960
|
+
}
|
|
25961
|
+
|
|
25962
|
+
// src/data/rest-api/routes/data/patient/patient-list-route.ts
|
|
25963
|
+
var PATIENT_RESOURCE_TYPE = "Patient";
|
|
25964
|
+
function stripModifier(key) {
|
|
25965
|
+
const idx = key.indexOf(":");
|
|
25966
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
25967
|
+
}
|
|
25968
|
+
function isResultParameter(key) {
|
|
25969
|
+
return key.startsWith("_");
|
|
25534
25970
|
}
|
|
25535
25971
|
function sendInvalidSearch4003(res, diagnostics) {
|
|
25536
25972
|
return res.status(400).json({
|
|
@@ -25538,41 +25974,93 @@ function sendInvalidSearch4003(res, diagnostics) {
|
|
|
25538
25974
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
25539
25975
|
});
|
|
25540
25976
|
}
|
|
25541
|
-
|
|
25542
|
-
const
|
|
25543
|
-
|
|
25544
|
-
|
|
25977
|
+
function extractSearchParamKeys(query) {
|
|
25978
|
+
const out = [];
|
|
25979
|
+
for (const rawKey of Object.keys(query)) {
|
|
25980
|
+
if (isResultParameter(rawKey)) {
|
|
25981
|
+
continue;
|
|
25982
|
+
}
|
|
25983
|
+
out.push({ rawKey, code: stripModifier(rawKey) });
|
|
25984
|
+
}
|
|
25985
|
+
return out;
|
|
25986
|
+
}
|
|
25987
|
+
function buildUnknownParamDiagnostics(unknownCodes) {
|
|
25988
|
+
const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
25989
|
+
const codes = unknownCodes.join(", ");
|
|
25990
|
+
const isPlural = unknownCodes.length !== 1;
|
|
25991
|
+
return [
|
|
25992
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Patient: ${codes}.`,
|
|
25993
|
+
`Valid codes: ${validCodes}.`
|
|
25994
|
+
].join(" ");
|
|
25995
|
+
}
|
|
25996
|
+
function findMalformedReference(query, searchParamKeys) {
|
|
25997
|
+
const referenceCodes = new Set(
|
|
25998
|
+
getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
25545
25999
|
);
|
|
25546
|
-
|
|
25547
|
-
if (
|
|
25548
|
-
|
|
25549
|
-
res,
|
|
25550
|
-
`?general-practitioner must be a typed reference like "Practitioner/<id>"; got "${generalPractitionerRef}".`
|
|
25551
|
-
);
|
|
26000
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
26001
|
+
if (!referenceCodes.has(code)) {
|
|
26002
|
+
continue;
|
|
25552
26003
|
}
|
|
25553
|
-
const
|
|
25554
|
-
|
|
25555
|
-
|
|
25556
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
25560
|
-
|
|
25561
|
-
|
|
25562
|
-
|
|
25563
|
-
res,
|
|
25564
|
-
err,
|
|
25565
|
-
"GET /Patient?general-practitioner= search error:"
|
|
25566
|
-
);
|
|
26004
|
+
const raw = query[rawKey];
|
|
26005
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
26006
|
+
for (const v of values) {
|
|
26007
|
+
const trimmed = v.trim();
|
|
26008
|
+
if (trimmed.length === 0) {
|
|
26009
|
+
continue;
|
|
26010
|
+
}
|
|
26011
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
26012
|
+
return { rawKey, value: trimmed };
|
|
26013
|
+
}
|
|
25567
26014
|
}
|
|
25568
26015
|
}
|
|
25569
|
-
return
|
|
25570
|
-
|
|
25571
|
-
|
|
25572
|
-
|
|
25573
|
-
|
|
25574
|
-
|
|
25575
|
-
|
|
26016
|
+
return void 0;
|
|
26017
|
+
}
|
|
26018
|
+
async function listPatientsRoute(req, res) {
|
|
26019
|
+
const searchParamKeys = extractSearchParamKeys(
|
|
26020
|
+
req.query
|
|
26021
|
+
);
|
|
26022
|
+
if (searchParamKeys.length === 0) {
|
|
26023
|
+
return handleListRoute({
|
|
26024
|
+
req,
|
|
26025
|
+
res,
|
|
26026
|
+
basePath: BASE_PATH.PATIENT,
|
|
26027
|
+
listOperation: listPatientsOperation,
|
|
26028
|
+
errorLogContext: "GET /Patient list error:"
|
|
26029
|
+
});
|
|
26030
|
+
}
|
|
26031
|
+
const registered = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE);
|
|
26032
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
26033
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
26034
|
+
if (unknownCodes.length > 0) {
|
|
26035
|
+
return sendInvalidSearch4003(
|
|
26036
|
+
res,
|
|
26037
|
+
buildUnknownParamDiagnostics([...new Set(unknownCodes)])
|
|
26038
|
+
);
|
|
26039
|
+
}
|
|
26040
|
+
const malformedRef = findMalformedReference(
|
|
26041
|
+
req.query,
|
|
26042
|
+
searchParamKeys
|
|
26043
|
+
);
|
|
26044
|
+
if (malformedRef !== void 0) {
|
|
26045
|
+
return sendInvalidSearch4003(
|
|
26046
|
+
res,
|
|
26047
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
26048
|
+
);
|
|
26049
|
+
}
|
|
26050
|
+
const ctx = req.openhiContext;
|
|
26051
|
+
try {
|
|
26052
|
+
const result = await genericSearchOperation({
|
|
26053
|
+
resourceType: PATIENT_RESOURCE_TYPE,
|
|
26054
|
+
tenantId: ctx.tenantId,
|
|
26055
|
+
workspaceId: ctx.workspaceId,
|
|
26056
|
+
query: req.query,
|
|
26057
|
+
resolver: defaultSearchParameterResolver
|
|
26058
|
+
});
|
|
26059
|
+
const bundle = buildSearchsetBundle(BASE_PATH.PATIENT, result.entries);
|
|
26060
|
+
return res.json(bundle);
|
|
26061
|
+
} catch (err) {
|
|
26062
|
+
return sendOperationOutcome500(res, err, "GET /Patient search error:");
|
|
26063
|
+
}
|
|
25576
26064
|
}
|
|
25577
26065
|
|
|
25578
26066
|
// src/data/operations/data/patient/patient-update-operation.ts
|
|
@@ -29401,7 +29889,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
29401
29889
|
}
|
|
29402
29890
|
|
|
29403
29891
|
// src/data/rest-api/routes/data/schedule/schedule-list-route.ts
|
|
29404
|
-
function
|
|
29892
|
+
function singleStringQueryParam3(req, name) {
|
|
29405
29893
|
const v = req.query[name];
|
|
29406
29894
|
if (typeof v !== "string") {
|
|
29407
29895
|
return void 0;
|
|
@@ -29416,7 +29904,7 @@ function sendInvalidSearch4004(res, diagnostics) {
|
|
|
29416
29904
|
});
|
|
29417
29905
|
}
|
|
29418
29906
|
async function listSchedulesRoute(req, res) {
|
|
29419
|
-
const actorRef =
|
|
29907
|
+
const actorRef = singleStringQueryParam3(req, "actor");
|
|
29420
29908
|
if (actorRef !== void 0) {
|
|
29421
29909
|
if (parseTypedReference(actorRef) === void 0) {
|
|
29422
29910
|
return sendInvalidSearch4004(
|
|
@@ -33209,7 +33697,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
33209
33697
|
}
|
|
33210
33698
|
|
|
33211
33699
|
// src/data/rest-api/routes/data/task/task-list-route.ts
|
|
33212
|
-
function
|
|
33700
|
+
function singleStringQueryParam4(req, name) {
|
|
33213
33701
|
const v = req.query[name];
|
|
33214
33702
|
if (typeof v !== "string") {
|
|
33215
33703
|
return void 0;
|
|
@@ -33224,8 +33712,8 @@ function sendInvalidSearch4005(res, diagnostics) {
|
|
|
33224
33712
|
});
|
|
33225
33713
|
}
|
|
33226
33714
|
async function listTasksRoute(req, res) {
|
|
33227
|
-
const ownerRef =
|
|
33228
|
-
const requesterRef =
|
|
33715
|
+
const ownerRef = singleStringQueryParam4(req, "owner");
|
|
33716
|
+
const requesterRef = singleStringQueryParam4(req, "requester");
|
|
33229
33717
|
if (ownerRef !== void 0 && requesterRef !== void 0) {
|
|
33230
33718
|
return sendInvalidSearch4005(
|
|
33231
33719
|
res,
|