@openhi/constructs 0.0.135 → 0.0.137
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 +182 -53
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +180 -53
- 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 +3 -3
|
@@ -6282,7 +6282,6 @@ var ENV_VAR_NAMES = [
|
|
|
6282
6282
|
"OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID",
|
|
6283
6283
|
"OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID",
|
|
6284
6284
|
"OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL",
|
|
6285
|
-
"OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI",
|
|
6286
6285
|
"OPENHI_RUNTIME_CONFIG_API_BASE_URL"
|
|
6287
6286
|
];
|
|
6288
6287
|
var CACHE_CONTROL_HEADER = "public, max-age=300, s-maxage=300";
|
|
@@ -6299,7 +6298,6 @@ function runtimeConfigGetRoute(_req, res) {
|
|
|
6299
6298
|
cognitoUserPoolId: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID,
|
|
6300
6299
|
cognitoUserPoolClientId: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID,
|
|
6301
6300
|
cognitoDomainUrl: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL,
|
|
6302
|
-
cognitoRedirectUri: process.env.OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI,
|
|
6303
6301
|
apiBaseUrl: process.env.OPENHI_RUNTIME_CONFIG_API_BASE_URL
|
|
6304
6302
|
};
|
|
6305
6303
|
res.setHeader("Cache-Control", CACHE_CONTROL_HEADER);
|
|
@@ -9763,6 +9761,29 @@ function getDefaultPostgresQueryRunner() {
|
|
|
9763
9761
|
}
|
|
9764
9762
|
|
|
9765
9763
|
// src/data/search/engine/date-predicate.ts
|
|
9764
|
+
var DATE_SEARCH_PREFIXES = [
|
|
9765
|
+
"eq",
|
|
9766
|
+
"gt",
|
|
9767
|
+
"lt",
|
|
9768
|
+
"ge",
|
|
9769
|
+
"le",
|
|
9770
|
+
"sa",
|
|
9771
|
+
"eb"
|
|
9772
|
+
];
|
|
9773
|
+
function isDateSearchPrefix(s) {
|
|
9774
|
+
return DATE_SEARCH_PREFIXES.includes(s);
|
|
9775
|
+
}
|
|
9776
|
+
var GENERIC_DATE_PREFIXES = ["eq", "gt", "ge", "lt", "le"];
|
|
9777
|
+
function isGenericDatePrefix(s) {
|
|
9778
|
+
return GENERIC_DATE_PREFIXES.includes(s);
|
|
9779
|
+
}
|
|
9780
|
+
function parseDateSearchValue(raw) {
|
|
9781
|
+
const head = raw.slice(0, 2);
|
|
9782
|
+
if (isDateSearchPrefix(head) && raw.length > 2) {
|
|
9783
|
+
return { prefix: head, value: raw.slice(2) };
|
|
9784
|
+
}
|
|
9785
|
+
return { prefix: "eq", value: raw };
|
|
9786
|
+
}
|
|
9766
9787
|
function flatJsonbExtract(jsonbPath) {
|
|
9767
9788
|
const match = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9768
9789
|
if (!match) {
|
|
@@ -9772,6 +9793,40 @@ function flatJsonbExtract(jsonbPath) {
|
|
|
9772
9793
|
}
|
|
9773
9794
|
return `resource->>'${match[1]}'`;
|
|
9774
9795
|
}
|
|
9796
|
+
function emitDatePredicate(opts) {
|
|
9797
|
+
const { jsonbPath, rawValue, paramName } = opts;
|
|
9798
|
+
const extract = flatJsonbExtract(jsonbPath);
|
|
9799
|
+
const { prefix, value } = parseDateSearchValue(rawValue);
|
|
9800
|
+
if (!isGenericDatePrefix(prefix)) {
|
|
9801
|
+
throw new Error(
|
|
9802
|
+
`Generic date predicate does not support prefix "${prefix}". Supported: ${GENERIC_DATE_PREFIXES.join(", ")}.`
|
|
9803
|
+
);
|
|
9804
|
+
}
|
|
9805
|
+
const sql = buildSingleBoundSql(extract, prefix, paramName);
|
|
9806
|
+
const params = [
|
|
9807
|
+
{ name: paramName, value }
|
|
9808
|
+
];
|
|
9809
|
+
return { sql, params };
|
|
9810
|
+
}
|
|
9811
|
+
function buildSingleBoundSql(extract, prefix, paramName) {
|
|
9812
|
+
switch (prefix) {
|
|
9813
|
+
case "eq":
|
|
9814
|
+
return `${extract} = :${paramName}`;
|
|
9815
|
+
case "gt":
|
|
9816
|
+
return `${extract} > :${paramName}`;
|
|
9817
|
+
case "ge":
|
|
9818
|
+
return `${extract} >= :${paramName}`;
|
|
9819
|
+
case "lt":
|
|
9820
|
+
return `${extract} < :${paramName}`;
|
|
9821
|
+
case "le":
|
|
9822
|
+
return `${extract} <= :${paramName}`;
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
function emitFieldMissingPredicate(opts) {
|
|
9826
|
+
const extract = flatJsonbExtract(opts.jsonbPath);
|
|
9827
|
+
const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
|
|
9828
|
+
return { sql, params: [] };
|
|
9829
|
+
}
|
|
9775
9830
|
var DEFAULT_INTERVAL_PARAM_PREFIX = "intervalDateConstraint";
|
|
9776
9831
|
function intervalConstraintParamName(prefix, index) {
|
|
9777
9832
|
return `${prefix}${index}`;
|
|
@@ -9887,6 +9942,46 @@ function buildReferenceContainmentPayload(params) {
|
|
|
9887
9942
|
containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
|
|
9888
9943
|
};
|
|
9889
9944
|
}
|
|
9945
|
+
function jsonbPathToReferenceShape(jsonbPath) {
|
|
9946
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9947
|
+
if (scalar) {
|
|
9948
|
+
return { kind: "scalar", field: scalar[1] };
|
|
9949
|
+
}
|
|
9950
|
+
const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
9951
|
+
if (arrayOfRefs) {
|
|
9952
|
+
return { kind: "array-of-references", field: arrayOfRefs[1] };
|
|
9953
|
+
}
|
|
9954
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
9955
|
+
jsonbPath
|
|
9956
|
+
);
|
|
9957
|
+
if (arrayOfObjs) {
|
|
9958
|
+
return {
|
|
9959
|
+
kind: "array-of-objects",
|
|
9960
|
+
field: arrayOfObjs[1],
|
|
9961
|
+
subfield: arrayOfObjs[2]
|
|
9962
|
+
};
|
|
9963
|
+
}
|
|
9964
|
+
throw new Error(
|
|
9965
|
+
`Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
9966
|
+
);
|
|
9967
|
+
}
|
|
9968
|
+
function emitReferencePredicate(opts) {
|
|
9969
|
+
const shape = jsonbPathToReferenceShape(opts.jsonbPath);
|
|
9970
|
+
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
9971
|
+
shape,
|
|
9972
|
+
reference: opts.rawValue,
|
|
9973
|
+
tenantId: opts.context.tenantId,
|
|
9974
|
+
workspaceId: opts.context.workspaceId
|
|
9975
|
+
});
|
|
9976
|
+
const relName = `${opts.paramName}R`;
|
|
9977
|
+
const urnName = `${opts.paramName}U`;
|
|
9978
|
+
const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
|
|
9979
|
+
const params = [
|
|
9980
|
+
{ name: relName, value: containmentRelative },
|
|
9981
|
+
{ name: urnName, value: containmentUrn }
|
|
9982
|
+
];
|
|
9983
|
+
return { sql, params };
|
|
9984
|
+
}
|
|
9890
9985
|
|
|
9891
9986
|
// src/data/operations/data/appointment/appointment-search-by-actor-operation.ts
|
|
9892
9987
|
var DEFAULT_LIMIT = 100;
|
|
@@ -29817,39 +29912,334 @@ async function listPatientsOperation(params) {
|
|
|
29817
29912
|
);
|
|
29818
29913
|
}
|
|
29819
29914
|
|
|
29820
|
-
// src/data/
|
|
29915
|
+
// src/data/search/engine/string-predicate.ts
|
|
29916
|
+
var STRING_MODIFIERS = ["exact", "contains"];
|
|
29917
|
+
function isStringModifier(s) {
|
|
29918
|
+
return STRING_MODIFIERS.includes(s);
|
|
29919
|
+
}
|
|
29920
|
+
function jsonbPathToStringShape(jsonbPath) {
|
|
29921
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
29922
|
+
if (scalar) {
|
|
29923
|
+
return { kind: "scalar", field: scalar[1] };
|
|
29924
|
+
}
|
|
29925
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
29926
|
+
if (arrayOfScalars) {
|
|
29927
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
29928
|
+
}
|
|
29929
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
29930
|
+
jsonbPath
|
|
29931
|
+
);
|
|
29932
|
+
if (arrayOfObjs) {
|
|
29933
|
+
return {
|
|
29934
|
+
kind: "array-of-objects",
|
|
29935
|
+
field: arrayOfObjs[1],
|
|
29936
|
+
subfield: arrayOfObjs[2]
|
|
29937
|
+
};
|
|
29938
|
+
}
|
|
29939
|
+
throw new Error(
|
|
29940
|
+
`String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
29941
|
+
);
|
|
29942
|
+
}
|
|
29943
|
+
function escapeLikePattern(value) {
|
|
29944
|
+
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
29945
|
+
}
|
|
29946
|
+
function buildLikePattern(value, modifier) {
|
|
29947
|
+
const escaped = escapeLikePattern(value);
|
|
29948
|
+
switch (modifier) {
|
|
29949
|
+
case "exact":
|
|
29950
|
+
return escaped;
|
|
29951
|
+
case "contains":
|
|
29952
|
+
return `%${escaped}%`;
|
|
29953
|
+
case void 0:
|
|
29954
|
+
return `${escaped}%`;
|
|
29955
|
+
}
|
|
29956
|
+
}
|
|
29957
|
+
function buildIlikeExtractSql(shape, paramName) {
|
|
29958
|
+
switch (shape.kind) {
|
|
29959
|
+
case "scalar":
|
|
29960
|
+
return `resource->>'${shape.field}' ILIKE :${paramName}`;
|
|
29961
|
+
case "array-of-scalars":
|
|
29962
|
+
return [
|
|
29963
|
+
"EXISTS (SELECT 1 FROM",
|
|
29964
|
+
`jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
|
|
29965
|
+
`WHERE s_elem.text_val ILIKE :${paramName})`
|
|
29966
|
+
].join(" ");
|
|
29967
|
+
case "array-of-objects":
|
|
29968
|
+
return [
|
|
29969
|
+
"EXISTS (SELECT 1 FROM",
|
|
29970
|
+
`jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
|
|
29971
|
+
`WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
|
|
29972
|
+
].join(" ");
|
|
29973
|
+
}
|
|
29974
|
+
}
|
|
29975
|
+
function emitStringPredicate(opts) {
|
|
29976
|
+
const shape = jsonbPathToStringShape(opts.jsonbPath);
|
|
29977
|
+
const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
|
|
29978
|
+
const sql = buildIlikeExtractSql(shape, opts.paramName);
|
|
29979
|
+
const pattern = buildLikePattern(opts.rawValue, modifier);
|
|
29980
|
+
const params = [
|
|
29981
|
+
{ name: opts.paramName, value: pattern }
|
|
29982
|
+
];
|
|
29983
|
+
return { sql, params };
|
|
29984
|
+
}
|
|
29985
|
+
function checkModifier(modifier) {
|
|
29986
|
+
if (!isStringModifier(modifier)) {
|
|
29987
|
+
throw new Error(
|
|
29988
|
+
`String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
|
|
29989
|
+
);
|
|
29990
|
+
}
|
|
29991
|
+
return modifier;
|
|
29992
|
+
}
|
|
29993
|
+
|
|
29994
|
+
// src/data/search/engine/token-predicate.ts
|
|
29995
|
+
function jsonbPathToTokenShape(jsonbPath) {
|
|
29996
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
29997
|
+
if (scalar) {
|
|
29998
|
+
return { kind: "scalar", field: scalar[1] };
|
|
29999
|
+
}
|
|
30000
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
30001
|
+
if (arrayOfScalars) {
|
|
30002
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
30003
|
+
}
|
|
30004
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
30005
|
+
jsonbPath
|
|
30006
|
+
);
|
|
30007
|
+
if (arrayOfObjs) {
|
|
30008
|
+
return {
|
|
30009
|
+
kind: "array-of-objects",
|
|
30010
|
+
field: arrayOfObjs[1],
|
|
30011
|
+
subfield: arrayOfObjs[2]
|
|
30012
|
+
};
|
|
30013
|
+
}
|
|
30014
|
+
throw new Error(
|
|
30015
|
+
`Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
30016
|
+
);
|
|
30017
|
+
}
|
|
30018
|
+
function parseTokenValue(raw) {
|
|
30019
|
+
const idx = raw.indexOf("|");
|
|
30020
|
+
if (idx === -1) {
|
|
30021
|
+
return { code: raw };
|
|
30022
|
+
}
|
|
30023
|
+
const system = raw.slice(0, idx);
|
|
30024
|
+
const code = raw.slice(idx + 1);
|
|
30025
|
+
return system.length > 0 ? { system, code } : { code };
|
|
30026
|
+
}
|
|
30027
|
+
function wrapTokenInShape(shape, value) {
|
|
30028
|
+
switch (shape.kind) {
|
|
30029
|
+
case "scalar":
|
|
30030
|
+
return { [shape.field]: value };
|
|
30031
|
+
case "array-of-scalars":
|
|
30032
|
+
return { [shape.field]: [value] };
|
|
30033
|
+
case "array-of-objects":
|
|
30034
|
+
return { [shape.field]: [{ [shape.subfield]: value }] };
|
|
30035
|
+
}
|
|
30036
|
+
}
|
|
30037
|
+
function emitTokenPredicate(opts) {
|
|
30038
|
+
const shape = jsonbPathToTokenShape(opts.jsonbPath);
|
|
30039
|
+
const { code } = parseTokenValue(opts.rawValue);
|
|
30040
|
+
const payload = wrapTokenInShape(shape, code);
|
|
30041
|
+
const sql = `resource @> :${opts.paramName}::jsonb`;
|
|
30042
|
+
const params = [
|
|
30043
|
+
{ name: opts.paramName, value: JSON.stringify(payload) }
|
|
30044
|
+
];
|
|
30045
|
+
return { sql, params };
|
|
30046
|
+
}
|
|
30047
|
+
|
|
30048
|
+
// src/data/search/engine/combinator.ts
|
|
30049
|
+
function parseQueryKey(key) {
|
|
30050
|
+
const idx = key.indexOf(":");
|
|
30051
|
+
if (idx === -1) {
|
|
30052
|
+
return { code: key, modifier: void 0 };
|
|
30053
|
+
}
|
|
30054
|
+
return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
|
|
30055
|
+
}
|
|
30056
|
+
function flattenQueryValues(raw) {
|
|
30057
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
30058
|
+
const out = [];
|
|
30059
|
+
for (const v of list) {
|
|
30060
|
+
for (const piece of v.split(",")) {
|
|
30061
|
+
out.push(piece);
|
|
30062
|
+
}
|
|
30063
|
+
}
|
|
30064
|
+
return out;
|
|
30065
|
+
}
|
|
30066
|
+
function parseQueryEntries(query) {
|
|
30067
|
+
const entries = [];
|
|
30068
|
+
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
30069
|
+
if (rawValue === void 0) continue;
|
|
30070
|
+
const { code, modifier } = parseQueryKey(rawKey);
|
|
30071
|
+
entries.push({
|
|
30072
|
+
code,
|
|
30073
|
+
modifier,
|
|
30074
|
+
values: flattenQueryValues(rawValue)
|
|
30075
|
+
});
|
|
30076
|
+
}
|
|
30077
|
+
return entries;
|
|
30078
|
+
}
|
|
30079
|
+
function findParam(params, code) {
|
|
30080
|
+
return params.find((p) => p.code === code);
|
|
30081
|
+
}
|
|
30082
|
+
function checkModifierAllowed(modifier, param) {
|
|
30083
|
+
const universal = modifier === "missing" || modifier === "not";
|
|
30084
|
+
const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
|
|
30085
|
+
if (universal || stringNative) {
|
|
30086
|
+
if (param.modifiers && !param.modifiers.includes(modifier)) {
|
|
30087
|
+
throw new Error(
|
|
30088
|
+
`Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
|
|
30089
|
+
);
|
|
30090
|
+
}
|
|
30091
|
+
return;
|
|
30092
|
+
}
|
|
30093
|
+
throw new Error(
|
|
30094
|
+
`Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
|
|
30095
|
+
);
|
|
30096
|
+
}
|
|
30097
|
+
function emitOne(opts) {
|
|
30098
|
+
const { param, modifier, rawValue, paramName, context } = opts;
|
|
30099
|
+
if (modifier === "missing") {
|
|
30100
|
+
const missing = parseMissingValue(rawValue);
|
|
30101
|
+
return emitFieldMissingPredicate({
|
|
30102
|
+
jsonbPath: param.jsonbPath,
|
|
30103
|
+
missing
|
|
30104
|
+
});
|
|
30105
|
+
}
|
|
30106
|
+
const negate = modifier === "not";
|
|
30107
|
+
const effectiveModifier = negate ? void 0 : modifier;
|
|
30108
|
+
const inner = emitForType({
|
|
30109
|
+
paramType: param.type,
|
|
30110
|
+
jsonbPath: param.jsonbPath,
|
|
30111
|
+
rawValue,
|
|
30112
|
+
paramName,
|
|
30113
|
+
modifier: effectiveModifier,
|
|
30114
|
+
context
|
|
30115
|
+
});
|
|
30116
|
+
if (!negate) {
|
|
30117
|
+
return inner;
|
|
30118
|
+
}
|
|
30119
|
+
return { sql: `NOT (${inner.sql})`, params: inner.params };
|
|
30120
|
+
}
|
|
30121
|
+
function parseMissingValue(raw) {
|
|
30122
|
+
if (raw === "true") return true;
|
|
30123
|
+
if (raw === "false") return false;
|
|
30124
|
+
throw new Error(
|
|
30125
|
+
`:missing requires a value of "true" or "false"; received "${raw}".`
|
|
30126
|
+
);
|
|
30127
|
+
}
|
|
30128
|
+
function emitForType(opts) {
|
|
30129
|
+
switch (opts.paramType) {
|
|
30130
|
+
case "token":
|
|
30131
|
+
return emitTokenPredicate({
|
|
30132
|
+
jsonbPath: opts.jsonbPath,
|
|
30133
|
+
rawValue: opts.rawValue,
|
|
30134
|
+
paramName: opts.paramName,
|
|
30135
|
+
context: opts.context
|
|
30136
|
+
});
|
|
30137
|
+
case "date":
|
|
30138
|
+
return emitDatePredicate({
|
|
30139
|
+
jsonbPath: opts.jsonbPath,
|
|
30140
|
+
rawValue: opts.rawValue,
|
|
30141
|
+
paramName: opts.paramName,
|
|
30142
|
+
context: opts.context
|
|
30143
|
+
});
|
|
30144
|
+
case "reference":
|
|
30145
|
+
return emitReferencePredicate({
|
|
30146
|
+
jsonbPath: opts.jsonbPath,
|
|
30147
|
+
rawValue: opts.rawValue,
|
|
30148
|
+
paramName: opts.paramName,
|
|
30149
|
+
context: opts.context
|
|
30150
|
+
});
|
|
30151
|
+
case "string":
|
|
30152
|
+
return emitStringPredicate({
|
|
30153
|
+
jsonbPath: opts.jsonbPath,
|
|
30154
|
+
rawValue: opts.rawValue,
|
|
30155
|
+
paramName: opts.paramName,
|
|
30156
|
+
modifier: opts.modifier,
|
|
30157
|
+
context: opts.context
|
|
30158
|
+
});
|
|
30159
|
+
}
|
|
30160
|
+
}
|
|
30161
|
+
function combineSearchPredicates(opts) {
|
|
30162
|
+
const entries = parseQueryEntries(opts.query);
|
|
30163
|
+
const groupSqls = [];
|
|
30164
|
+
const allParams = [];
|
|
30165
|
+
let paramIdx = 0;
|
|
30166
|
+
for (const entry of entries) {
|
|
30167
|
+
const registered = findParam(opts.registeredParams, entry.code);
|
|
30168
|
+
if (!registered) {
|
|
30169
|
+
continue;
|
|
30170
|
+
}
|
|
30171
|
+
if (entry.modifier !== void 0) {
|
|
30172
|
+
checkModifierAllowed(entry.modifier, registered);
|
|
30173
|
+
}
|
|
30174
|
+
if (entry.values.length === 0) {
|
|
30175
|
+
continue;
|
|
30176
|
+
}
|
|
30177
|
+
const fragmentSqls = [];
|
|
30178
|
+
let valueIdx = 0;
|
|
30179
|
+
for (const rawValue of entry.values) {
|
|
30180
|
+
const paramName = `p${paramIdx}v${valueIdx}`;
|
|
30181
|
+
const frag = emitOne({
|
|
30182
|
+
param: registered,
|
|
30183
|
+
modifier: entry.modifier,
|
|
30184
|
+
rawValue,
|
|
30185
|
+
paramName,
|
|
30186
|
+
context: opts.context
|
|
30187
|
+
});
|
|
30188
|
+
fragmentSqls.push(frag.sql);
|
|
30189
|
+
for (const p of frag.params) {
|
|
30190
|
+
allParams.push(p);
|
|
30191
|
+
}
|
|
30192
|
+
valueIdx++;
|
|
30193
|
+
}
|
|
30194
|
+
paramIdx++;
|
|
30195
|
+
if (fragmentSqls.length === 1) {
|
|
30196
|
+
groupSqls.push(fragmentSqls[0]);
|
|
30197
|
+
} else {
|
|
30198
|
+
groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
|
|
30199
|
+
}
|
|
30200
|
+
}
|
|
30201
|
+
if (groupSqls.length === 0) {
|
|
30202
|
+
return { sql: "", params: [] };
|
|
30203
|
+
}
|
|
30204
|
+
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
30205
|
+
}
|
|
30206
|
+
|
|
30207
|
+
// src/data/search/operations/generic-search-operation.ts
|
|
29821
30208
|
var DEFAULT_LIMIT7 = 100;
|
|
29822
|
-
function
|
|
29823
|
-
|
|
30209
|
+
function buildGenericSearchSql(opts) {
|
|
30210
|
+
const lines = [
|
|
29824
30211
|
"SELECT resource_id AS id, resource",
|
|
29825
30212
|
"FROM resources",
|
|
29826
30213
|
"WHERE tenant_id = :tenantId",
|
|
29827
30214
|
" AND workspace_id = :workspaceId",
|
|
29828
|
-
" AND resource_type =
|
|
29829
|
-
" AND deleted_at IS NULL"
|
|
29830
|
-
|
|
29831
|
-
|
|
29832
|
-
|
|
29833
|
-
|
|
30215
|
+
" AND resource_type = :resourceType",
|
|
30216
|
+
" AND deleted_at IS NULL"
|
|
30217
|
+
];
|
|
30218
|
+
if (opts.combinedSql.length > 0) {
|
|
30219
|
+
lines.push(` AND (${opts.combinedSql})`);
|
|
30220
|
+
}
|
|
30221
|
+
lines.push("ORDER BY last_updated DESC");
|
|
30222
|
+
lines.push("LIMIT :limit;");
|
|
30223
|
+
return lines.join("\n");
|
|
29834
30224
|
}
|
|
29835
|
-
async function
|
|
29836
|
-
const {
|
|
29837
|
-
const { tenantId, workspaceId } = context;
|
|
30225
|
+
async function genericSearchOperation(params) {
|
|
30226
|
+
const { resourceType, tenantId, workspaceId, query, resolver } = params;
|
|
29838
30227
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
29839
30228
|
const limit = params.limit ?? DEFAULT_LIMIT7;
|
|
29840
|
-
const
|
|
29841
|
-
|
|
29842
|
-
|
|
29843
|
-
|
|
29844
|
-
|
|
30229
|
+
const registeredParams = resolver(resourceType, tenantId);
|
|
30230
|
+
const context = { tenantId, workspaceId, resourceType };
|
|
30231
|
+
const combined = combineSearchPredicates({
|
|
30232
|
+
query,
|
|
30233
|
+
registeredParams,
|
|
30234
|
+
context
|
|
29845
30235
|
});
|
|
29846
|
-
const sql =
|
|
30236
|
+
const sql = buildGenericSearchSql({ combinedSql: combined.sql });
|
|
29847
30237
|
const queryParams = [
|
|
29848
30238
|
{ name: "tenantId", value: tenantId },
|
|
29849
30239
|
{ name: "workspaceId", value: workspaceId },
|
|
29850
|
-
{ name: "
|
|
29851
|
-
{ name: "
|
|
29852
|
-
|
|
30240
|
+
{ name: "resourceType", value: resourceType },
|
|
30241
|
+
{ name: "limit", value: limit },
|
|
30242
|
+
...combined.params
|
|
29853
30243
|
];
|
|
29854
30244
|
const rows = await runner.query(sql, queryParams);
|
|
29855
30245
|
const entries = rows.map((row) => ({
|
|
@@ -29859,14 +30249,60 @@ async function searchPatientsByGeneralPractitionerOperation(params) {
|
|
|
29859
30249
|
return { entries, total: entries.length };
|
|
29860
30250
|
}
|
|
29861
30251
|
|
|
29862
|
-
// src/data/
|
|
29863
|
-
|
|
29864
|
-
|
|
29865
|
-
|
|
29866
|
-
|
|
30252
|
+
// src/data/search/registry/patient-search-parameters.ts
|
|
30253
|
+
var PATIENT_SEARCH_PARAMETERS = [
|
|
30254
|
+
{ code: "gender", type: "token", jsonbPath: "$.gender" },
|
|
30255
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
|
|
30256
|
+
{ code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
|
|
30257
|
+
{
|
|
30258
|
+
code: "name",
|
|
30259
|
+
type: "string",
|
|
30260
|
+
jsonbPath: "$.name[*].text",
|
|
30261
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
30262
|
+
},
|
|
30263
|
+
{
|
|
30264
|
+
code: "family",
|
|
30265
|
+
type: "string",
|
|
30266
|
+
jsonbPath: "$.name[*].family",
|
|
30267
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
30268
|
+
},
|
|
30269
|
+
{
|
|
30270
|
+
code: "given",
|
|
30271
|
+
type: "string",
|
|
30272
|
+
jsonbPath: "$.name[*].given",
|
|
30273
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
30274
|
+
},
|
|
30275
|
+
{ code: "active", type: "token", jsonbPath: "$.active" },
|
|
30276
|
+
{ code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
|
|
30277
|
+
{
|
|
30278
|
+
code: "general-practitioner",
|
|
30279
|
+
type: "reference",
|
|
30280
|
+
jsonbPath: "$.generalPractitioner[*]"
|
|
30281
|
+
},
|
|
30282
|
+
{
|
|
30283
|
+
code: "organization",
|
|
30284
|
+
type: "reference",
|
|
30285
|
+
jsonbPath: "$.managingOrganization"
|
|
29867
30286
|
}
|
|
29868
|
-
|
|
29869
|
-
|
|
30287
|
+
];
|
|
30288
|
+
|
|
30289
|
+
// src/data/search/registry/resolver.ts
|
|
30290
|
+
var STATIC_SEARCH_PARAMETER_MAP = {
|
|
30291
|
+
Patient: PATIENT_SEARCH_PARAMETERS
|
|
30292
|
+
};
|
|
30293
|
+
var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
30294
|
+
function getRegisteredSearchParameters(resourceType) {
|
|
30295
|
+
return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
30296
|
+
}
|
|
30297
|
+
|
|
30298
|
+
// src/data/rest-api/routes/data/patient/patient-list-route.ts
|
|
30299
|
+
var PATIENT_RESOURCE_TYPE = "Patient";
|
|
30300
|
+
function stripModifier(key) {
|
|
30301
|
+
const idx = key.indexOf(":");
|
|
30302
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
30303
|
+
}
|
|
30304
|
+
function isResultParameter(key) {
|
|
30305
|
+
return key.startsWith("_");
|
|
29870
30306
|
}
|
|
29871
30307
|
function sendInvalidSearch4003(res, diagnostics) {
|
|
29872
30308
|
return res.status(400).json({
|
|
@@ -29874,41 +30310,93 @@ function sendInvalidSearch4003(res, diagnostics) {
|
|
|
29874
30310
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
29875
30311
|
});
|
|
29876
30312
|
}
|
|
29877
|
-
|
|
29878
|
-
const
|
|
29879
|
-
|
|
29880
|
-
|
|
30313
|
+
function extractSearchParamKeys(query) {
|
|
30314
|
+
const out = [];
|
|
30315
|
+
for (const rawKey of Object.keys(query)) {
|
|
30316
|
+
if (isResultParameter(rawKey)) {
|
|
30317
|
+
continue;
|
|
30318
|
+
}
|
|
30319
|
+
out.push({ rawKey, code: stripModifier(rawKey) });
|
|
30320
|
+
}
|
|
30321
|
+
return out;
|
|
30322
|
+
}
|
|
30323
|
+
function buildUnknownParamDiagnostics(unknownCodes) {
|
|
30324
|
+
const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
30325
|
+
const codes = unknownCodes.join(", ");
|
|
30326
|
+
const isPlural = unknownCodes.length !== 1;
|
|
30327
|
+
return [
|
|
30328
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Patient: ${codes}.`,
|
|
30329
|
+
`Valid codes: ${validCodes}.`
|
|
30330
|
+
].join(" ");
|
|
30331
|
+
}
|
|
30332
|
+
function findMalformedReference(query, searchParamKeys) {
|
|
30333
|
+
const referenceCodes = new Set(
|
|
30334
|
+
getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
29881
30335
|
);
|
|
29882
|
-
|
|
29883
|
-
if (
|
|
29884
|
-
|
|
29885
|
-
res,
|
|
29886
|
-
`?general-practitioner must be a typed reference like "Practitioner/<id>"; got "${generalPractitionerRef}".`
|
|
29887
|
-
);
|
|
30336
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
30337
|
+
if (!referenceCodes.has(code)) {
|
|
30338
|
+
continue;
|
|
29888
30339
|
}
|
|
29889
|
-
const
|
|
29890
|
-
|
|
29891
|
-
|
|
29892
|
-
|
|
29893
|
-
|
|
29894
|
-
|
|
29895
|
-
|
|
29896
|
-
|
|
29897
|
-
|
|
29898
|
-
|
|
29899
|
-
res,
|
|
29900
|
-
err,
|
|
29901
|
-
"GET /Patient?general-practitioner= search error:"
|
|
29902
|
-
);
|
|
30340
|
+
const raw = query[rawKey];
|
|
30341
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
30342
|
+
for (const v of values) {
|
|
30343
|
+
const trimmed = v.trim();
|
|
30344
|
+
if (trimmed.length === 0) {
|
|
30345
|
+
continue;
|
|
30346
|
+
}
|
|
30347
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
30348
|
+
return { rawKey, value: trimmed };
|
|
30349
|
+
}
|
|
29903
30350
|
}
|
|
29904
30351
|
}
|
|
29905
|
-
return
|
|
29906
|
-
|
|
29907
|
-
|
|
29908
|
-
|
|
29909
|
-
|
|
29910
|
-
|
|
29911
|
-
|
|
30352
|
+
return void 0;
|
|
30353
|
+
}
|
|
30354
|
+
async function listPatientsRoute(req, res) {
|
|
30355
|
+
const searchParamKeys = extractSearchParamKeys(
|
|
30356
|
+
req.query
|
|
30357
|
+
);
|
|
30358
|
+
if (searchParamKeys.length === 0) {
|
|
30359
|
+
return handleListRoute({
|
|
30360
|
+
req,
|
|
30361
|
+
res,
|
|
30362
|
+
basePath: BASE_PATH.PATIENT,
|
|
30363
|
+
listOperation: listPatientsOperation,
|
|
30364
|
+
errorLogContext: "GET /Patient list error:"
|
|
30365
|
+
});
|
|
30366
|
+
}
|
|
30367
|
+
const registered = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE);
|
|
30368
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
30369
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
30370
|
+
if (unknownCodes.length > 0) {
|
|
30371
|
+
return sendInvalidSearch4003(
|
|
30372
|
+
res,
|
|
30373
|
+
buildUnknownParamDiagnostics([...new Set(unknownCodes)])
|
|
30374
|
+
);
|
|
30375
|
+
}
|
|
30376
|
+
const malformedRef = findMalformedReference(
|
|
30377
|
+
req.query,
|
|
30378
|
+
searchParamKeys
|
|
30379
|
+
);
|
|
30380
|
+
if (malformedRef !== void 0) {
|
|
30381
|
+
return sendInvalidSearch4003(
|
|
30382
|
+
res,
|
|
30383
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
30384
|
+
);
|
|
30385
|
+
}
|
|
30386
|
+
const ctx = req.openhiContext;
|
|
30387
|
+
try {
|
|
30388
|
+
const result = await genericSearchOperation({
|
|
30389
|
+
resourceType: PATIENT_RESOURCE_TYPE,
|
|
30390
|
+
tenantId: ctx.tenantId,
|
|
30391
|
+
workspaceId: ctx.workspaceId,
|
|
30392
|
+
query: req.query,
|
|
30393
|
+
resolver: defaultSearchParameterResolver
|
|
30394
|
+
});
|
|
30395
|
+
const bundle = buildSearchsetBundle(BASE_PATH.PATIENT, result.entries);
|
|
30396
|
+
return res.json(bundle);
|
|
30397
|
+
} catch (err) {
|
|
30398
|
+
return sendOperationOutcome500(res, err, "GET /Patient search error:");
|
|
30399
|
+
}
|
|
29912
30400
|
}
|
|
29913
30401
|
|
|
29914
30402
|
// src/data/operations/data/patient/patient-update-operation.ts
|
|
@@ -33772,7 +34260,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
33772
34260
|
}
|
|
33773
34261
|
|
|
33774
34262
|
// src/data/rest-api/routes/data/schedule/schedule-list-route.ts
|
|
33775
|
-
function
|
|
34263
|
+
function singleStringQueryParam3(req, name) {
|
|
33776
34264
|
const v = req.query[name];
|
|
33777
34265
|
if (typeof v !== "string") {
|
|
33778
34266
|
return void 0;
|
|
@@ -33787,7 +34275,7 @@ function sendInvalidSearch4004(res, diagnostics) {
|
|
|
33787
34275
|
});
|
|
33788
34276
|
}
|
|
33789
34277
|
async function listSchedulesRoute(req, res) {
|
|
33790
|
-
const actorRef =
|
|
34278
|
+
const actorRef = singleStringQueryParam3(req, "actor");
|
|
33791
34279
|
if (actorRef !== void 0) {
|
|
33792
34280
|
if (parseTypedReference(actorRef) === void 0) {
|
|
33793
34281
|
return sendInvalidSearch4004(
|
|
@@ -37580,7 +38068,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
37580
38068
|
}
|
|
37581
38069
|
|
|
37582
38070
|
// src/data/rest-api/routes/data/task/task-list-route.ts
|
|
37583
|
-
function
|
|
38071
|
+
function singleStringQueryParam4(req, name) {
|
|
37584
38072
|
const v = req.query[name];
|
|
37585
38073
|
if (typeof v !== "string") {
|
|
37586
38074
|
return void 0;
|
|
@@ -37595,8 +38083,8 @@ function sendInvalidSearch4005(res, diagnostics) {
|
|
|
37595
38083
|
});
|
|
37596
38084
|
}
|
|
37597
38085
|
async function listTasksRoute(req, res) {
|
|
37598
|
-
const ownerRef =
|
|
37599
|
-
const requesterRef =
|
|
38086
|
+
const ownerRef = singleStringQueryParam4(req, "owner");
|
|
38087
|
+
const requesterRef = singleStringQueryParam4(req, "requester");
|
|
37600
38088
|
if (ownerRef !== void 0 && requesterRef !== void 0) {
|
|
37601
38089
|
return sendInvalidSearch4005(
|
|
37602
38090
|
res,
|